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::renderer::ImageSource;
9use crate::shaders::ShaderConfig;
10use crate::elements::{
11    FloatingAttachToElement, FloatingClipToElement, PointerCaptureMode,
12};
13use crate::layout::{LayoutDirection, CornerRadius};
14use crate::math::{BoundingBox, Dimensions, Vector2};
15use crate::text::{TextConfig, WrapMode};
16
17const DEFAULT_MAX_ELEMENT_COUNT: i32 = 8192;
18const DEFAULT_MAX_MEASURE_TEXT_WORD_CACHE_COUNT: i32 = 16384;
19const MAXFLOAT: f32 = 3.40282346638528859812e+38;
20const EPSILON: f32 = 0.01;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23#[repr(u8)]
24pub enum SizingType {
25    #[default]
26    Fit,
27    Grow,
28    Percent,
29    Fixed,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33#[repr(u8)]
34pub enum RenderCommandType {
35    #[default]
36    None,
37    Rectangle,
38    Border,
39    Text,
40    Image,
41    ScissorStart,
42    ScissorEnd,
43    Custom,
44    GroupBegin,
45    GroupEnd,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
49#[repr(u8)]
50pub enum PointerDataInteractionState {
51    PressedThisFrame,
52    Pressed,
53    ReleasedThisFrame,
54    #[default]
55    Released,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum ArrowDirection {
60    Left,
61    Right,
62    Up,
63    Down,
64}
65
66/// Actions that can be performed on a focused text input.
67#[derive(Debug, Clone)]
68pub enum TextInputAction {
69    MoveLeft { shift: bool },
70    MoveRight { shift: bool },
71    MoveWordLeft { shift: bool },
72    MoveWordRight { shift: bool },
73    MoveHome { shift: bool },
74    MoveEnd { shift: bool },
75    MoveUp { shift: bool },
76    MoveDown { shift: bool },
77    Backspace,
78    Delete,
79    BackspaceWord,
80    DeleteWord,
81    SelectAll,
82    Copy,
83    Cut,
84    Paste { text: String },
85    Submit,
86    Undo,
87    Redo,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91#[repr(u8)]
92pub enum ElementConfigType {
93    Shared,
94    Text,
95    Image,
96    Floating,
97    Custom,
98    Clip,
99    Border,
100    Aspect,
101    TextInput,
102}
103
104#[derive(Debug, Clone, Copy, Default)]
105pub struct SizingMinMax {
106    pub min: f32,
107    pub max: f32,
108}
109
110#[derive(Debug, Clone, Copy, Default)]
111pub struct SizingAxis {
112    pub type_: SizingType,
113    pub min_max: SizingMinMax,
114    pub percent: f32,
115}
116
117#[derive(Debug, Clone, Copy, Default)]
118pub struct SizingConfig {
119    pub width: SizingAxis,
120    pub height: SizingAxis,
121}
122
123#[derive(Debug, Clone, Copy, Default)]
124pub struct PaddingConfig {
125    pub left: u16,
126    pub right: u16,
127    pub top: u16,
128    pub bottom: u16,
129}
130
131#[derive(Debug, Clone, Copy, Default)]
132pub struct ChildAlignmentConfig {
133    pub x: AlignX,
134    pub y: AlignY,
135}
136
137#[derive(Debug, Clone, Copy, Default)]
138pub struct LayoutConfig {
139    pub sizing: SizingConfig,
140    pub padding: PaddingConfig,
141    pub child_gap: u16,
142    pub child_alignment: ChildAlignmentConfig,
143    pub layout_direction: LayoutDirection,
144}
145
146
147#[derive(Debug, Clone, Copy)]
148pub struct VisualRotationConfig {
149    /// Rotation angle in radians.
150    pub rotation_radians: f32,
151    /// Normalized pivot X (0.0 = left, 0.5 = center, 1.0 = right). Default 0.5.
152    pub pivot_x: f32,
153    /// Normalized pivot Y (0.0 = top, 0.5 = center, 1.0 = bottom). Default 0.5.
154    pub pivot_y: f32,
155    /// Mirror horizontally.
156    pub flip_x: bool,
157    /// Mirror vertically.
158    pub flip_y: bool,
159}
160
161impl Default for VisualRotationConfig {
162    fn default() -> Self {
163        Self {
164            rotation_radians: 0.0,
165            pivot_x: 0.5,
166            pivot_y: 0.5,
167            flip_x: false,
168            flip_y: false,
169        }
170    }
171}
172
173impl VisualRotationConfig {
174    /// Returns `true` when the config is effectively a no-op.
175    pub fn is_noop(&self) -> bool {
176        self.rotation_radians == 0.0 && !self.flip_x && !self.flip_y
177    }
178}
179
180#[derive(Debug, Clone, Copy)]
181pub struct ShapeRotationConfig {
182    /// Rotation angle in radians.
183    pub rotation_radians: f32,
184    /// Mirror horizontally (applied before rotation).
185    pub flip_x: bool,
186    /// Mirror vertically (applied before rotation).
187    pub flip_y: bool,
188}
189
190impl Default for ShapeRotationConfig {
191    fn default() -> Self {
192        Self {
193            rotation_radians: 0.0,
194            flip_x: false,
195            flip_y: false,
196        }
197    }
198}
199
200impl ShapeRotationConfig {
201    /// Returns `true` when the config is effectively a no-op.
202    pub fn is_noop(&self) -> bool {
203        self.rotation_radians == 0.0 && !self.flip_x && !self.flip_y
204    }
205}
206
207#[derive(Debug, Clone, Copy, Default)]
208pub struct FloatingAttachPoints {
209    pub element_x: AlignX,
210    pub element_y: AlignY,
211    pub parent_x: AlignX,
212    pub parent_y: AlignY,
213}
214
215#[derive(Debug, Clone, Copy, Default)]
216pub struct FloatingConfig {
217    pub offset: Vector2,
218    pub parent_id: u32,
219    pub z_index: i16,
220    pub attach_points: FloatingAttachPoints,
221    pub pointer_capture_mode: PointerCaptureMode,
222    pub attach_to: FloatingAttachToElement,
223    pub clip_to: FloatingClipToElement,
224}
225
226#[derive(Debug, Clone, Copy, Default)]
227pub struct ClipConfig {
228    pub horizontal: bool,
229    pub vertical: bool,
230    pub scroll_x: bool,
231    pub scroll_y: bool,
232    pub child_offset: Vector2,
233}
234
235#[derive(Debug, Clone, Copy, Default)]
236pub struct BorderWidth {
237    pub left: u16,
238    pub right: u16,
239    pub top: u16,
240    pub bottom: u16,
241    pub between_children: u16,
242}
243
244impl BorderWidth {
245    pub fn is_zero(&self) -> bool {
246        self.left == 0
247            && self.right == 0
248            && self.top == 0
249            && self.bottom == 0
250            && self.between_children == 0
251    }
252}
253
254#[derive(Debug, Clone, Copy, Default)]
255pub struct BorderConfig {
256    pub color: Color,
257    pub width: BorderWidth,
258}
259
260/// The top-level element declaration.
261#[derive(Debug, Clone)]
262pub struct ElementDeclaration<CustomElementData: Clone + Default + std::fmt::Debug = ()> {
263    pub layout: LayoutConfig,
264    pub background_color: Color,
265    pub corner_radius: CornerRadius,
266    pub aspect_ratio: f32,
267    pub image_data: Option<ImageSource>,
268    pub floating: FloatingConfig,
269    pub custom_data: Option<CustomElementData>,
270    pub clip: ClipConfig,
271    pub border: BorderConfig,
272    pub user_data: usize,
273    pub effects: Vec<ShaderConfig>,
274    pub shaders: Vec<ShaderConfig>,
275    pub visual_rotation: Option<VisualRotationConfig>,
276    pub shape_rotation: Option<ShapeRotationConfig>,
277    pub accessibility: Option<crate::accessibility::AccessibilityConfig>,
278    pub text_input: Option<crate::text_input::TextInputConfig>,
279    pub preserve_focus: bool,
280}
281
282impl<CustomElementData: Clone + Default + std::fmt::Debug> Default for ElementDeclaration<CustomElementData> {
283    fn default() -> Self {
284        Self {
285            layout: LayoutConfig::default(),
286            background_color: Color::rgba(0.0, 0.0, 0.0, 0.0),
287            corner_radius: CornerRadius::default(),
288            aspect_ratio: 0.0,
289            image_data: None,
290            floating: FloatingConfig::default(),
291            custom_data: None,
292            clip: ClipConfig::default(),
293            border: BorderConfig::default(),
294            user_data: 0,
295            effects: Vec::new(),
296            shaders: Vec::new(),
297            visual_rotation: None,
298            shape_rotation: None,
299            accessibility: None,
300            text_input: None,
301            preserve_focus: false,
302        }
303    }
304}
305
306use crate::id::{Id, StringId};
307
308#[derive(Debug, Clone, Copy, Default)]
309struct SharedElementConfig {
310    background_color: Color,
311    corner_radius: CornerRadius,
312    user_data: usize,
313}
314
315#[derive(Debug, Clone, Copy)]
316struct ElementConfig {
317    config_type: ElementConfigType,
318    config_index: usize,
319}
320
321#[derive(Debug, Clone, Copy, Default)]
322struct ElementConfigSlice {
323    start: usize,
324    length: i32,
325}
326
327#[derive(Debug, Clone, Copy, Default)]
328struct WrappedTextLine {
329    dimensions: Dimensions,
330    start: usize,
331    length: usize,
332}
333
334#[derive(Debug, Clone)]
335struct TextElementData {
336    text: String,
337    preferred_dimensions: Dimensions,
338    element_index: i32,
339    wrapped_lines_start: usize,
340    wrapped_lines_length: i32,
341}
342
343#[derive(Debug, Clone, Copy, Default)]
344struct LayoutElement {
345    // Children data (for non-text elements)
346    children_start: usize,
347    children_length: u16,
348    // Text data (for text elements)
349    text_data_index: i32, // -1 means no text, >= 0 is index
350    dimensions: Dimensions,
351    min_dimensions: Dimensions,
352    layout_config_index: usize,
353    element_configs: ElementConfigSlice,
354    id: u32,
355    floating_children_count: u16,
356}
357
358#[derive(Default)]
359struct LayoutElementHashMapItem {
360    bounding_box: BoundingBox,
361    element_id: Id,
362    layout_element_index: i32,
363    on_hover_fn: Option<Box<dyn FnMut(Id, PointerData)>>,
364    on_press_fn: Option<Box<dyn FnMut(Id, PointerData)>>,
365    on_release_fn: Option<Box<dyn FnMut(Id, PointerData)>>,
366    on_focus_fn: Option<Box<dyn FnMut(Id)>>,
367    on_unfocus_fn: Option<Box<dyn FnMut(Id)>>,
368    on_text_changed_fn: Option<Box<dyn FnMut(&str)>>,
369    on_text_submit_fn: Option<Box<dyn FnMut(&str)>>,
370    is_text_input: bool,
371    preserve_focus: bool,
372    generation: u32,
373    collision: bool,
374    collapsed: bool,
375}
376
377impl Clone for LayoutElementHashMapItem {
378    fn clone(&self) -> Self {
379        Self {
380            bounding_box: self.bounding_box,
381            element_id: self.element_id.clone(),
382            layout_element_index: self.layout_element_index,
383            on_hover_fn: None, // Callbacks are not cloneable
384            on_press_fn: None,
385            on_release_fn: None,
386            on_focus_fn: None,
387            on_unfocus_fn: None,
388            on_text_changed_fn: None,
389            on_text_submit_fn: None,
390            is_text_input: self.is_text_input,
391            preserve_focus: self.preserve_focus,
392            generation: self.generation,
393            collision: self.collision,
394            collapsed: self.collapsed,
395        }
396    }
397}
398
399#[derive(Debug, Clone, Copy, Default)]
400struct MeasuredWord {
401    start_offset: i32,
402    length: i32,
403    width: f32,
404    next: i32,
405}
406
407#[derive(Debug, Clone, Copy, Default)]
408#[allow(dead_code)]
409struct MeasureTextCacheItem {
410    unwrapped_dimensions: Dimensions,
411    measured_words_start_index: i32,
412    min_width: f32,
413    contains_newlines: bool,
414    id: u32,
415    generation: u32,
416}
417
418#[derive(Debug, Clone, Copy, Default)]
419#[allow(dead_code)]
420struct ScrollContainerDataInternal {
421    bounding_box: BoundingBox,
422    content_size: Dimensions,
423    scroll_origin: Vector2,
424    pointer_origin: Vector2,
425    scroll_momentum: Vector2,
426    scroll_position: Vector2,
427    previous_delta: Vector2,
428    element_id: u32,
429    layout_element_index: i32,
430    open_this_frame: bool,
431    pointer_scroll_active: bool,
432}
433
434#[derive(Debug, Clone, Copy, Default)]
435struct LayoutElementTreeNode {
436    layout_element_index: i32,
437    position: Vector2,
438    next_child_offset: Vector2,
439}
440
441#[derive(Debug, Clone, Copy, Default)]
442struct LayoutElementTreeRoot {
443    layout_element_index: i32,
444    parent_id: u32,
445    clip_element_id: u32,
446    z_index: i16,
447    pointer_offset: Vector2,
448}
449
450#[derive(Debug, Clone, Copy)]
451struct FocusableEntry {
452    element_id: u32,
453    tab_index: Option<i32>,
454    insertion_order: u32,
455}
456
457#[derive(Debug, Clone, Copy, Default)]
458pub struct PointerData {
459    pub position: Vector2,
460    pub state: PointerDataInteractionState,
461}
462
463#[derive(Debug, Clone, Copy, Default)]
464#[allow(dead_code)]
465struct BooleanWarnings {
466    max_elements_exceeded: bool,
467    text_measurement_fn_not_set: bool,
468    max_text_measure_cache_exceeded: bool,
469    max_render_commands_exceeded: bool,
470}
471
472#[derive(Debug, Clone)]
473pub struct InternalRenderCommand<CustomElementData: Clone + Default + std::fmt::Debug = ()> {
474    pub bounding_box: BoundingBox,
475    pub command_type: RenderCommandType,
476    pub render_data: InternalRenderData<CustomElementData>,
477    pub user_data: usize,
478    pub id: u32,
479    pub z_index: i16,
480    pub effects: Vec<ShaderConfig>,
481    pub visual_rotation: Option<VisualRotationConfig>,
482    pub shape_rotation: Option<ShapeRotationConfig>,
483}
484
485#[derive(Debug, Clone)]
486pub enum InternalRenderData<CustomElementData: Clone + Default + std::fmt::Debug = ()> {
487    None,
488    Rectangle {
489        background_color: Color,
490        corner_radius: CornerRadius,
491    },
492    Text {
493        text: String,
494        text_color: Color,
495        font_size: u16,
496        letter_spacing: u16,
497        line_height: u16,
498        font_asset: Option<&'static crate::renderer::FontAsset>,
499    },
500    Image {
501        background_color: Color,
502        corner_radius: CornerRadius,
503        image_data: ImageSource,
504    },
505    Custom {
506        background_color: Color,
507        corner_radius: CornerRadius,
508        custom_data: CustomElementData,
509    },
510    Border {
511        color: Color,
512        corner_radius: CornerRadius,
513        width: BorderWidth,
514    },
515    Clip {
516        horizontal: bool,
517        vertical: bool,
518    },
519}
520
521impl<CustomElementData: Clone + Default + std::fmt::Debug> Default for InternalRenderData<CustomElementData> {
522    fn default() -> Self {
523        Self::None
524    }
525}
526
527impl<CustomElementData: Clone + Default + std::fmt::Debug> Default for InternalRenderCommand<CustomElementData> {
528    fn default() -> Self {
529        Self {
530            bounding_box: BoundingBox::default(),
531            command_type: RenderCommandType::None,
532            render_data: InternalRenderData::None,
533            user_data: 0,
534            id: 0,
535            z_index: 0,
536            effects: Vec::new(),
537            visual_rotation: None,
538            shape_rotation: None,
539        }
540    }
541}
542
543#[derive(Debug, Clone, Copy)]
544pub struct ScrollContainerData {
545    pub scroll_position: Vector2,
546    pub scroll_container_dimensions: Dimensions,
547    pub content_dimensions: Dimensions,
548    pub horizontal: bool,
549    pub vertical: bool,
550    pub found: bool,
551}
552
553impl Default for ScrollContainerData {
554    fn default() -> Self {
555        Self {
556            scroll_position: Vector2::default(),
557            scroll_container_dimensions: Dimensions::default(),
558            content_dimensions: Dimensions::default(),
559            horizontal: false,
560            vertical: false,
561            found: false,
562        }
563    }
564}
565
566pub struct PlyContext<CustomElementData: Clone + Default + std::fmt::Debug = ()> {
567    // Settings
568    pub max_element_count: i32,
569    pub max_measure_text_cache_word_count: i32,
570    pub debug_mode_enabled: bool,
571    pub culling_disabled: bool,
572    pub external_scroll_handling_enabled: bool,
573    pub debug_selected_element_id: u32,
574    pub generation: u32,
575
576    // Warnings
577    boolean_warnings: BooleanWarnings,
578
579    // Pointer info
580    pointer_info: PointerData,
581    pub layout_dimensions: Dimensions,
582
583    // Dynamic element tracking
584    dynamic_element_index: u32,
585
586    // Measure text callback
587    measure_text_fn: Option<Box<dyn Fn(&str, &TextConfig) -> Dimensions>>,
588
589    // Layout elements
590    layout_elements: Vec<LayoutElement>,
591    render_commands: Vec<InternalRenderCommand<CustomElementData>>,
592    open_layout_element_stack: Vec<i32>,
593    layout_element_children: Vec<i32>,
594    layout_element_children_buffer: Vec<i32>,
595    text_element_data: Vec<TextElementData>,
596    aspect_ratio_element_indexes: Vec<i32>,
597    reusable_element_index_buffer: Vec<i32>,
598    layout_element_clip_element_ids: Vec<i32>,
599
600    // Configs
601    layout_configs: Vec<LayoutConfig>,
602    element_configs: Vec<ElementConfig>,
603    text_element_configs: Vec<TextConfig>,
604    aspect_ratio_configs: Vec<f32>,
605    image_element_configs: Vec<ImageSource>,
606    floating_element_configs: Vec<FloatingConfig>,
607    clip_element_configs: Vec<ClipConfig>,
608    custom_element_configs: Vec<CustomElementData>,
609    border_element_configs: Vec<BorderConfig>,
610    shared_element_configs: Vec<SharedElementConfig>,
611
612    // Per-element shader effects (indexed by layout element index)
613    element_effects: Vec<Vec<ShaderConfig>>,
614    // Per-element group shaders (indexed by layout element index)
615    element_shaders: Vec<Vec<ShaderConfig>>,
616
617    // Per-element visual rotation (indexed by layout element index)
618    element_visual_rotations: Vec<Option<VisualRotationConfig>>,
619
620    // Per-element shape rotation (indexed by layout element index)
621    element_shape_rotations: Vec<Option<ShapeRotationConfig>>,
622    // Original dimensions before AABB expansion (only set when shape_rotation is active)
623    element_pre_rotation_dimensions: Vec<Option<Dimensions>>,
624
625    // String IDs for debug
626    layout_element_id_strings: Vec<StringId>,
627
628    // Text wrapping
629    wrapped_text_lines: Vec<WrappedTextLine>,
630
631    // Tree traversal
632    tree_node_array: Vec<LayoutElementTreeNode>,
633    layout_element_tree_roots: Vec<LayoutElementTreeRoot>,
634
635    // Layout element map: element id -> element data (bounding box, hover callback, etc.)
636    layout_element_map: FxHashMap<u32, LayoutElementHashMapItem>,
637
638    // Text measurement cache: content hash -> measured dimensions and words
639    measure_text_cache: FxHashMap<u32, MeasureTextCacheItem>,
640    measured_words: Vec<MeasuredWord>,
641    measured_words_free_list: Vec<i32>,
642
643    // Clip/scroll
644    open_clip_element_stack: Vec<i32>,
645    pointer_over_ids: Vec<Id>,
646    pressed_element_ids: Vec<Id>,
647    scroll_container_datas: Vec<ScrollContainerDataInternal>,
648
649    // Accessibility / focus
650    pub focused_element_id: u32, // 0 = no focus
651    /// True when focus was set via keyboard (Tab/arrow keys), false when via mouse click.
652    pub(crate) focus_from_keyboard: bool,
653    focusable_elements: Vec<FocusableEntry>,
654    pub(crate) accessibility_configs: FxHashMap<u32, crate::accessibility::AccessibilityConfig>,
655    pub(crate) accessibility_element_order: Vec<u32>,
656
657    // Text input
658    pub(crate) text_edit_states: FxHashMap<u32, crate::text_input::TextEditState>,
659    text_input_configs: Vec<crate::text_input::TextInputConfig>,
660    /// Set of element IDs that are text inputs this frame.
661    pub(crate) text_input_element_ids: Vec<u32>,
662    /// Pending click on a text input: (element_id, click_x_relative, click_y_relative, shift_held)
663    pub(crate) pending_text_click: Option<(u32, f32, f32, bool)>,
664    /// Text input drag-scroll state (mobile-first: drag scrolls, doesn't select).
665    pub(crate) text_input_drag_active: bool,
666    pub(crate) text_input_drag_origin: crate::math::Vector2,
667    pub(crate) text_input_drag_scroll_origin: crate::math::Vector2,
668    pub(crate) text_input_drag_element_id: u32,
669    /// Current absolute time in seconds (set by lib.rs each frame).
670    pub(crate) current_time: f64,
671    /// Delta time for the current frame in seconds (set by lib.rs each frame).
672    pub(crate) frame_delta_time: f32,
673
674    // Visited flags for DFS
675    tree_node_visited: Vec<bool>,
676
677    // Dynamic string data (for int-to-string etc.)
678    dynamic_string_data: Vec<u8>,
679
680    // Font height cache: (font_key, font_size) -> height in pixels.
681    // Avoids repeated calls to measure_fn("Mg", ...) which are expensive.
682    font_height_cache: FxHashMap<(&'static str, u16), f32>,
683
684    // The key of the default font (set by Ply::new, used in debug view)
685    pub(crate) default_font_key: &'static str,
686
687    // Debug view: heap-allocated strings that survive the frame
688}
689
690fn hash_data_scalar(data: &[u8]) -> u64 {
691    let mut hash: u64 = 0;
692    for &b in data {
693        hash = hash.wrapping_add(b as u64);
694        hash = hash.wrapping_add(hash << 10);
695        hash ^= hash >> 6;
696    }
697    hash
698}
699
700pub fn hash_string(key: &str, seed: u32) -> Id {
701    let mut hash: u32 = seed;
702    for b in key.bytes() {
703        hash = hash.wrapping_add(b as u32);
704        hash = hash.wrapping_add(hash << 10);
705        hash ^= hash >> 6;
706    }
707    hash = hash.wrapping_add(hash << 3);
708    hash ^= hash >> 11;
709    hash = hash.wrapping_add(hash << 15);
710    Id {
711        id: hash.wrapping_add(1),
712        offset: 0,
713        base_id: hash.wrapping_add(1),
714        string_id: StringId::from_str(key),
715    }
716}
717
718pub fn hash_string_with_offset(key: &str, offset: u32, seed: u32) -> Id {
719    let mut base: u32 = seed;
720    for b in key.bytes() {
721        base = base.wrapping_add(b as u32);
722        base = base.wrapping_add(base << 10);
723        base ^= base >> 6;
724    }
725    let mut hash = base;
726    hash = hash.wrapping_add(offset);
727    hash = hash.wrapping_add(hash << 10);
728    hash ^= hash >> 6;
729
730    hash = hash.wrapping_add(hash << 3);
731    base = base.wrapping_add(base << 3);
732    hash ^= hash >> 11;
733    base ^= base >> 11;
734    hash = hash.wrapping_add(hash << 15);
735    base = base.wrapping_add(base << 15);
736    Id {
737        id: hash.wrapping_add(1),
738        offset,
739        base_id: base.wrapping_add(1),
740        string_id: StringId::from_str(key),
741    }
742}
743
744fn hash_number(offset: u32, seed: u32) -> Id {
745    let mut hash = seed;
746    hash = hash.wrapping_add(offset.wrapping_add(48));
747    hash = hash.wrapping_add(hash << 10);
748    hash ^= hash >> 6;
749    hash = hash.wrapping_add(hash << 3);
750    hash ^= hash >> 11;
751    hash = hash.wrapping_add(hash << 15);
752    Id {
753        id: hash.wrapping_add(1),
754        offset,
755        base_id: seed,
756        string_id: StringId::empty(),
757    }
758}
759
760fn hash_string_contents_with_config(
761    text: &str,
762    config: &TextConfig,
763) -> u32 {
764    let mut hash: u32 = (hash_data_scalar(text.as_bytes()) % u32::MAX as u64) as u32;
765    // Fold in font key bytes
766    for &b in config.font_asset.map(|a| a.key()).unwrap_or("").as_bytes() {
767        hash = hash.wrapping_add(b as u32);
768        hash = hash.wrapping_add(hash << 10);
769        hash ^= hash >> 6;
770    }
771    hash = hash.wrapping_add(config.font_size as u32);
772    hash = hash.wrapping_add(hash << 10);
773    hash ^= hash >> 6;
774    hash = hash.wrapping_add(config.letter_spacing as u32);
775    hash = hash.wrapping_add(hash << 10);
776    hash ^= hash >> 6;
777    hash = hash.wrapping_add(hash << 3);
778    hash ^= hash >> 11;
779    hash = hash.wrapping_add(hash << 15);
780    hash.wrapping_add(1)
781}
782
783fn float_equal(left: f32, right: f32) -> bool {
784    let diff = left - right;
785    diff < EPSILON && diff > -EPSILON
786}
787
788fn point_is_inside_rect(point: Vector2, rect: BoundingBox) -> bool {
789    point.x >= rect.x
790        && point.x <= rect.x + rect.width
791        && point.y >= rect.y
792        && point.y <= rect.y + rect.height
793}
794
795impl<CustomElementData: Clone + Default + std::fmt::Debug> PlyContext<CustomElementData> {
796    pub fn new(dimensions: Dimensions) -> Self {
797        let max_element_count = DEFAULT_MAX_ELEMENT_COUNT;
798        let max_measure_text_cache_word_count = DEFAULT_MAX_MEASURE_TEXT_WORD_CACHE_COUNT;
799
800        let ctx = Self {
801            max_element_count,
802            max_measure_text_cache_word_count,
803            debug_mode_enabled: false,
804            culling_disabled: false,
805            external_scroll_handling_enabled: false,
806            debug_selected_element_id: 0,
807            generation: 0,
808            boolean_warnings: BooleanWarnings::default(),
809            pointer_info: PointerData::default(),
810            layout_dimensions: dimensions,
811            dynamic_element_index: 0,
812            measure_text_fn: None,
813            layout_elements: Vec::new(),
814            render_commands: Vec::new(),
815            open_layout_element_stack: Vec::new(),
816            layout_element_children: Vec::new(),
817            layout_element_children_buffer: Vec::new(),
818            text_element_data: Vec::new(),
819            aspect_ratio_element_indexes: Vec::new(),
820            reusable_element_index_buffer: Vec::new(),
821            layout_element_clip_element_ids: Vec::new(),
822            layout_configs: Vec::new(),
823            element_configs: Vec::new(),
824            text_element_configs: Vec::new(),
825            aspect_ratio_configs: Vec::new(),
826            image_element_configs: Vec::new(),
827            floating_element_configs: Vec::new(),
828            clip_element_configs: Vec::new(),
829            custom_element_configs: Vec::new(),
830            border_element_configs: Vec::new(),
831            shared_element_configs: Vec::new(),
832            element_effects: Vec::new(),
833            element_shaders: Vec::new(),
834            element_visual_rotations: Vec::new(),
835            element_shape_rotations: Vec::new(),
836            element_pre_rotation_dimensions: Vec::new(),
837            layout_element_id_strings: Vec::new(),
838            wrapped_text_lines: Vec::new(),
839            tree_node_array: Vec::new(),
840            layout_element_tree_roots: Vec::new(),
841            layout_element_map: FxHashMap::default(),
842            measure_text_cache: FxHashMap::default(),
843            measured_words: Vec::new(),
844            measured_words_free_list: Vec::new(),
845            open_clip_element_stack: Vec::new(),
846            pointer_over_ids: Vec::new(),
847            pressed_element_ids: Vec::new(),
848            scroll_container_datas: Vec::new(),
849            focused_element_id: 0,
850            focus_from_keyboard: false,
851            focusable_elements: Vec::new(),
852            accessibility_configs: FxHashMap::default(),
853            accessibility_element_order: Vec::new(),
854            text_edit_states: FxHashMap::default(),
855            text_input_configs: Vec::new(),
856            text_input_element_ids: Vec::new(),
857            pending_text_click: None,
858            text_input_drag_active: false,
859            text_input_drag_origin: Vector2::default(),
860            text_input_drag_scroll_origin: Vector2::default(),
861            text_input_drag_element_id: 0,
862            current_time: 0.0,
863            frame_delta_time: 0.0,
864            tree_node_visited: Vec::new(),
865            dynamic_string_data: Vec::new(),
866            font_height_cache: FxHashMap::default(),
867            default_font_key: "",
868        };
869        ctx
870    }
871
872    fn get_open_layout_element(&self) -> usize {
873        let idx = *self.open_layout_element_stack.last().unwrap();
874        idx as usize
875    }
876
877    /// Returns the internal u32 id of the currently open element.
878    pub fn get_open_element_id(&self) -> u32 {
879        let open_idx = self.get_open_layout_element();
880        self.layout_elements[open_idx].id
881    }
882
883    pub fn get_parent_element_id(&self) -> u32 {
884        let stack_len = self.open_layout_element_stack.len();
885        let parent_idx = self.open_layout_element_stack[stack_len - 2] as usize;
886        self.layout_elements[parent_idx].id
887    }
888
889    fn add_hash_map_item(
890        &mut self,
891        element_id: &Id,
892        layout_element_index: i32,
893    ) {
894        let gen = self.generation;
895        match self.layout_element_map.entry(element_id.id) {
896            std::collections::hash_map::Entry::Occupied(mut entry) => {
897                let item = entry.get_mut();
898                if item.generation <= gen {
899                    item.element_id = element_id.clone();
900                    item.generation = gen + 1;
901                    item.layout_element_index = layout_element_index;
902                    item.collision = false;
903                    item.on_hover_fn = None;
904                    item.on_press_fn = None;
905                    item.on_release_fn = None;
906                    item.on_focus_fn = None;
907                    item.on_unfocus_fn = None;
908                    item.on_text_changed_fn = None;
909                    item.on_text_submit_fn = None;
910                    item.is_text_input = false;
911                    item.preserve_focus = false;
912                } else {
913                    // Duplicate ID
914                    item.collision = true;
915                }
916            }
917            std::collections::hash_map::Entry::Vacant(entry) => {
918                entry.insert(LayoutElementHashMapItem {
919                    element_id: element_id.clone(),
920                    layout_element_index,
921                    generation: gen + 1,
922                    bounding_box: BoundingBox::default(),
923                    on_hover_fn: None,
924                    on_press_fn: None,
925                    on_release_fn: None,
926                    on_focus_fn: None,
927                    on_unfocus_fn: None,
928                    on_text_changed_fn: None,
929                    on_text_submit_fn: None,
930                    is_text_input: false,
931                    preserve_focus: false,
932                    collision: false,
933                    collapsed: false,
934                });
935            }
936        }
937    }
938
939    fn generate_id_for_anonymous_element(&mut self, open_element_index: usize) -> Id {
940        let stack_len = self.open_layout_element_stack.len();
941        let parent_idx = self.open_layout_element_stack[stack_len - 2] as usize;
942        let parent = &self.layout_elements[parent_idx];
943        let offset =
944            parent.children_length as u32 + parent.floating_children_count as u32;
945        let parent_id = parent.id;
946        let element_id = hash_number(offset, parent_id);
947        self.layout_elements[open_element_index].id = element_id.id;
948        self.add_hash_map_item(&element_id, open_element_index as i32);
949        if self.debug_mode_enabled {
950            self.layout_element_id_strings.push(element_id.string_id.clone());
951        }
952        element_id
953    }
954
955    fn element_has_config(
956        &self,
957        element_index: usize,
958        config_type: ElementConfigType,
959    ) -> bool {
960        let element = &self.layout_elements[element_index];
961        let start = element.element_configs.start;
962        let length = element.element_configs.length;
963        for i in 0..length {
964            let config = &self.element_configs[start + i as usize];
965            if config.config_type == config_type {
966                return true;
967            }
968        }
969        false
970    }
971
972    fn find_element_config_index(
973        &self,
974        element_index: usize,
975        config_type: ElementConfigType,
976    ) -> Option<usize> {
977        let element = &self.layout_elements[element_index];
978        let start = element.element_configs.start;
979        let length = element.element_configs.length;
980        for i in 0..length {
981            let config = &self.element_configs[start + i as usize];
982            if config.config_type == config_type {
983                return Some(config.config_index);
984            }
985        }
986        None
987    }
988
989    fn update_aspect_ratio_box(&mut self, element_index: usize) {
990        if let Some(config_idx) =
991            self.find_element_config_index(element_index, ElementConfigType::Aspect)
992        {
993            let aspect_ratio = self.aspect_ratio_configs[config_idx];
994            if aspect_ratio == 0.0 {
995                return;
996            }
997            let elem = &mut self.layout_elements[element_index];
998            if elem.dimensions.width == 0.0 && elem.dimensions.height != 0.0 {
999                elem.dimensions.width = elem.dimensions.height * aspect_ratio;
1000            } else if elem.dimensions.width != 0.0 && elem.dimensions.height == 0.0 {
1001                elem.dimensions.height = elem.dimensions.width * (1.0 / aspect_ratio);
1002            }
1003        }
1004    }
1005
1006    pub fn store_text_element_config(
1007        &mut self,
1008        config: TextConfig,
1009    ) -> usize {
1010        self.text_element_configs.push(config);
1011        self.text_element_configs.len() - 1
1012    }
1013
1014    fn store_layout_config(&mut self, config: LayoutConfig) -> usize {
1015        self.layout_configs.push(config);
1016        self.layout_configs.len() - 1
1017    }
1018
1019    fn store_shared_config(&mut self, config: SharedElementConfig) -> usize {
1020        self.shared_element_configs.push(config);
1021        self.shared_element_configs.len() - 1
1022    }
1023
1024    fn attach_element_config(&mut self, config_type: ElementConfigType, config_index: usize) {
1025        if self.boolean_warnings.max_elements_exceeded {
1026            return;
1027        }
1028        let open_idx = self.get_open_layout_element();
1029        self.layout_elements[open_idx].element_configs.length += 1;
1030        self.element_configs.push(ElementConfig {
1031            config_type,
1032            config_index,
1033        });
1034    }
1035
1036    pub fn open_element(&mut self) {
1037        if self.boolean_warnings.max_elements_exceeded {
1038            return;
1039        }
1040        let elem = LayoutElement {
1041            text_data_index: -1,
1042            ..Default::default()
1043        };
1044        self.layout_elements.push(elem);
1045        let idx = (self.layout_elements.len() - 1) as i32;
1046        self.open_layout_element_stack.push(idx);
1047
1048        // Ensure clip IDs array is large enough
1049        while self.layout_element_clip_element_ids.len() < self.layout_elements.len() {
1050            self.layout_element_clip_element_ids.push(0);
1051        }
1052
1053        self.generate_id_for_anonymous_element(idx as usize);
1054
1055        if !self.open_clip_element_stack.is_empty() {
1056            let clip_id = *self.open_clip_element_stack.last().unwrap();
1057            self.layout_element_clip_element_ids[idx as usize] = clip_id;
1058        } else {
1059            self.layout_element_clip_element_ids[idx as usize] = 0;
1060        }
1061    }
1062
1063    pub fn open_element_with_id(&mut self, element_id: &Id) {
1064        if self.boolean_warnings.max_elements_exceeded {
1065            return;
1066        }
1067        let mut elem = LayoutElement {
1068            text_data_index: -1,
1069            ..Default::default()
1070        };
1071        elem.id = element_id.id;
1072        self.layout_elements.push(elem);
1073        let idx = (self.layout_elements.len() - 1) as i32;
1074        self.open_layout_element_stack.push(idx);
1075
1076        while self.layout_element_clip_element_ids.len() < self.layout_elements.len() {
1077            self.layout_element_clip_element_ids.push(0);
1078        }
1079
1080        self.add_hash_map_item(element_id, idx);
1081        if self.debug_mode_enabled {
1082            self.layout_element_id_strings.push(element_id.string_id.clone());
1083        }
1084
1085        if !self.open_clip_element_stack.is_empty() {
1086            let clip_id = *self.open_clip_element_stack.last().unwrap();
1087            self.layout_element_clip_element_ids[idx as usize] = clip_id;
1088        } else {
1089            self.layout_element_clip_element_ids[idx as usize] = 0;
1090        }
1091    }
1092
1093    pub fn configure_open_element(&mut self, declaration: &ElementDeclaration<CustomElementData>) {
1094        if self.boolean_warnings.max_elements_exceeded {
1095            return;
1096        }
1097        let open_idx = self.get_open_layout_element();
1098        let layout_config_index = self.store_layout_config(declaration.layout);
1099        self.layout_elements[open_idx].layout_config_index = layout_config_index;
1100
1101        // Record the start of element configs for this element
1102        self.layout_elements[open_idx].element_configs.start = self.element_configs.len();
1103
1104        // Shared config (background color, corner radius, user data)
1105        let mut shared_config_index: Option<usize> = None;
1106        if declaration.background_color.a > 0.0 {
1107            let idx = self.store_shared_config(SharedElementConfig {
1108                background_color: declaration.background_color,
1109                corner_radius: CornerRadius::default(),
1110                user_data: 0,
1111            });
1112            shared_config_index = Some(idx);
1113            self.attach_element_config(ElementConfigType::Shared, idx);
1114        }
1115        if !declaration.corner_radius.is_zero() {
1116            if let Some(idx) = shared_config_index {
1117                self.shared_element_configs[idx].corner_radius = declaration.corner_radius;
1118            } else {
1119                let idx = self.store_shared_config(SharedElementConfig {
1120                    background_color: Color::rgba(0.0, 0.0, 0.0, 0.0),
1121                    corner_radius: declaration.corner_radius,
1122                    user_data: 0,
1123                });
1124                shared_config_index = Some(idx);
1125                self.attach_element_config(ElementConfigType::Shared, idx);
1126            }
1127        }
1128        if declaration.user_data != 0 {
1129            if let Some(idx) = shared_config_index {
1130                self.shared_element_configs[idx].user_data = declaration.user_data;
1131            } else {
1132                let idx = self.store_shared_config(SharedElementConfig {
1133                    background_color: Color::rgba(0.0, 0.0, 0.0, 0.0),
1134                    corner_radius: CornerRadius::default(),
1135                    user_data: declaration.user_data,
1136                });
1137                self.attach_element_config(ElementConfigType::Shared, idx);
1138            }
1139        }
1140
1141        // Image config
1142        if let Some(image_data) = declaration.image_data.clone() {
1143            self.image_element_configs.push(image_data);
1144            let idx = self.image_element_configs.len() - 1;
1145            self.attach_element_config(ElementConfigType::Image, idx);
1146        }
1147
1148        // Aspect ratio config
1149        if declaration.aspect_ratio > 0.0 {
1150            self.aspect_ratio_configs.push(declaration.aspect_ratio);
1151            let idx = self.aspect_ratio_configs.len() - 1;
1152            self.attach_element_config(ElementConfigType::Aspect, idx);
1153            self.aspect_ratio_element_indexes
1154                .push((self.layout_elements.len() - 1) as i32);
1155        }
1156
1157        // Floating config
1158        if declaration.floating.attach_to != FloatingAttachToElement::None {
1159            let mut floating_config = declaration.floating;
1160            let stack_len = self.open_layout_element_stack.len();
1161
1162            if stack_len >= 2 {
1163                let hierarchical_parent_idx =
1164                    self.open_layout_element_stack[stack_len - 2] as usize;
1165                let hierarchical_parent_id = self.layout_elements[hierarchical_parent_idx].id;
1166
1167                let mut clip_element_id: u32 = 0;
1168
1169                if declaration.floating.attach_to == FloatingAttachToElement::Parent {
1170                    floating_config.parent_id = hierarchical_parent_id;
1171                    if !self.open_clip_element_stack.is_empty() {
1172                        clip_element_id =
1173                            *self.open_clip_element_stack.last().unwrap() as u32;
1174                    }
1175                } else if declaration.floating.attach_to
1176                    == FloatingAttachToElement::ElementWithId
1177                {
1178                    if let Some(parent_item) =
1179                        self.layout_element_map.get(&floating_config.parent_id)
1180                    {
1181                        let parent_elem_idx = parent_item.layout_element_index as usize;
1182                        clip_element_id =
1183                            self.layout_element_clip_element_ids[parent_elem_idx] as u32;
1184                    }
1185                } else if declaration.floating.attach_to
1186                    == FloatingAttachToElement::Root
1187                {
1188                    floating_config.parent_id =
1189                        hash_string("Ply__RootContainer", 0).id;
1190                }
1191
1192                if declaration.floating.clip_to == FloatingClipToElement::None {
1193                    clip_element_id = 0;
1194                }
1195
1196                let current_element_index =
1197                    *self.open_layout_element_stack.last().unwrap();
1198                self.layout_element_clip_element_ids[current_element_index as usize] =
1199                    clip_element_id as i32;
1200                self.open_clip_element_stack.push(clip_element_id as i32);
1201
1202                self.layout_element_tree_roots
1203                    .push(LayoutElementTreeRoot {
1204                        layout_element_index: current_element_index,
1205                        parent_id: floating_config.parent_id,
1206                        clip_element_id,
1207                        z_index: floating_config.z_index,
1208                        pointer_offset: Vector2::default(),
1209                    });
1210
1211                self.floating_element_configs.push(floating_config);
1212                let idx = self.floating_element_configs.len() - 1;
1213                self.attach_element_config(ElementConfigType::Floating, idx);
1214            }
1215        }
1216
1217        // Custom config
1218        if let Some(ref custom_data) = declaration.custom_data {
1219            self.custom_element_configs.push(custom_data.clone());
1220            let idx = self.custom_element_configs.len() - 1;
1221            self.attach_element_config(ElementConfigType::Custom, idx);
1222        }
1223
1224        // Clip config
1225        if declaration.clip.horizontal || declaration.clip.vertical {
1226            let mut clip = declaration.clip;
1227
1228            let elem_id = self.layout_elements[open_idx].id;
1229
1230            // Auto-apply stored scroll position as child_offset
1231            if clip.scroll_x || clip.scroll_y {
1232                for scd in &self.scroll_container_datas {
1233                    if scd.element_id == elem_id {
1234                        clip.child_offset = scd.scroll_position;
1235                        break;
1236                    }
1237                }
1238            }
1239
1240            self.clip_element_configs.push(clip);
1241            let idx = self.clip_element_configs.len() - 1;
1242            self.attach_element_config(ElementConfigType::Clip, idx);
1243
1244            self.open_clip_element_stack.push(elem_id as i32);
1245
1246            // Track scroll container
1247            if clip.scroll_x || clip.scroll_y {
1248                let mut found_existing = false;
1249                for scd in &mut self.scroll_container_datas {
1250                    if elem_id == scd.element_id {
1251                        scd.layout_element_index = open_idx as i32;
1252                        scd.open_this_frame = true;
1253                        found_existing = true;
1254                        break;
1255                    }
1256                }
1257                if !found_existing {
1258                    self.scroll_container_datas.push(ScrollContainerDataInternal {
1259                        layout_element_index: open_idx as i32,
1260                        scroll_origin: Vector2::new(-1.0, -1.0),
1261                        element_id: elem_id,
1262                        open_this_frame: true,
1263                        ..Default::default()
1264                    });
1265                }
1266            }
1267        }
1268
1269        // Border config
1270        if !declaration.border.width.is_zero() {
1271            self.border_element_configs.push(declaration.border);
1272            let idx = self.border_element_configs.len() - 1;
1273            self.attach_element_config(ElementConfigType::Border, idx);
1274        }
1275
1276        // Store per-element shader effects
1277        // Ensure element_effects is large enough for open_idx
1278        while self.element_effects.len() <= open_idx {
1279            self.element_effects.push(Vec::new());
1280        }
1281        self.element_effects[open_idx] = declaration.effects.clone();
1282
1283        // Store per-element group shaders
1284        while self.element_shaders.len() <= open_idx {
1285            self.element_shaders.push(Vec::new());
1286        }
1287        self.element_shaders[open_idx] = declaration.shaders.clone();
1288
1289        // Store per-element visual rotation
1290        while self.element_visual_rotations.len() <= open_idx {
1291            self.element_visual_rotations.push(None);
1292        }
1293        self.element_visual_rotations[open_idx] = declaration.visual_rotation;
1294
1295        // Store per-element shape rotation
1296        while self.element_shape_rotations.len() <= open_idx {
1297            self.element_shape_rotations.push(None);
1298        }
1299        self.element_shape_rotations[open_idx] = declaration.shape_rotation;
1300
1301        // Accessibility config
1302        if let Some(ref a11y) = declaration.accessibility {
1303            let elem_id = self.layout_elements[open_idx].id;
1304            if a11y.focusable {
1305                self.focusable_elements.push(FocusableEntry {
1306                    element_id: elem_id,
1307                    tab_index: a11y.tab_index,
1308                    insertion_order: self.focusable_elements.len() as u32,
1309                });
1310            }
1311            self.accessibility_configs.insert(elem_id, a11y.clone());
1312            self.accessibility_element_order.push(elem_id);
1313        }
1314
1315        // Text input config
1316        if let Some(ref ti_config) = declaration.text_input {
1317            let elem_id = self.layout_elements[open_idx].id;
1318            self.text_input_configs.push(ti_config.clone());
1319            let idx = self.text_input_configs.len() - 1;
1320            self.attach_element_config(ElementConfigType::TextInput, idx);
1321            self.text_input_element_ids.push(elem_id);
1322
1323            // Mark the element as a text input in the layout map
1324            if let Some(item) = self.layout_element_map.get_mut(&elem_id) {
1325                item.is_text_input = true;
1326            }
1327
1328            // Ensure a TextEditState exists for this element
1329            self.text_edit_states.entry(elem_id)
1330                .or_insert_with(crate::text_input::TextEditState::default);
1331
1332            // Sync config flags to persistent state
1333            if let Some(state) = self.text_edit_states.get_mut(&elem_id) {
1334                state.no_styles_movement = ti_config.no_styles_movement;
1335            }
1336
1337            // Process any pending click on this text input
1338            if let Some((click_elem, click_x, click_y, click_shift)) = self.pending_text_click.take() {
1339                if click_elem == elem_id {
1340                    if let Some(ref measure_fn) = self.measure_text_fn {
1341                        let state = self.text_edit_states.get(&elem_id).cloned()
1342                            .unwrap_or_default();
1343                        let disp_text = crate::text_input::display_text(
1344                            &state.text,
1345                            &ti_config.placeholder,
1346                            ti_config.is_password,
1347                        );
1348                        // Only position cursor in actual text, not placeholder
1349                        if !state.text.is_empty() {
1350                            // Double-click detection
1351                            let is_double_click = state.last_click_element == elem_id
1352                                && (self.current_time - state.last_click_time) < 0.4;
1353
1354                            if ti_config.is_multiline {
1355                                // Multiline: determine which visual line was clicked
1356                                let elem_width = self.layout_element_map.get(&elem_id)
1357                                    .map(|item| item.bounding_box.width)
1358                                    .unwrap_or(200.0);
1359                                let visual_lines = crate::text_input::wrap_lines(
1360                                    &disp_text,
1361                                    elem_width,
1362                                    ti_config.font_asset,
1363                                    ti_config.font_size,
1364                                    measure_fn.as_ref(),
1365                                );
1366                                let font_height = if ti_config.line_height > 0 {
1367                                    ti_config.line_height as f32
1368                                } else {
1369                                    let config = crate::text::TextConfig {
1370                                        font_asset: ti_config.font_asset,
1371                                        font_size: ti_config.font_size,
1372                                        ..Default::default()
1373                                    };
1374                                    measure_fn(&"Mg", &config).height
1375                                };
1376                                let adjusted_y = click_y + state.scroll_offset_y;
1377                                let clicked_line = (adjusted_y / font_height).floor().max(0.0) as usize;
1378                                let clicked_line = clicked_line.min(visual_lines.len().saturating_sub(1));
1379
1380                                let vl = &visual_lines[clicked_line];
1381                                let line_char_x_positions = crate::text_input::compute_char_x_positions(
1382                                    &vl.text,
1383                                    ti_config.font_asset,
1384                                    ti_config.font_size,
1385                                    measure_fn.as_ref(),
1386                                );
1387                                let col = crate::text_input::find_nearest_char_boundary(
1388                                    click_x, &line_char_x_positions,
1389                                );
1390                                let global_pos = vl.global_char_start + col;
1391
1392                                if let Some(state) = self.text_edit_states.get_mut(&elem_id) {
1393                                    #[cfg(feature = "text-styling")]
1394                                    {
1395                                        let visual_pos = crate::text_input::styling::raw_to_cursor(&state.text, global_pos);
1396                                        if is_double_click {
1397                                            state.select_word_at_styled(visual_pos);
1398                                        } else {
1399                                            state.click_to_cursor_styled(visual_pos, click_shift);
1400                                        }
1401                                    }
1402                                    #[cfg(not(feature = "text-styling"))]
1403                                    {
1404                                        if is_double_click {
1405                                            state.select_word_at(global_pos);
1406                                        } else {
1407                                            if click_shift {
1408                                                if state.selection_anchor.is_none() {
1409                                                    state.selection_anchor = Some(state.cursor_pos);
1410                                                }
1411                                            } else {
1412                                                state.selection_anchor = None;
1413                                            }
1414                                            state.cursor_pos = global_pos;
1415                                            state.reset_blink();
1416                                        }
1417                                    }
1418                                    state.last_click_time = self.current_time;
1419                                    state.last_click_element = elem_id;
1420                                }
1421                            } else {
1422                                // Single-line: existing behavior
1423                                let char_x_positions = crate::text_input::compute_char_x_positions(
1424                                    &disp_text,
1425                                    ti_config.font_asset,
1426                                    ti_config.font_size,
1427                                    measure_fn.as_ref(),
1428                                );
1429                                let adjusted_x = click_x + state.scroll_offset;
1430
1431                                if let Some(state) = self.text_edit_states.get_mut(&elem_id) {
1432                                    let raw_click_pos = crate::text_input::find_nearest_char_boundary(
1433                                        adjusted_x, &char_x_positions,
1434                                    );
1435                                    #[cfg(feature = "text-styling")]
1436                                    {
1437                                        let visual_pos = crate::text_input::styling::raw_to_cursor(&state.text, raw_click_pos);
1438                                        if is_double_click {
1439                                            state.select_word_at_styled(visual_pos);
1440                                        } else {
1441                                            state.click_to_cursor_styled(visual_pos, click_shift);
1442                                        }
1443                                    }
1444                                    #[cfg(not(feature = "text-styling"))]
1445                                    {
1446                                        if is_double_click {
1447                                            state.select_word_at(raw_click_pos);
1448                                        } else {
1449                                            state.click_to_cursor(adjusted_x, &char_x_positions, click_shift);
1450                                        }
1451                                    }
1452                                    state.last_click_time = self.current_time;
1453                                    state.last_click_element = elem_id;
1454                                }
1455                            }
1456                        } else if let Some(state) = self.text_edit_states.get_mut(&elem_id) {
1457                            state.cursor_pos = 0;
1458                            state.selection_anchor = None;
1459                            state.last_click_time = self.current_time;
1460                            state.last_click_element = elem_id;
1461                            state.reset_blink();
1462                        }
1463                    }
1464                } else {
1465                    // Wasn't for this element, put it back
1466                    self.pending_text_click = Some((click_elem, click_x, click_y, click_shift));
1467                }
1468            }
1469
1470            // Auto-register as focusable if not already done via accessibility
1471            if declaration.accessibility.is_none() || !declaration.accessibility.as_ref().unwrap().focusable {
1472                // Check it's not already registered
1473                let already = self.focusable_elements.iter().any(|e| e.element_id == elem_id);
1474                if !already {
1475                    self.focusable_elements.push(FocusableEntry {
1476                        element_id: elem_id,
1477                        tab_index: None,
1478                        insertion_order: self.focusable_elements.len() as u32,
1479                    });
1480                }
1481            }
1482        }
1483
1484        // Preserve-focus flag
1485        if declaration.preserve_focus {
1486            let elem_id = self.layout_elements[open_idx].id;
1487            if let Some(item) = self.layout_element_map.get_mut(&elem_id) {
1488                item.preserve_focus = true;
1489            }
1490        }
1491    }
1492
1493    pub fn close_element(&mut self) {
1494        if self.boolean_warnings.max_elements_exceeded {
1495            return;
1496        }
1497
1498        let open_idx = self.get_open_layout_element();
1499        let layout_config_index = self.layout_elements[open_idx].layout_config_index;
1500        let layout_config = self.layout_configs[layout_config_index];
1501
1502        // Check for clip and floating configs
1503        let mut element_has_clip_horizontal = false;
1504        let mut element_has_clip_vertical = false;
1505        let element_configs_start = self.layout_elements[open_idx].element_configs.start;
1506        let element_configs_length = self.layout_elements[open_idx].element_configs.length;
1507
1508        for i in 0..element_configs_length {
1509            let config = &self.element_configs[element_configs_start + i as usize];
1510            if config.config_type == ElementConfigType::Clip {
1511                let clip = &self.clip_element_configs[config.config_index];
1512                element_has_clip_horizontal = clip.horizontal;
1513                element_has_clip_vertical = clip.vertical;
1514                self.open_clip_element_stack.pop();
1515                break;
1516            } else if config.config_type == ElementConfigType::Floating {
1517                self.open_clip_element_stack.pop();
1518            }
1519        }
1520
1521        let left_right_padding =
1522            (layout_config.padding.left + layout_config.padding.right) as f32;
1523        let top_bottom_padding =
1524            (layout_config.padding.top + layout_config.padding.bottom) as f32;
1525
1526        let children_length = self.layout_elements[open_idx].children_length;
1527
1528        // Attach children to the current open element
1529        let children_start = self.layout_element_children.len();
1530        self.layout_elements[open_idx].children_start = children_start;
1531
1532        if layout_config.layout_direction == LayoutDirection::LeftToRight {
1533            self.layout_elements[open_idx].dimensions.width = left_right_padding;
1534            self.layout_elements[open_idx].min_dimensions.width = left_right_padding;
1535
1536            for i in 0..children_length {
1537                let buf_idx = self.layout_element_children_buffer.len()
1538                    - children_length as usize
1539                    + i as usize;
1540                let child_index = self.layout_element_children_buffer[buf_idx];
1541                let child = &self.layout_elements[child_index as usize];
1542                let child_width = child.dimensions.width;
1543                let child_height = child.dimensions.height;
1544                let child_min_width = child.min_dimensions.width;
1545                let child_min_height = child.min_dimensions.height;
1546
1547                self.layout_elements[open_idx].dimensions.width += child_width;
1548                let current_height = self.layout_elements[open_idx].dimensions.height;
1549                self.layout_elements[open_idx].dimensions.height =
1550                    f32::max(current_height, child_height + top_bottom_padding);
1551
1552                if !element_has_clip_horizontal {
1553                    self.layout_elements[open_idx].min_dimensions.width += child_min_width;
1554                }
1555                if !element_has_clip_vertical {
1556                    let current_min_h = self.layout_elements[open_idx].min_dimensions.height;
1557                    self.layout_elements[open_idx].min_dimensions.height =
1558                        f32::max(current_min_h, child_min_height + top_bottom_padding);
1559                }
1560                self.layout_element_children.push(child_index);
1561            }
1562            let child_gap =
1563                (children_length.saturating_sub(1) as u32 * layout_config.child_gap as u32) as f32;
1564            self.layout_elements[open_idx].dimensions.width += child_gap;
1565            if !element_has_clip_horizontal {
1566                self.layout_elements[open_idx].min_dimensions.width += child_gap;
1567            }
1568        } else {
1569            // TopToBottom
1570            self.layout_elements[open_idx].dimensions.height = top_bottom_padding;
1571            self.layout_elements[open_idx].min_dimensions.height = top_bottom_padding;
1572
1573            for i in 0..children_length {
1574                let buf_idx = self.layout_element_children_buffer.len()
1575                    - children_length as usize
1576                    + i as usize;
1577                let child_index = self.layout_element_children_buffer[buf_idx];
1578                let child = &self.layout_elements[child_index as usize];
1579                let child_width = child.dimensions.width;
1580                let child_height = child.dimensions.height;
1581                let child_min_width = child.min_dimensions.width;
1582                let child_min_height = child.min_dimensions.height;
1583
1584                self.layout_elements[open_idx].dimensions.height += child_height;
1585                let current_width = self.layout_elements[open_idx].dimensions.width;
1586                self.layout_elements[open_idx].dimensions.width =
1587                    f32::max(current_width, child_width + left_right_padding);
1588
1589                if !element_has_clip_vertical {
1590                    self.layout_elements[open_idx].min_dimensions.height += child_min_height;
1591                }
1592                if !element_has_clip_horizontal {
1593                    let current_min_w = self.layout_elements[open_idx].min_dimensions.width;
1594                    self.layout_elements[open_idx].min_dimensions.width =
1595                        f32::max(current_min_w, child_min_width + left_right_padding);
1596                }
1597                self.layout_element_children.push(child_index);
1598            }
1599            let child_gap =
1600                (children_length.saturating_sub(1) as u32 * layout_config.child_gap as u32) as f32;
1601            self.layout_elements[open_idx].dimensions.height += child_gap;
1602            if !element_has_clip_vertical {
1603                self.layout_elements[open_idx].min_dimensions.height += child_gap;
1604            }
1605        }
1606
1607        // Remove children from buffer
1608        let remove_count = children_length as usize;
1609        let new_len = self.layout_element_children_buffer.len().saturating_sub(remove_count);
1610        self.layout_element_children_buffer.truncate(new_len);
1611
1612        // Clamp width
1613        {
1614            let sizing_type = self.layout_configs[layout_config_index].sizing.width.type_;
1615            if sizing_type != SizingType::Percent {
1616                let mut max_w = self.layout_configs[layout_config_index].sizing.width.min_max.max;
1617                if max_w <= 0.0 {
1618                    max_w = MAXFLOAT;
1619                    self.layout_configs[layout_config_index].sizing.width.min_max.max = max_w;
1620                }
1621                let min_w = self.layout_configs[layout_config_index].sizing.width.min_max.min;
1622                self.layout_elements[open_idx].dimensions.width = f32::min(
1623                    f32::max(self.layout_elements[open_idx].dimensions.width, min_w),
1624                    max_w,
1625                );
1626                self.layout_elements[open_idx].min_dimensions.width = f32::min(
1627                    f32::max(self.layout_elements[open_idx].min_dimensions.width, min_w),
1628                    max_w,
1629                );
1630            } else {
1631                self.layout_elements[open_idx].dimensions.width = 0.0;
1632            }
1633        }
1634
1635        // Clamp height
1636        {
1637            let sizing_type = self.layout_configs[layout_config_index].sizing.height.type_;
1638            if sizing_type != SizingType::Percent {
1639                let mut max_h = self.layout_configs[layout_config_index].sizing.height.min_max.max;
1640                if max_h <= 0.0 {
1641                    max_h = MAXFLOAT;
1642                    self.layout_configs[layout_config_index].sizing.height.min_max.max = max_h;
1643                }
1644                let min_h = self.layout_configs[layout_config_index].sizing.height.min_max.min;
1645                self.layout_elements[open_idx].dimensions.height = f32::min(
1646                    f32::max(self.layout_elements[open_idx].dimensions.height, min_h),
1647                    max_h,
1648                );
1649                self.layout_elements[open_idx].min_dimensions.height = f32::min(
1650                    f32::max(self.layout_elements[open_idx].min_dimensions.height, min_h),
1651                    max_h,
1652                );
1653            } else {
1654                self.layout_elements[open_idx].dimensions.height = 0.0;
1655            }
1656        }
1657
1658        self.update_aspect_ratio_box(open_idx);
1659
1660        // Apply shape rotation AABB expansion
1661        if let Some(shape_rot) = self.element_shape_rotations.get(open_idx).copied().flatten() {
1662            if !shape_rot.is_noop() {
1663                let orig_w = self.layout_elements[open_idx].dimensions.width;
1664                let orig_h = self.layout_elements[open_idx].dimensions.height;
1665
1666                // Find corner radius for this element
1667                let cr = self
1668                    .find_element_config_index(open_idx, ElementConfigType::Shared)
1669                    .map(|idx| self.shared_element_configs[idx].corner_radius)
1670                    .unwrap_or_default();
1671
1672                let (eff_w, eff_h) = crate::math::compute_rotated_aabb(
1673                    orig_w,
1674                    orig_h,
1675                    &cr,
1676                    shape_rot.rotation_radians,
1677                );
1678
1679                // Store original dimensions for renderer
1680                while self.element_pre_rotation_dimensions.len() <= open_idx {
1681                    self.element_pre_rotation_dimensions.push(None);
1682                }
1683                self.element_pre_rotation_dimensions[open_idx] =
1684                    Some(Dimensions::new(orig_w, orig_h));
1685
1686                // Replace layout dimensions with AABB
1687                self.layout_elements[open_idx].dimensions.width = eff_w;
1688                self.layout_elements[open_idx].dimensions.height = eff_h;
1689                self.layout_elements[open_idx].min_dimensions.width = eff_w;
1690                self.layout_elements[open_idx].min_dimensions.height = eff_h;
1691            }
1692        }
1693
1694        let element_is_floating =
1695            self.element_has_config(open_idx, ElementConfigType::Floating);
1696
1697        // Pop from open stack
1698        self.open_layout_element_stack.pop();
1699
1700        // Add to parent's children
1701        if self.open_layout_element_stack.len() > 1 {
1702            if element_is_floating {
1703                let parent_idx = self.get_open_layout_element();
1704                self.layout_elements[parent_idx].floating_children_count += 1;
1705                return;
1706            }
1707            let parent_idx = self.get_open_layout_element();
1708            self.layout_elements[parent_idx].children_length += 1;
1709            self.layout_element_children_buffer.push(open_idx as i32);
1710        }
1711    }
1712
1713    pub fn open_text_element(
1714        &mut self,
1715        text: &str,
1716        text_config_index: usize,
1717    ) {
1718        if self.boolean_warnings.max_elements_exceeded {
1719            return;
1720        }
1721
1722        let parent_idx = self.get_open_layout_element();
1723        let parent_id = self.layout_elements[parent_idx].id;
1724        let parent_children_count = self.layout_elements[parent_idx].children_length;
1725
1726        // Create text layout element
1727        let text_element = LayoutElement {
1728            text_data_index: -1,
1729            ..Default::default()
1730        };
1731        self.layout_elements.push(text_element);
1732        let text_elem_idx = (self.layout_elements.len() - 1) as i32;
1733
1734        while self.layout_element_clip_element_ids.len() < self.layout_elements.len() {
1735            self.layout_element_clip_element_ids.push(0);
1736        }
1737        if !self.open_clip_element_stack.is_empty() {
1738            let clip_id = *self.open_clip_element_stack.last().unwrap();
1739            self.layout_element_clip_element_ids[text_elem_idx as usize] = clip_id;
1740        } else {
1741            self.layout_element_clip_element_ids[text_elem_idx as usize] = 0;
1742        }
1743
1744        self.layout_element_children_buffer.push(text_elem_idx);
1745
1746        // Measure text
1747        let text_config = self.text_element_configs[text_config_index].clone();
1748        let text_measured =
1749            self.measure_text_cached(text, &text_config);
1750
1751        let element_id = hash_number(parent_children_count as u32, parent_id);
1752        self.layout_elements[text_elem_idx as usize].id = element_id.id;
1753        self.add_hash_map_item(&element_id, text_elem_idx);
1754        if self.debug_mode_enabled {
1755            self.layout_element_id_strings.push(element_id.string_id);
1756        }
1757
1758        // If the text element is marked accessible, register it in the
1759        // accessibility tree with a StaticText role and the text content
1760        // as the label.
1761        if text_config.accessible {
1762            let a11y = crate::accessibility::AccessibilityConfig {
1763                role: crate::accessibility::AccessibilityRole::StaticText,
1764                label: text.to_string(),
1765                ..Default::default()
1766            };
1767            self.accessibility_configs.insert(element_id.id, a11y);
1768            self.accessibility_element_order.push(element_id.id);
1769        }
1770
1771        let text_width = text_measured.unwrapped_dimensions.width;
1772        let text_height = if text_config.line_height > 0 {
1773            text_config.line_height as f32
1774        } else {
1775            text_measured.unwrapped_dimensions.height
1776        };
1777        let min_width = text_measured.min_width;
1778
1779        self.layout_elements[text_elem_idx as usize].dimensions =
1780            Dimensions::new(text_width, text_height);
1781        self.layout_elements[text_elem_idx as usize].min_dimensions =
1782            Dimensions::new(min_width, text_height);
1783
1784        // Store text element data
1785        let text_data = TextElementData {
1786            text: text.to_string(),
1787            preferred_dimensions: text_measured.unwrapped_dimensions,
1788            element_index: text_elem_idx,
1789            wrapped_lines_start: 0,
1790            wrapped_lines_length: 0,
1791        };
1792        self.text_element_data.push(text_data);
1793        let text_data_idx = (self.text_element_data.len() - 1) as i32;
1794        self.layout_elements[text_elem_idx as usize].text_data_index = text_data_idx;
1795
1796        // Attach text config
1797        self.layout_elements[text_elem_idx as usize].element_configs.start =
1798            self.element_configs.len();
1799        self.element_configs.push(ElementConfig {
1800            config_type: ElementConfigType::Text,
1801            config_index: text_config_index,
1802        });
1803        self.layout_elements[text_elem_idx as usize].element_configs.length = 1;
1804
1805        // Set default layout config
1806        let default_layout_idx = self.store_layout_config(LayoutConfig::default());
1807        self.layout_elements[text_elem_idx as usize].layout_config_index = default_layout_idx;
1808
1809        // Add to parent's children count
1810        self.layout_elements[parent_idx].children_length += 1;
1811    }
1812
1813    /// Returns the cached font height for the given (font_asset, font_size) pair.
1814    /// Measures `"Mg"` on the first call for each pair and caches the result.
1815    fn font_height(&mut self, font_asset: Option<&'static crate::renderer::FontAsset>, font_size: u16) -> f32 {
1816        let font_key = font_asset.map(|a| a.key()).unwrap_or("");
1817        let key = (font_key, font_size);
1818        if let Some(&h) = self.font_height_cache.get(&key) {
1819            return h;
1820        }
1821        let h = if let Some(ref measure_fn) = self.measure_text_fn {
1822            let config = TextConfig {
1823                font_asset,
1824                font_size,
1825                ..Default::default()
1826            };
1827            measure_fn("Mg", &config).height
1828        } else {
1829            font_size as f32
1830        };
1831        self.font_height_cache.insert(key, h);
1832        h
1833    }
1834
1835    fn measure_text_cached(
1836        &mut self,
1837        text: &str,
1838        config: &TextConfig,
1839    ) -> MeasureTextCacheItem {
1840        match &self.measure_text_fn {
1841            Some(_) => {},
1842            None => {
1843                if !self.boolean_warnings.text_measurement_fn_not_set {
1844                    self.boolean_warnings.text_measurement_fn_not_set = true;
1845                }
1846                return MeasureTextCacheItem::default();
1847            }
1848        };
1849
1850        let id = hash_string_contents_with_config(text, config);
1851
1852        // Check cache
1853        if let Some(item) = self.measure_text_cache.get_mut(&id) {
1854            item.generation = self.generation;
1855            return *item;
1856        }
1857
1858        // Not cached - measure now
1859        let text_data = text.as_bytes();
1860        let text_length = text_data.len() as i32;
1861
1862        let space_str = " ";
1863        let space_width = (self.measure_text_fn.as_ref().unwrap())(space_str, config).width;
1864
1865        let mut start: i32 = 0;
1866        let mut end: i32 = 0;
1867        let mut line_width: f32 = 0.0;
1868        let mut measured_width: f32 = 0.0;
1869        let mut measured_height: f32 = 0.0;
1870        let mut min_width: f32 = 0.0;
1871        let mut contains_newlines = false;
1872
1873        let mut temp_word_next: i32 = -1;
1874        let mut previous_word_index: i32 = -1;
1875
1876        while end < text_length {
1877            let current = text_data[end as usize];
1878            if current == b' ' || current == b'\n' {
1879                let length = end - start;
1880                let mut dimensions = Dimensions::default();
1881                if length > 0 {
1882                    let substr =
1883                        core::str::from_utf8(&text_data[start as usize..end as usize]).unwrap();
1884                    dimensions = (self.measure_text_fn.as_ref().unwrap())(substr, config);
1885                }
1886                min_width = f32::max(dimensions.width, min_width);
1887                measured_height = f32::max(measured_height, dimensions.height);
1888
1889                if current == b' ' {
1890                    dimensions.width += space_width;
1891                    let word = MeasuredWord {
1892                        start_offset: start,
1893                        length: length + 1,
1894                        width: dimensions.width,
1895                        next: -1,
1896                    };
1897                    let word_idx = self.add_measured_word(word, previous_word_index);
1898                    if previous_word_index == -1 {
1899                        temp_word_next = word_idx;
1900                    }
1901                    previous_word_index = word_idx;
1902                    line_width += dimensions.width;
1903                }
1904                if current == b'\n' {
1905                    if length > 0 {
1906                        let word = MeasuredWord {
1907                            start_offset: start,
1908                            length,
1909                            width: dimensions.width,
1910                            next: -1,
1911                        };
1912                        let word_idx = self.add_measured_word(word, previous_word_index);
1913                        if previous_word_index == -1 {
1914                            temp_word_next = word_idx;
1915                        }
1916                        previous_word_index = word_idx;
1917                    }
1918                    let newline_word = MeasuredWord {
1919                        start_offset: end + 1,
1920                        length: 0,
1921                        width: 0.0,
1922                        next: -1,
1923                    };
1924                    let word_idx = self.add_measured_word(newline_word, previous_word_index);
1925                    if previous_word_index == -1 {
1926                        temp_word_next = word_idx;
1927                    }
1928                    previous_word_index = word_idx;
1929                    line_width += dimensions.width;
1930                    measured_width = f32::max(line_width, measured_width);
1931                    contains_newlines = true;
1932                    line_width = 0.0;
1933                }
1934                start = end + 1;
1935            }
1936            end += 1;
1937        }
1938
1939        if end - start > 0 {
1940            let substr =
1941                core::str::from_utf8(&text_data[start as usize..end as usize]).unwrap();
1942            let dimensions = (self.measure_text_fn.as_ref().unwrap())(substr, config);
1943            let word = MeasuredWord {
1944                start_offset: start,
1945                length: end - start,
1946                width: dimensions.width,
1947                next: -1,
1948            };
1949            let word_idx = self.add_measured_word(word, previous_word_index);
1950            if previous_word_index == -1 {
1951                temp_word_next = word_idx;
1952            }
1953            line_width += dimensions.width;
1954            measured_height = f32::max(measured_height, dimensions.height);
1955            min_width = f32::max(dimensions.width, min_width);
1956        }
1957
1958        measured_width =
1959            f32::max(line_width, measured_width) - config.letter_spacing as f32;
1960
1961        let result = MeasureTextCacheItem {
1962            id,
1963            generation: self.generation,
1964            measured_words_start_index: temp_word_next,
1965            unwrapped_dimensions: Dimensions::new(measured_width, measured_height),
1966            min_width,
1967            contains_newlines,
1968        };
1969        self.measure_text_cache.insert(id, result);
1970        result
1971    }
1972
1973    fn add_measured_word(&mut self, word: MeasuredWord, previous_word_index: i32) -> i32 {
1974        let new_index: i32;
1975        if let Some(&free_idx) = self.measured_words_free_list.last() {
1976            self.measured_words_free_list.pop();
1977            new_index = free_idx;
1978            self.measured_words[free_idx as usize] = word;
1979        } else {
1980            self.measured_words.push(word);
1981            new_index = (self.measured_words.len() - 1) as i32;
1982        }
1983        if previous_word_index >= 0 {
1984            self.measured_words[previous_word_index as usize].next = new_index;
1985        }
1986        new_index
1987    }
1988
1989    pub fn begin_layout(&mut self) {
1990        self.initialize_ephemeral_memory();
1991        self.generation += 1;
1992        self.dynamic_element_index = 0;
1993
1994        // Evict stale text measurement cache entries
1995        self.evict_stale_text_cache();
1996
1997        let root_width = self.layout_dimensions.width;
1998        let root_height = self.layout_dimensions.height;
1999
2000        self.boolean_warnings = BooleanWarnings::default();
2001
2002        let root_id = hash_string("Ply__RootContainer", 0);
2003        self.open_element_with_id(&root_id);
2004
2005        let root_decl = ElementDeclaration {
2006            layout: LayoutConfig {
2007                sizing: SizingConfig {
2008                    width: SizingAxis {
2009                        type_: SizingType::Fixed,
2010                        min_max: SizingMinMax {
2011                            min: root_width,
2012                            max: root_width,
2013                        },
2014                        percent: 0.0,
2015                    },
2016                    height: SizingAxis {
2017                        type_: SizingType::Fixed,
2018                        min_max: SizingMinMax {
2019                            min: root_height,
2020                            max: root_height,
2021                        },
2022                        percent: 0.0,
2023                    },
2024                },
2025                ..Default::default()
2026            },
2027            ..Default::default()
2028        };
2029        self.configure_open_element(&root_decl);
2030        self.open_layout_element_stack.push(0);
2031        self.layout_element_tree_roots.push(LayoutElementTreeRoot {
2032            layout_element_index: 0,
2033            ..Default::default()
2034        });
2035    }
2036
2037    pub fn end_layout(&mut self) -> &[InternalRenderCommand<CustomElementData>] {
2038        self.close_element();
2039
2040        if self.open_layout_element_stack.len() > 1 {
2041            // Unbalanced open/close warning
2042        }
2043
2044        if self.debug_mode_enabled {
2045            self.render_debug_view();
2046        }
2047
2048        self.calculate_final_layout();
2049        &self.render_commands
2050    }
2051
2052    /// Evicts stale entries from the text measurement cache.
2053    /// Entries that haven't been used for more than 2 generations are removed.
2054    fn evict_stale_text_cache(&mut self) {
2055        let gen = self.generation;
2056        let measured_words = &mut self.measured_words;
2057        let free_list = &mut self.measured_words_free_list;
2058        self.measure_text_cache.retain(|_, item| {
2059            if gen.wrapping_sub(item.generation) <= 2 {
2060                true
2061            } else {
2062                // Clean up measured words for this evicted entry
2063                let mut idx = item.measured_words_start_index;
2064                while idx != -1 {
2065                    let word = measured_words[idx as usize];
2066                    free_list.push(idx);
2067                    idx = word.next;
2068                }
2069                false
2070            }
2071        });
2072    }
2073
2074    fn initialize_ephemeral_memory(&mut self) {
2075        self.layout_element_children_buffer.clear();
2076        self.layout_elements.clear();
2077        self.layout_configs.clear();
2078        self.element_configs.clear();
2079        self.text_element_configs.clear();
2080        self.aspect_ratio_configs.clear();
2081        self.image_element_configs.clear();
2082        self.floating_element_configs.clear();
2083        self.clip_element_configs.clear();
2084        self.custom_element_configs.clear();
2085        self.border_element_configs.clear();
2086        self.shared_element_configs.clear();
2087        self.element_effects.clear();
2088        self.element_shaders.clear();
2089        self.element_visual_rotations.clear();
2090        self.element_shape_rotations.clear();
2091        self.element_pre_rotation_dimensions.clear();
2092        self.layout_element_id_strings.clear();
2093        self.wrapped_text_lines.clear();
2094        self.tree_node_array.clear();
2095        self.layout_element_tree_roots.clear();
2096        self.layout_element_children.clear();
2097        self.open_layout_element_stack.clear();
2098        self.text_element_data.clear();
2099        self.aspect_ratio_element_indexes.clear();
2100        self.render_commands.clear();
2101        self.tree_node_visited.clear();
2102        self.open_clip_element_stack.clear();
2103        self.reusable_element_index_buffer.clear();
2104        self.layout_element_clip_element_ids.clear();
2105        self.dynamic_string_data.clear();
2106        self.focusable_elements.clear();
2107        self.accessibility_configs.clear();
2108        self.accessibility_element_order.clear();
2109        self.text_input_configs.clear();
2110        self.text_input_element_ids.clear();
2111    }
2112
2113    fn size_containers_along_axis(&mut self, x_axis: bool) {
2114        let mut bfs_buffer: Vec<i32> = Vec::new();
2115        let mut resizable_container_buffer: Vec<i32> = Vec::new();
2116
2117        for root_index in 0..self.layout_element_tree_roots.len() {
2118            bfs_buffer.clear();
2119            let root = self.layout_element_tree_roots[root_index];
2120            let root_elem_idx = root.layout_element_index as usize;
2121            bfs_buffer.push(root.layout_element_index);
2122
2123            // Size floating containers to their parents
2124            if self.element_has_config(root_elem_idx, ElementConfigType::Floating) {
2125                if let Some(float_cfg_idx) =
2126                    self.find_element_config_index(root_elem_idx, ElementConfigType::Floating)
2127                {
2128                    let parent_id = self.floating_element_configs[float_cfg_idx].parent_id;
2129                    if let Some(parent_item) = self.layout_element_map.get(&parent_id) {
2130                        let parent_elem_idx = parent_item.layout_element_index as usize;
2131                        let parent_dims = self.layout_elements[parent_elem_idx].dimensions;
2132                        let root_layout_idx =
2133                            self.layout_elements[root_elem_idx].layout_config_index;
2134
2135                        let w_type = self.layout_configs[root_layout_idx].sizing.width.type_;
2136                        match w_type {
2137                            SizingType::Grow => {
2138                                self.layout_elements[root_elem_idx].dimensions.width =
2139                                    parent_dims.width;
2140                            }
2141                            SizingType::Percent => {
2142                                self.layout_elements[root_elem_idx].dimensions.width =
2143                                    parent_dims.width
2144                                        * self.layout_configs[root_layout_idx]
2145                                            .sizing
2146                                            .width
2147                                            .percent;
2148                            }
2149                            _ => {}
2150                        }
2151                        let h_type = self.layout_configs[root_layout_idx].sizing.height.type_;
2152                        match h_type {
2153                            SizingType::Grow => {
2154                                self.layout_elements[root_elem_idx].dimensions.height =
2155                                    parent_dims.height;
2156                            }
2157                            SizingType::Percent => {
2158                                self.layout_elements[root_elem_idx].dimensions.height =
2159                                    parent_dims.height
2160                                        * self.layout_configs[root_layout_idx]
2161                                            .sizing
2162                                            .height
2163                                            .percent;
2164                            }
2165                            _ => {}
2166                        }
2167                    }
2168                }
2169            }
2170
2171            // Clamp root element
2172            let root_layout_idx = self.layout_elements[root_elem_idx].layout_config_index;
2173            if self.layout_configs[root_layout_idx].sizing.width.type_ != SizingType::Percent {
2174                let min = self.layout_configs[root_layout_idx].sizing.width.min_max.min;
2175                let max = self.layout_configs[root_layout_idx].sizing.width.min_max.max;
2176                self.layout_elements[root_elem_idx].dimensions.width = f32::min(
2177                    f32::max(self.layout_elements[root_elem_idx].dimensions.width, min),
2178                    max,
2179                );
2180            }
2181            if self.layout_configs[root_layout_idx].sizing.height.type_ != SizingType::Percent {
2182                let min = self.layout_configs[root_layout_idx].sizing.height.min_max.min;
2183                let max = self.layout_configs[root_layout_idx].sizing.height.min_max.max;
2184                self.layout_elements[root_elem_idx].dimensions.height = f32::min(
2185                    f32::max(self.layout_elements[root_elem_idx].dimensions.height, min),
2186                    max,
2187                );
2188            }
2189
2190            let mut i = 0;
2191            while i < bfs_buffer.len() {
2192                let parent_index = bfs_buffer[i] as usize;
2193                i += 1;
2194
2195                let parent_layout_idx = self.layout_elements[parent_index].layout_config_index;
2196                let parent_config = self.layout_configs[parent_layout_idx];
2197                let parent_size = if x_axis {
2198                    self.layout_elements[parent_index].dimensions.width
2199                } else {
2200                    self.layout_elements[parent_index].dimensions.height
2201                };
2202                let parent_padding = if x_axis {
2203                    (parent_config.padding.left + parent_config.padding.right) as f32
2204                } else {
2205                    (parent_config.padding.top + parent_config.padding.bottom) as f32
2206                };
2207                let sizing_along_axis = (x_axis
2208                    && parent_config.layout_direction == LayoutDirection::LeftToRight)
2209                    || (!x_axis
2210                        && parent_config.layout_direction == LayoutDirection::TopToBottom);
2211
2212                let mut inner_content_size: f32 = 0.0;
2213                let mut total_padding_and_child_gaps = parent_padding;
2214                let mut grow_container_count: i32 = 0;
2215                let parent_child_gap = parent_config.child_gap as f32;
2216
2217                resizable_container_buffer.clear();
2218
2219                let children_start = self.layout_elements[parent_index].children_start;
2220                let children_length = self.layout_elements[parent_index].children_length as usize;
2221
2222                for child_offset in 0..children_length {
2223                    let child_element_index =
2224                        self.layout_element_children[children_start + child_offset] as usize;
2225                    let child_layout_idx =
2226                        self.layout_elements[child_element_index].layout_config_index;
2227                    let child_sizing = if x_axis {
2228                        self.layout_configs[child_layout_idx].sizing.width
2229                    } else {
2230                        self.layout_configs[child_layout_idx].sizing.height
2231                    };
2232                    let child_size = if x_axis {
2233                        self.layout_elements[child_element_index].dimensions.width
2234                    } else {
2235                        self.layout_elements[child_element_index].dimensions.height
2236                    };
2237
2238                    let is_text_element =
2239                        self.element_has_config(child_element_index, ElementConfigType::Text);
2240                    let has_children = self.layout_elements[child_element_index].children_length > 0;
2241
2242                    if !is_text_element && has_children {
2243                        bfs_buffer.push(child_element_index as i32);
2244                    }
2245
2246                    let is_wrapping_text = if is_text_element {
2247                        if let Some(text_cfg_idx) = self.find_element_config_index(
2248                            child_element_index,
2249                            ElementConfigType::Text,
2250                        ) {
2251                            self.text_element_configs[text_cfg_idx].wrap_mode
2252                                == WrapMode::Words
2253                        } else {
2254                            false
2255                        }
2256                    } else {
2257                        false
2258                    };
2259
2260                    if child_sizing.type_ != SizingType::Percent
2261                        && child_sizing.type_ != SizingType::Fixed
2262                        && (!is_text_element || is_wrapping_text)
2263                    {
2264                        resizable_container_buffer.push(child_element_index as i32);
2265                    }
2266
2267                    if sizing_along_axis {
2268                        inner_content_size += if child_sizing.type_ == SizingType::Percent {
2269                            0.0
2270                        } else {
2271                            child_size
2272                        };
2273                        if child_sizing.type_ == SizingType::Grow {
2274                            grow_container_count += 1;
2275                        }
2276                        if child_offset > 0 {
2277                            inner_content_size += parent_child_gap;
2278                            total_padding_and_child_gaps += parent_child_gap;
2279                        }
2280                    } else {
2281                        inner_content_size = f32::max(child_size, inner_content_size);
2282                    }
2283                }
2284
2285                // Expand percentage containers
2286                for child_offset in 0..children_length {
2287                    let child_element_index =
2288                        self.layout_element_children[children_start + child_offset] as usize;
2289                    let child_layout_idx =
2290                        self.layout_elements[child_element_index].layout_config_index;
2291                    let child_sizing = if x_axis {
2292                        self.layout_configs[child_layout_idx].sizing.width
2293                    } else {
2294                        self.layout_configs[child_layout_idx].sizing.height
2295                    };
2296                    if child_sizing.type_ == SizingType::Percent {
2297                        let new_size =
2298                            (parent_size - total_padding_and_child_gaps) * child_sizing.percent;
2299                        if x_axis {
2300                            self.layout_elements[child_element_index].dimensions.width = new_size;
2301                        } else {
2302                            self.layout_elements[child_element_index].dimensions.height = new_size;
2303                        }
2304                        if sizing_along_axis {
2305                            inner_content_size += new_size;
2306                        }
2307                        self.update_aspect_ratio_box(child_element_index);
2308                    }
2309                }
2310
2311                if sizing_along_axis {
2312                    let size_to_distribute = parent_size - parent_padding - inner_content_size;
2313
2314                    if size_to_distribute < 0.0 {
2315                        // Check if parent clips
2316                        let parent_clips = if let Some(clip_idx) = self
2317                            .find_element_config_index(parent_index, ElementConfigType::Clip)
2318                        {
2319                            let clip = &self.clip_element_configs[clip_idx];
2320                            (x_axis && clip.horizontal) || (!x_axis && clip.vertical)
2321                        } else {
2322                            false
2323                        };
2324                        if parent_clips {
2325                            continue;
2326                        }
2327
2328                        // Compress children
2329                        let mut distribute = size_to_distribute;
2330                        while distribute < -EPSILON && !resizable_container_buffer.is_empty() {
2331                            let mut largest: f32 = 0.0;
2332                            let mut second_largest: f32 = 0.0;
2333                            let mut width_to_add = distribute;
2334
2335                            for &child_idx in &resizable_container_buffer {
2336                                let cs = if x_axis {
2337                                    self.layout_elements[child_idx as usize].dimensions.width
2338                                } else {
2339                                    self.layout_elements[child_idx as usize].dimensions.height
2340                                };
2341                                if float_equal(cs, largest) {
2342                                    continue;
2343                                }
2344                                if cs > largest {
2345                                    second_largest = largest;
2346                                    largest = cs;
2347                                }
2348                                if cs < largest {
2349                                    second_largest = f32::max(second_largest, cs);
2350                                    width_to_add = second_largest - largest;
2351                                }
2352                            }
2353                            width_to_add = f32::max(
2354                                width_to_add,
2355                                distribute / resizable_container_buffer.len() as f32,
2356                            );
2357
2358                            let mut j = 0;
2359                            while j < resizable_container_buffer.len() {
2360                                let child_idx = resizable_container_buffer[j] as usize;
2361                                let current_size = if x_axis {
2362                                    self.layout_elements[child_idx].dimensions.width
2363                                } else {
2364                                    self.layout_elements[child_idx].dimensions.height
2365                                };
2366                                let min_size = if x_axis {
2367                                    self.layout_elements[child_idx].min_dimensions.width
2368                                } else {
2369                                    self.layout_elements[child_idx].min_dimensions.height
2370                                };
2371                                if float_equal(current_size, largest) {
2372                                    let new_size = current_size + width_to_add;
2373                                    if new_size <= min_size {
2374                                        if x_axis {
2375                                            self.layout_elements[child_idx].dimensions.width = min_size;
2376                                        } else {
2377                                            self.layout_elements[child_idx].dimensions.height = min_size;
2378                                        }
2379                                        distribute -= min_size - current_size;
2380                                        resizable_container_buffer.swap_remove(j);
2381                                        continue;
2382                                    }
2383                                    if x_axis {
2384                                        self.layout_elements[child_idx].dimensions.width = new_size;
2385                                    } else {
2386                                        self.layout_elements[child_idx].dimensions.height = new_size;
2387                                    }
2388                                    distribute -= new_size - current_size;
2389                                }
2390                                j += 1;
2391                            }
2392                        }
2393                    } else if size_to_distribute > 0.0 && grow_container_count > 0 {
2394                        // Remove non-grow from resizable buffer
2395                        let mut j = 0;
2396                        while j < resizable_container_buffer.len() {
2397                            let child_idx = resizable_container_buffer[j] as usize;
2398                            let child_layout_idx =
2399                                self.layout_elements[child_idx].layout_config_index;
2400                            let child_sizing_type = if x_axis {
2401                                self.layout_configs[child_layout_idx].sizing.width.type_
2402                            } else {
2403                                self.layout_configs[child_layout_idx].sizing.height.type_
2404                            };
2405                            if child_sizing_type != SizingType::Grow {
2406                                resizable_container_buffer.swap_remove(j);
2407                            } else {
2408                                j += 1;
2409                            }
2410                        }
2411
2412                        let mut distribute = size_to_distribute;
2413                        while distribute > EPSILON && !resizable_container_buffer.is_empty() {
2414                            let mut smallest: f32 = MAXFLOAT;
2415                            let mut second_smallest: f32 = MAXFLOAT;
2416                            let mut width_to_add = distribute;
2417
2418                            for &child_idx in &resizable_container_buffer {
2419                                let cs = if x_axis {
2420                                    self.layout_elements[child_idx as usize].dimensions.width
2421                                } else {
2422                                    self.layout_elements[child_idx as usize].dimensions.height
2423                                };
2424                                if float_equal(cs, smallest) {
2425                                    continue;
2426                                }
2427                                if cs < smallest {
2428                                    second_smallest = smallest;
2429                                    smallest = cs;
2430                                }
2431                                if cs > smallest {
2432                                    second_smallest = f32::min(second_smallest, cs);
2433                                    width_to_add = second_smallest - smallest;
2434                                }
2435                            }
2436                            width_to_add = f32::min(
2437                                width_to_add,
2438                                distribute / resizable_container_buffer.len() as f32,
2439                            );
2440
2441                            let mut j = 0;
2442                            while j < resizable_container_buffer.len() {
2443                                let child_idx = resizable_container_buffer[j] as usize;
2444                                let child_layout_idx =
2445                                    self.layout_elements[child_idx].layout_config_index;
2446                                let max_size = if x_axis {
2447                                    self.layout_configs[child_layout_idx]
2448                                        .sizing
2449                                        .width
2450                                        .min_max
2451                                        .max
2452                                } else {
2453                                    self.layout_configs[child_layout_idx]
2454                                        .sizing
2455                                        .height
2456                                        .min_max
2457                                        .max
2458                                };
2459                                let child_size_ref = if x_axis {
2460                                    &mut self.layout_elements[child_idx].dimensions.width
2461                                } else {
2462                                    &mut self.layout_elements[child_idx].dimensions.height
2463                                };
2464                                if float_equal(*child_size_ref, smallest) {
2465                                    let previous = *child_size_ref;
2466                                    *child_size_ref += width_to_add;
2467                                    if *child_size_ref >= max_size {
2468                                        *child_size_ref = max_size;
2469                                        resizable_container_buffer.swap_remove(j);
2470                                        continue;
2471                                    }
2472                                    distribute -= *child_size_ref - previous;
2473                                }
2474                                j += 1;
2475                            }
2476                        }
2477                    }
2478                } else {
2479                    // Off-axis sizing
2480                    for &child_idx in &resizable_container_buffer {
2481                        let child_idx = child_idx as usize;
2482                        let child_layout_idx =
2483                            self.layout_elements[child_idx].layout_config_index;
2484                        let child_sizing = if x_axis {
2485                            self.layout_configs[child_layout_idx].sizing.width
2486                        } else {
2487                            self.layout_configs[child_layout_idx].sizing.height
2488                        };
2489                        let min_size = if x_axis {
2490                            self.layout_elements[child_idx].min_dimensions.width
2491                        } else {
2492                            self.layout_elements[child_idx].min_dimensions.height
2493                        };
2494
2495                        let mut max_size = parent_size - parent_padding;
2496                        if let Some(clip_idx) =
2497                            self.find_element_config_index(parent_index, ElementConfigType::Clip)
2498                        {
2499                            let clip = &self.clip_element_configs[clip_idx];
2500                            if (x_axis && clip.horizontal) || (!x_axis && clip.vertical) {
2501                                max_size = f32::max(max_size, inner_content_size);
2502                            }
2503                        }
2504
2505                        let child_size_ref = if x_axis {
2506                            &mut self.layout_elements[child_idx].dimensions.width
2507                        } else {
2508                            &mut self.layout_elements[child_idx].dimensions.height
2509                        };
2510
2511                        if child_sizing.type_ == SizingType::Grow {
2512                            *child_size_ref =
2513                                f32::min(max_size, child_sizing.min_max.max);
2514                        }
2515                        *child_size_ref = f32::max(min_size, f32::min(*child_size_ref, max_size));
2516                    }
2517                }
2518            }
2519        }
2520    }
2521
2522    fn calculate_final_layout(&mut self) {
2523        // Size along X axis
2524        self.size_containers_along_axis(true);
2525
2526        // Wrap text
2527        self.wrap_text();
2528
2529        // Scale vertical heights by aspect ratio
2530        for i in 0..self.aspect_ratio_element_indexes.len() {
2531            let elem_idx = self.aspect_ratio_element_indexes[i] as usize;
2532            if let Some(cfg_idx) =
2533                self.find_element_config_index(elem_idx, ElementConfigType::Aspect)
2534            {
2535                let aspect_ratio = self.aspect_ratio_configs[cfg_idx];
2536                let new_height =
2537                    (1.0 / aspect_ratio) * self.layout_elements[elem_idx].dimensions.width;
2538                self.layout_elements[elem_idx].dimensions.height = new_height;
2539                let layout_idx = self.layout_elements[elem_idx].layout_config_index;
2540                self.layout_configs[layout_idx].sizing.height.min_max.min = new_height;
2541                self.layout_configs[layout_idx].sizing.height.min_max.max = new_height;
2542            }
2543        }
2544
2545        // Propagate height changes up tree (DFS)
2546        self.propagate_sizes_up_tree();
2547
2548        // Size along Y axis
2549        self.size_containers_along_axis(false);
2550
2551        // Scale horizontal widths by aspect ratio
2552        for i in 0..self.aspect_ratio_element_indexes.len() {
2553            let elem_idx = self.aspect_ratio_element_indexes[i] as usize;
2554            if let Some(cfg_idx) =
2555                self.find_element_config_index(elem_idx, ElementConfigType::Aspect)
2556            {
2557                let aspect_ratio = self.aspect_ratio_configs[cfg_idx];
2558                let new_width =
2559                    aspect_ratio * self.layout_elements[elem_idx].dimensions.height;
2560                self.layout_elements[elem_idx].dimensions.width = new_width;
2561                let layout_idx = self.layout_elements[elem_idx].layout_config_index;
2562                self.layout_configs[layout_idx].sizing.width.min_max.min = new_width;
2563                self.layout_configs[layout_idx].sizing.width.min_max.max = new_width;
2564            }
2565        }
2566
2567        // Sort tree roots by z-index (bubble sort)
2568        let mut sort_max = self.layout_element_tree_roots.len().saturating_sub(1);
2569        while sort_max > 0 {
2570            for i in 0..sort_max {
2571                if self.layout_element_tree_roots[i + 1].z_index
2572                    < self.layout_element_tree_roots[i].z_index
2573                {
2574                    self.layout_element_tree_roots.swap(i, i + 1);
2575                }
2576            }
2577            sort_max -= 1;
2578        }
2579
2580        // Generate render commands
2581        self.generate_render_commands();
2582    }
2583
2584    fn wrap_text(&mut self) {
2585        for text_idx in 0..self.text_element_data.len() {
2586            let elem_index = self.text_element_data[text_idx].element_index as usize;
2587            let text = self.text_element_data[text_idx].text.clone();
2588            let preferred_dims = self.text_element_data[text_idx].preferred_dimensions;
2589
2590            self.text_element_data[text_idx].wrapped_lines_start = self.wrapped_text_lines.len();
2591            self.text_element_data[text_idx].wrapped_lines_length = 0;
2592
2593            let container_width = self.layout_elements[elem_index].dimensions.width;
2594
2595            // Find text config
2596            let text_config_idx = self
2597                .find_element_config_index(elem_index, ElementConfigType::Text)
2598                .unwrap_or(0);
2599            let text_config = self.text_element_configs[text_config_idx].clone();
2600
2601            let measured = self.measure_text_cached(&text, &text_config);
2602
2603            let line_height = if text_config.line_height > 0 {
2604                text_config.line_height as f32
2605            } else {
2606                preferred_dims.height
2607            };
2608
2609            if !measured.contains_newlines && preferred_dims.width <= container_width {
2610                // Single line
2611                self.wrapped_text_lines.push(WrappedTextLine {
2612                    dimensions: self.layout_elements[elem_index].dimensions,
2613                    start: 0,
2614                    length: text.len(),
2615                });
2616                self.text_element_data[text_idx].wrapped_lines_length = 1;
2617                continue;
2618            }
2619
2620            // Multi-line wrapping
2621            let measure_fn = self.measure_text_fn.as_ref().unwrap();
2622            let space_width = {
2623                let space_config = text_config.clone();
2624                measure_fn(" ", &space_config).width
2625            };
2626
2627            let mut word_index = measured.measured_words_start_index;
2628            let mut line_width: f32 = 0.0;
2629            let mut line_length_chars: i32 = 0;
2630            let mut line_start_offset: i32 = 0;
2631
2632            while word_index != -1 {
2633                let measured_word = self.measured_words[word_index as usize];
2634
2635                // Word doesn't fit but it's the only word on the line
2636                if line_length_chars == 0 && line_width + measured_word.width > container_width {
2637                    self.wrapped_text_lines.push(WrappedTextLine {
2638                        dimensions: Dimensions::new(measured_word.width, line_height),
2639                        start: measured_word.start_offset as usize,
2640                        length: measured_word.length as usize,
2641                    });
2642                    self.text_element_data[text_idx].wrapped_lines_length += 1;
2643                    word_index = measured_word.next;
2644                    line_start_offset = measured_word.start_offset + measured_word.length;
2645                }
2646                // Newline or overflow
2647                else if measured_word.length == 0
2648                    || line_width + measured_word.width > container_width
2649                {
2650                    let text_bytes = text.as_bytes();
2651                    let final_char_idx = (line_start_offset + line_length_chars - 1).max(0) as usize;
2652                    let final_char_is_space =
2653                        final_char_idx < text_bytes.len() && text_bytes[final_char_idx] == b' ';
2654                    let adj_width = line_width
2655                        + if final_char_is_space {
2656                            -space_width
2657                        } else {
2658                            0.0
2659                        };
2660                    let adj_length = line_length_chars
2661                        + if final_char_is_space { -1 } else { 0 };
2662
2663                    self.wrapped_text_lines.push(WrappedTextLine {
2664                        dimensions: Dimensions::new(adj_width, line_height),
2665                        start: line_start_offset as usize,
2666                        length: adj_length as usize,
2667                    });
2668                    self.text_element_data[text_idx].wrapped_lines_length += 1;
2669
2670                    if line_length_chars == 0 || measured_word.length == 0 {
2671                        word_index = measured_word.next;
2672                    }
2673                    line_width = 0.0;
2674                    line_length_chars = 0;
2675                    line_start_offset = measured_word.start_offset;
2676                } else {
2677                    line_width += measured_word.width + text_config.letter_spacing as f32;
2678                    line_length_chars += measured_word.length;
2679                    word_index = measured_word.next;
2680                }
2681            }
2682
2683            if line_length_chars > 0 {
2684                self.wrapped_text_lines.push(WrappedTextLine {
2685                    dimensions: Dimensions::new(
2686                        line_width - text_config.letter_spacing as f32,
2687                        line_height,
2688                    ),
2689                    start: line_start_offset as usize,
2690                    length: line_length_chars as usize,
2691                });
2692                self.text_element_data[text_idx].wrapped_lines_length += 1;
2693            }
2694
2695            let num_lines = self.text_element_data[text_idx].wrapped_lines_length;
2696            self.layout_elements[elem_index].dimensions.height =
2697                line_height * num_lines as f32;
2698        }
2699    }
2700
2701    fn propagate_sizes_up_tree(&mut self) {
2702        let mut dfs_buffer: Vec<i32> = Vec::new();
2703        let mut visited: Vec<bool> = Vec::new();
2704
2705        for i in 0..self.layout_element_tree_roots.len() {
2706            let root = self.layout_element_tree_roots[i];
2707            dfs_buffer.push(root.layout_element_index);
2708            visited.push(false);
2709        }
2710
2711        while !dfs_buffer.is_empty() {
2712            let buf_idx = dfs_buffer.len() - 1;
2713            let current_elem_idx = dfs_buffer[buf_idx] as usize;
2714
2715            if !visited[buf_idx] {
2716                visited[buf_idx] = true;
2717                let is_text =
2718                    self.element_has_config(current_elem_idx, ElementConfigType::Text);
2719                let children_length = self.layout_elements[current_elem_idx].children_length;
2720                if is_text || children_length == 0 {
2721                    dfs_buffer.pop();
2722                    visited.pop();
2723                    continue;
2724                }
2725                let children_start = self.layout_elements[current_elem_idx].children_start;
2726                for j in 0..children_length as usize {
2727                    let child_idx = self.layout_element_children[children_start + j];
2728                    dfs_buffer.push(child_idx);
2729                    visited.push(false);
2730                }
2731                continue;
2732            }
2733
2734            dfs_buffer.pop();
2735            visited.pop();
2736
2737            let layout_idx = self.layout_elements[current_elem_idx].layout_config_index;
2738            let layout_config = self.layout_configs[layout_idx];
2739            let children_start = self.layout_elements[current_elem_idx].children_start;
2740            let children_length = self.layout_elements[current_elem_idx].children_length;
2741
2742            if layout_config.layout_direction == LayoutDirection::LeftToRight {
2743                for j in 0..children_length as usize {
2744                    let child_idx =
2745                        self.layout_element_children[children_start + j] as usize;
2746                    let child_height_with_padding = f32::max(
2747                        self.layout_elements[child_idx].dimensions.height
2748                            + layout_config.padding.top as f32
2749                            + layout_config.padding.bottom as f32,
2750                        self.layout_elements[current_elem_idx].dimensions.height,
2751                    );
2752                    self.layout_elements[current_elem_idx].dimensions.height = f32::min(
2753                        f32::max(
2754                            child_height_with_padding,
2755                            layout_config.sizing.height.min_max.min,
2756                        ),
2757                        layout_config.sizing.height.min_max.max,
2758                    );
2759                }
2760            } else {
2761                let mut content_height = layout_config.padding.top as f32
2762                    + layout_config.padding.bottom as f32;
2763                for j in 0..children_length as usize {
2764                    let child_idx =
2765                        self.layout_element_children[children_start + j] as usize;
2766                    content_height += self.layout_elements[child_idx].dimensions.height;
2767                }
2768                content_height += children_length.saturating_sub(1) as f32
2769                    * layout_config.child_gap as f32;
2770                self.layout_elements[current_elem_idx].dimensions.height = f32::min(
2771                    f32::max(content_height, layout_config.sizing.height.min_max.min),
2772                    layout_config.sizing.height.min_max.max,
2773                );
2774            }
2775        }
2776    }
2777
2778    fn element_is_offscreen(&self, bbox: &BoundingBox) -> bool {
2779        if self.culling_disabled {
2780            return false;
2781        }
2782        bbox.x > self.layout_dimensions.width
2783            || bbox.y > self.layout_dimensions.height
2784            || bbox.x + bbox.width < 0.0
2785            || bbox.y + bbox.height < 0.0
2786    }
2787
2788    fn add_render_command(&mut self, cmd: InternalRenderCommand<CustomElementData>) {
2789        self.render_commands.push(cmd);
2790    }
2791
2792    fn generate_render_commands(&mut self) {
2793        self.render_commands.clear();
2794        let mut dfs_buffer: Vec<LayoutElementTreeNode> = Vec::new();
2795        let mut visited: Vec<bool> = Vec::new();
2796
2797        for root_index in 0..self.layout_element_tree_roots.len() {
2798            dfs_buffer.clear();
2799            visited.clear();
2800            let root = self.layout_element_tree_roots[root_index];
2801            let root_elem_idx = root.layout_element_index as usize;
2802            let root_element = &self.layout_elements[root_elem_idx];
2803            let mut root_position = Vector2::default();
2804
2805            // Position floating containers
2806            if self.element_has_config(root_elem_idx, ElementConfigType::Floating) {
2807                if let Some(parent_item) = self.layout_element_map.get(&root.parent_id) {
2808                    let parent_bbox = parent_item.bounding_box;
2809                    if let Some(float_cfg_idx) = self
2810                        .find_element_config_index(root_elem_idx, ElementConfigType::Floating)
2811                    {
2812                        let config = &self.floating_element_configs[float_cfg_idx];
2813                        let root_dims = root_element.dimensions;
2814                        let mut target = Vector2::default();
2815
2816                        // X position - parent attach point
2817                        match config.attach_points.parent_x {
2818                            AlignX::Left => {
2819                                target.x = parent_bbox.x;
2820                            }
2821                            AlignX::CenterX => {
2822                                target.x = parent_bbox.x + parent_bbox.width / 2.0;
2823                            }
2824                            AlignX::Right => {
2825                                target.x = parent_bbox.x + parent_bbox.width;
2826                            }
2827                        }
2828                        // X position - element attach point
2829                        match config.attach_points.element_x {
2830                            AlignX::Left => {}
2831                            AlignX::CenterX => {
2832                                target.x -= root_dims.width / 2.0;
2833                            }
2834                            AlignX::Right => {
2835                                target.x -= root_dims.width;
2836                            }
2837                        }
2838                        // Y position - parent attach point
2839                        match config.attach_points.parent_y {
2840                            AlignY::Top => {
2841                                target.y = parent_bbox.y;
2842                            }
2843                            AlignY::CenterY => {
2844                                target.y = parent_bbox.y + parent_bbox.height / 2.0;
2845                            }
2846                            AlignY::Bottom => {
2847                                target.y = parent_bbox.y + parent_bbox.height;
2848                            }
2849                        }
2850                        // Y position - element attach point
2851                        match config.attach_points.element_y {
2852                            AlignY::Top => {}
2853                            AlignY::CenterY => {
2854                                target.y -= root_dims.height / 2.0;
2855                            }
2856                            AlignY::Bottom => {
2857                                target.y -= root_dims.height;
2858                            }
2859                        }
2860                        target.x += config.offset.x;
2861                        target.y += config.offset.y;
2862                        root_position = target;
2863                    }
2864                }
2865            }
2866
2867            // Clip scissor start
2868            if root.clip_element_id != 0 {
2869                if let Some(clip_item) = self.layout_element_map.get(&root.clip_element_id) {
2870                    let clip_bbox = clip_item.bounding_box;
2871                    self.add_render_command(InternalRenderCommand {
2872                        bounding_box: clip_bbox,
2873                        command_type: RenderCommandType::ScissorStart,
2874                        id: hash_number(
2875                            root_element.id,
2876                            root_element.children_length as u32 + 10,
2877                        )
2878                        .id,
2879                        z_index: root.z_index,
2880                        ..Default::default()
2881                    });
2882                }
2883            }
2884
2885            let root_layout_idx = self.layout_elements[root_elem_idx].layout_config_index;
2886            let root_padding_left = self.layout_configs[root_layout_idx].padding.left as f32;
2887            let root_padding_top = self.layout_configs[root_layout_idx].padding.top as f32;
2888
2889            dfs_buffer.push(LayoutElementTreeNode {
2890                layout_element_index: root.layout_element_index,
2891                position: root_position,
2892                next_child_offset: Vector2::new(root_padding_left, root_padding_top),
2893            });
2894            visited.push(false);
2895
2896            while !dfs_buffer.is_empty() {
2897                let buf_idx = dfs_buffer.len() - 1;
2898                let current_node = dfs_buffer[buf_idx];
2899                let current_elem_idx = current_node.layout_element_index as usize;
2900                let layout_idx = self.layout_elements[current_elem_idx].layout_config_index;
2901                let layout_config = self.layout_configs[layout_idx];
2902                let mut scroll_offset = Vector2::default();
2903
2904                if !visited[buf_idx] {
2905                    visited[buf_idx] = true;
2906
2907                    let current_bbox = BoundingBox::new(
2908                        current_node.position.x,
2909                        current_node.position.y,
2910                        self.layout_elements[current_elem_idx].dimensions.width,
2911                        self.layout_elements[current_elem_idx].dimensions.height,
2912                    );
2913
2914                    // Apply scroll offset
2915                    let mut _scroll_container_data_idx: Option<usize> = None;
2916                    if self.element_has_config(current_elem_idx, ElementConfigType::Clip) {
2917                        if let Some(clip_cfg_idx) = self
2918                            .find_element_config_index(current_elem_idx, ElementConfigType::Clip)
2919                        {
2920                            let clip_config = self.clip_element_configs[clip_cfg_idx];
2921                            for si in 0..self.scroll_container_datas.len() {
2922                                if self.scroll_container_datas[si].layout_element_index
2923                                    == current_elem_idx as i32
2924                                {
2925                                    _scroll_container_data_idx = Some(si);
2926                                    self.scroll_container_datas[si].bounding_box = current_bbox;
2927                                    scroll_offset = clip_config.child_offset;
2928                                    break;
2929                                }
2930                            }
2931                        }
2932                    }
2933
2934                    // Update hash map bounding box
2935                    let elem_id = self.layout_elements[current_elem_idx].id;
2936                    if let Some(item) = self.layout_element_map.get_mut(&elem_id) {
2937                        item.bounding_box = current_bbox;
2938                    }
2939
2940                    // Generate render commands for this element
2941                    let shared_config = self
2942                        .find_element_config_index(current_elem_idx, ElementConfigType::Shared)
2943                        .map(|idx| self.shared_element_configs[idx]);
2944                    let shared = shared_config.unwrap_or_default();
2945                    let mut emit_rectangle = shared.background_color.a > 0.0;
2946                    let offscreen = self.element_is_offscreen(&current_bbox);
2947                    let should_render_base = !offscreen;
2948
2949                    // Get per-element shader effects
2950                    let elem_effects = self.element_effects.get(current_elem_idx).cloned().unwrap_or_default();
2951
2952                    // Get per-element visual rotation
2953                    let elem_visual_rotation = self.element_visual_rotations.get(current_elem_idx).cloned().flatten();
2954                    // Filter out no-op rotations
2955                    let elem_visual_rotation = elem_visual_rotation.filter(|vr| !vr.is_noop());
2956
2957                    // Get per-element shape rotation and compute original bbox
2958                    let elem_shape_rotation = self.element_shape_rotations.get(current_elem_idx).cloned().flatten()
2959                        .filter(|sr| !sr.is_noop());
2960                    // If shape rotation is active, current_bbox has AABB dims.
2961                    // Compute the original-dimension bbox centered within the AABB.
2962                    let shape_draw_bbox = if let Some(ref _sr) = elem_shape_rotation {
2963                        if let Some(orig_dims) = self.element_pre_rotation_dimensions.get(current_elem_idx).copied().flatten() {
2964                            let offset_x = (current_bbox.width - orig_dims.width) / 2.0;
2965                            let offset_y = (current_bbox.height - orig_dims.height) / 2.0;
2966                            BoundingBox::new(
2967                                current_bbox.x + offset_x,
2968                                current_bbox.y + offset_y,
2969                                orig_dims.width,
2970                                orig_dims.height,
2971                            )
2972                        } else {
2973                            current_bbox
2974                        }
2975                    } else {
2976                        current_bbox
2977                    };
2978
2979                    // Emit GroupBegin commands for group shaders BEFORE element drawing
2980                    // so that the element's background, children, and border are all captured.
2981                    // If visual_rotation is present, it is attached to the outermost group.
2982                    let elem_shaders = self.element_shaders.get(current_elem_idx).cloned().unwrap_or_default();
2983
2984                    if !elem_shaders.is_empty() {
2985                        // Emit GroupBegin for each shader (outermost first = reversed order)
2986                        for (i, shader) in elem_shaders.iter().rev().enumerate() {
2987                            // Attach visual_rotation to the outermost GroupBegin (i == 0)
2988                            let vr = if i == 0 { elem_visual_rotation } else { None };
2989                            self.add_render_command(InternalRenderCommand {
2990                                bounding_box: current_bbox,
2991                                command_type: RenderCommandType::GroupBegin,
2992                                effects: vec![shader.clone()],
2993                                id: elem_id,
2994                                z_index: root.z_index,
2995                                visual_rotation: vr,
2996                                ..Default::default()
2997                            });
2998                        }
2999                    } else if let Some(vr) = elem_visual_rotation {
3000                        // No shaders but visual rotation: emit standalone GroupBegin/End
3001                        self.add_render_command(InternalRenderCommand {
3002                            bounding_box: current_bbox,
3003                            command_type: RenderCommandType::GroupBegin,
3004                            effects: vec![],
3005                            id: elem_id,
3006                            z_index: root.z_index,
3007                            visual_rotation: Some(vr),
3008                            ..Default::default()
3009                        });
3010                    }
3011
3012                    // Process each config
3013                    let configs_start = self.layout_elements[current_elem_idx].element_configs.start;
3014                    let configs_length =
3015                        self.layout_elements[current_elem_idx].element_configs.length;
3016
3017                    for cfg_i in 0..configs_length {
3018                        let config = self.element_configs[configs_start + cfg_i as usize];
3019                        let should_render = should_render_base;
3020
3021                        match config.config_type {
3022                            ElementConfigType::Shared
3023                            | ElementConfigType::Aspect
3024                            | ElementConfigType::Floating
3025                            | ElementConfigType::Border => {}
3026                            ElementConfigType::Clip => {
3027                                if should_render {
3028                                    let clip = &self.clip_element_configs[config.config_index];
3029                                    self.add_render_command(InternalRenderCommand {
3030                                        bounding_box: current_bbox,
3031                                        command_type: RenderCommandType::ScissorStart,
3032                                        render_data: InternalRenderData::Clip {
3033                                            horizontal: clip.horizontal,
3034                                            vertical: clip.vertical,
3035                                        },
3036                                        user_data: 0,
3037                                        id: elem_id,
3038                                        z_index: root.z_index,
3039                                        visual_rotation: None,
3040                                        shape_rotation: None,
3041                                        effects: Vec::new(),
3042                                    });
3043                                }
3044                            }
3045                            ElementConfigType::Image => {
3046                                if should_render {
3047                                    let image_data =
3048                                        self.image_element_configs[config.config_index].clone();
3049                                    self.add_render_command(InternalRenderCommand {
3050                                        bounding_box: shape_draw_bbox,
3051                                        command_type: RenderCommandType::Image,
3052                                        render_data: InternalRenderData::Image {
3053                                            background_color: shared.background_color,
3054                                            corner_radius: shared.corner_radius,
3055                                            image_data,
3056                                        },
3057                                        user_data: shared.user_data,
3058                                        id: elem_id,
3059                                        z_index: root.z_index,
3060                                        visual_rotation: None,
3061                                        shape_rotation: elem_shape_rotation,
3062                                        effects: elem_effects.clone(),
3063                                    });
3064                                }
3065                                emit_rectangle = false;
3066                            }
3067                            ElementConfigType::Text => {
3068                                if !should_render {
3069                                    continue;
3070                                }
3071                                let text_config =
3072                                    self.text_element_configs[config.config_index].clone();
3073                                let text_data_idx =
3074                                    self.layout_elements[current_elem_idx].text_data_index;
3075                                if text_data_idx < 0 {
3076                                    continue;
3077                                }
3078                                let text_data = &self.text_element_data[text_data_idx as usize];
3079                                let natural_line_height = text_data.preferred_dimensions.height;
3080                                let final_line_height = if text_config.line_height > 0 {
3081                                    text_config.line_height as f32
3082                                } else {
3083                                    natural_line_height
3084                                };
3085                                let line_height_offset =
3086                                    (final_line_height - natural_line_height) / 2.0;
3087                                let mut y_position = line_height_offset;
3088
3089                                let lines_start = text_data.wrapped_lines_start;
3090                                let lines_length = text_data.wrapped_lines_length;
3091                                let parent_text = text_data.text.clone();
3092
3093                                // Collect line data first to avoid borrow issues
3094                                let lines_data: Vec<_> = (0..lines_length)
3095                                    .map(|li| {
3096                                        let line = &self.wrapped_text_lines[lines_start + li as usize];
3097                                        (line.start, line.length, line.dimensions)
3098                                    })
3099                                    .collect();
3100
3101                                for (line_index, &(start, length, line_dims)) in lines_data.iter().enumerate() {
3102                                    if length == 0 {
3103                                        y_position += final_line_height;
3104                                        continue;
3105                                    }
3106
3107                                    let line_text = parent_text[start..start + length].to_string();
3108
3109                                    let align_width = if buf_idx > 0 {
3110                                        let parent_node = dfs_buffer[buf_idx - 1];
3111                                        let parent_elem_idx =
3112                                            parent_node.layout_element_index as usize;
3113                                        let parent_layout_idx = self.layout_elements
3114                                            [parent_elem_idx]
3115                                            .layout_config_index;
3116                                        let pp = self.layout_configs[parent_layout_idx].padding;
3117                                        self.layout_elements[parent_elem_idx].dimensions.width
3118                                            - pp.left as f32
3119                                            - pp.right as f32
3120                                    } else {
3121                                        current_bbox.width
3122                                    };
3123
3124                                    let mut offset = align_width - line_dims.width;
3125                                    if text_config.alignment == AlignX::Left {
3126                                        offset = 0.0;
3127                                    }
3128                                    if text_config.alignment == AlignX::CenterX {
3129                                        offset /= 2.0;
3130                                    }
3131
3132                                    self.add_render_command(InternalRenderCommand {
3133                                        bounding_box: BoundingBox::new(
3134                                            current_bbox.x + offset,
3135                                            current_bbox.y + y_position,
3136                                            line_dims.width,
3137                                            line_dims.height,
3138                                        ),
3139                                        command_type: RenderCommandType::Text,
3140                                        render_data: InternalRenderData::Text {
3141                                            text: line_text,
3142                                            text_color: text_config.color,
3143                                            font_size: text_config.font_size,
3144                                            letter_spacing: text_config.letter_spacing,
3145                                            line_height: text_config.line_height,
3146                                            font_asset: text_config.font_asset,
3147                                        },
3148                                        user_data: text_config.user_data,
3149                                        id: hash_number(line_index as u32, elem_id).id,
3150                                        z_index: root.z_index,
3151                                        visual_rotation: None,
3152                                        shape_rotation: None,
3153                                        effects: text_config.effects.clone(),
3154                                    });
3155                                    y_position += final_line_height;
3156                                }
3157                            }
3158                            ElementConfigType::Custom => {
3159                                if should_render {
3160                                    let custom_data =
3161                                        self.custom_element_configs[config.config_index].clone();
3162                                    self.add_render_command(InternalRenderCommand {
3163                                        bounding_box: shape_draw_bbox,
3164                                        command_type: RenderCommandType::Custom,
3165                                        render_data: InternalRenderData::Custom {
3166                                            background_color: shared.background_color,
3167                                            corner_radius: shared.corner_radius,
3168                                            custom_data,
3169                                        },
3170                                        user_data: shared.user_data,
3171                                        id: elem_id,
3172                                        z_index: root.z_index,
3173                                        visual_rotation: None,
3174                                        shape_rotation: elem_shape_rotation,
3175                                        effects: elem_effects.clone(),
3176                                    });
3177                                }
3178                                emit_rectangle = false;
3179                            }
3180                            ElementConfigType::TextInput => {
3181                                if should_render {
3182                                    let ti_config = self.text_input_configs[config.config_index].clone();
3183                                    let is_focused = self.focused_element_id == elem_id;
3184
3185                                    // Emit background rectangle FIRST so text renders on top
3186                                    if shared.background_color.a > 0.0 || shared.corner_radius.bottom_left > 0.0 {
3187                                        self.add_render_command(InternalRenderCommand {
3188                                            bounding_box: shape_draw_bbox,
3189                                            command_type: RenderCommandType::Rectangle,
3190                                            render_data: InternalRenderData::Rectangle {
3191                                                background_color: shared.background_color,
3192                                                corner_radius: shared.corner_radius,
3193                                            },
3194                                            user_data: shared.user_data,
3195                                            id: elem_id,
3196                                            z_index: root.z_index,
3197                                            visual_rotation: None,
3198                                            shape_rotation: elem_shape_rotation,
3199                                            effects: elem_effects.clone(),
3200                                        });
3201                                    }
3202
3203                                    // Get or create edit state
3204                                    let state = self.text_edit_states
3205                                        .entry(elem_id)
3206                                        .or_insert_with(crate::text_input::TextEditState::default)
3207                                        .clone();
3208
3209                                    let disp_text = crate::text_input::display_text(
3210                                        &state.text,
3211                                        &ti_config.placeholder,
3212                                        ti_config.is_password && !ti_config.is_multiline,
3213                                    );
3214
3215                                    let is_placeholder = state.text.is_empty();
3216                                    let text_color = if is_placeholder {
3217                                        ti_config.placeholder_color
3218                                    } else {
3219                                        ti_config.text_color
3220                                    };
3221
3222                                    // Measure font height for cursor
3223                                    let natural_font_height = self.font_height(ti_config.font_asset, ti_config.font_size);
3224                                    let line_step = if ti_config.line_height > 0 {
3225                                        ti_config.line_height as f32
3226                                    } else {
3227                                        natural_font_height
3228                                    };
3229                                    // Offset to vertically center text/cursor within each line slot
3230                                    let line_y_offset = (line_step - natural_font_height) / 2.0;
3231
3232                                    // Clip text content to the element's bounding box
3233                                    self.add_render_command(InternalRenderCommand {
3234                                        bounding_box: current_bbox,
3235                                        command_type: RenderCommandType::ScissorStart,
3236                                        render_data: InternalRenderData::Clip {
3237                                            horizontal: true,
3238                                            vertical: true,
3239                                        },
3240                                        user_data: 0,
3241                                        id: hash_number(1000, elem_id).id,
3242                                        z_index: root.z_index,
3243                                        visual_rotation: None,
3244                                        shape_rotation: None,
3245                                        effects: Vec::new(),
3246                                    });
3247
3248                                    if ti_config.is_multiline {
3249                                        // ── Multiline rendering (with word wrapping) ──
3250                                        let scroll_offset_x = state.scroll_offset;
3251                                        let scroll_offset_y = state.scroll_offset_y;
3252
3253                                        let visual_lines = if let Some(ref measure_fn) = self.measure_text_fn {
3254                                            crate::text_input::wrap_lines(
3255                                                &disp_text,
3256                                                current_bbox.width,
3257                                                ti_config.font_asset,
3258                                                ti_config.font_size,
3259                                                measure_fn.as_ref(),
3260                                            )
3261                                        } else {
3262                                            vec![crate::text_input::VisualLine {
3263                                                text: disp_text.clone(),
3264                                                global_char_start: 0,
3265                                                char_count: disp_text.chars().count(),
3266                                            }]
3267                                        };
3268
3269                                        let (cursor_line, cursor_col) = if is_placeholder {
3270                                            (0, 0)
3271                                        } else {
3272                                            #[cfg(feature = "text-styling")]
3273                                            let raw_cursor = state.cursor_pos_raw();
3274                                            #[cfg(not(feature = "text-styling"))]
3275                                            let raw_cursor = state.cursor_pos;
3276                                            crate::text_input::cursor_to_visual_pos(&visual_lines, raw_cursor)
3277                                        };
3278
3279                                        // Compute per-line char positions
3280                                        let line_positions: Vec<Vec<f32>> = if let Some(ref measure_fn) = self.measure_text_fn {
3281                                            visual_lines.iter().map(|vl| {
3282                                                crate::text_input::compute_char_x_positions(
3283                                                    &vl.text,
3284                                                    ti_config.font_asset,
3285                                                    ti_config.font_size,
3286                                                    measure_fn.as_ref(),
3287                                                )
3288                                            }).collect()
3289                                        } else {
3290                                            visual_lines.iter().map(|_| vec![0.0]).collect()
3291                                        };
3292
3293                                        // Selection rendering (multiline)
3294                                        if is_focused {
3295                                            #[cfg(feature = "text-styling")]
3296                                            let sel_range = state.selection_range_raw();
3297                                            #[cfg(not(feature = "text-styling"))]
3298                                            let sel_range = state.selection_range();
3299                                            if let Some((sel_start, sel_end)) = sel_range {
3300                                                let (sel_start_line, sel_start_col) = crate::text_input::cursor_to_visual_pos(&visual_lines, sel_start);
3301                                                let (sel_end_line, sel_end_col) = crate::text_input::cursor_to_visual_pos(&visual_lines, sel_end);
3302                                                for (line_idx, vl) in visual_lines.iter().enumerate() {
3303                                                    if line_idx < sel_start_line || line_idx > sel_end_line {
3304                                                        continue;
3305                                                    }
3306                                                    let positions = &line_positions[line_idx];
3307                                                    let col_start = if line_idx == sel_start_line { sel_start_col } else { 0 };
3308                                                    let col_end = if line_idx == sel_end_line { sel_end_col } else { vl.char_count };
3309                                                    let x_start = positions.get(col_start).copied().unwrap_or(0.0);
3310                                                    let x_end = positions.get(col_end).copied().unwrap_or(
3311                                                        positions.last().copied().unwrap_or(0.0)
3312                                                    );
3313                                                    let sel_width = x_end - x_start;
3314                                                    if sel_width > 0.0 {
3315                                                        let sel_y = current_bbox.y + line_idx as f32 * line_step - scroll_offset_y;
3316                                                        self.add_render_command(InternalRenderCommand {
3317                                                            bounding_box: BoundingBox::new(
3318                                                                current_bbox.x - scroll_offset_x + x_start,
3319                                                                sel_y,
3320                                                                sel_width,
3321                                                                line_step,
3322                                                            ),
3323                                                            command_type: RenderCommandType::Rectangle,
3324                                                            render_data: InternalRenderData::Rectangle {
3325                                                                background_color: ti_config.selection_color,
3326                                                                corner_radius: CornerRadius::default(),
3327                                                            },
3328                                                            user_data: 0,
3329                                                            id: hash_number(1001 + line_idx as u32, elem_id).id,
3330                                                            z_index: root.z_index,
3331                                                            visual_rotation: None,
3332                                                            shape_rotation: None,
3333                                                            effects: Vec::new(),
3334                                                        });
3335                                                    }
3336                                                }
3337                                            }
3338                                        }
3339
3340                                        // Render each visual line of text
3341                                        for (line_idx, vl) in visual_lines.iter().enumerate() {
3342                                            if !vl.text.is_empty() {
3343                                                let positions = &line_positions[line_idx];
3344                                                let text_width = positions.last().copied().unwrap_or(0.0);
3345                                                let line_y = current_bbox.y + line_idx as f32 * line_step + line_y_offset - scroll_offset_y;
3346                                                self.add_render_command(InternalRenderCommand {
3347                                                    bounding_box: BoundingBox::new(
3348                                                        current_bbox.x - scroll_offset_x,
3349                                                        line_y,
3350                                                        text_width,
3351                                                        natural_font_height,
3352                                                    ),
3353                                                    command_type: RenderCommandType::Text,
3354                                                    render_data: InternalRenderData::Text {
3355                                                        text: vl.text.clone(),
3356                                                        text_color,
3357                                                        font_size: ti_config.font_size,
3358                                                        letter_spacing: 0,
3359                                                        line_height: 0,
3360                                                        font_asset: ti_config.font_asset,
3361                                                    },
3362                                                    user_data: 0,
3363                                                    id: hash_number(2000 + line_idx as u32, elem_id).id,
3364                                                    z_index: root.z_index,
3365                                                    visual_rotation: None,
3366                                                    shape_rotation: None,
3367                                                    effects: Vec::new(),
3368                                                });
3369                                            }
3370                                        }
3371
3372                                        // Cursor (multiline)
3373                                        if is_focused && state.cursor_visible() {
3374                                            let cursor_positions = &line_positions[cursor_line.min(line_positions.len() - 1)];
3375                                            let cursor_x_pos = cursor_positions.get(cursor_col).copied().unwrap_or(0.0);
3376                                            let cursor_y = current_bbox.y + cursor_line as f32 * line_step - scroll_offset_y;
3377                                            self.add_render_command(InternalRenderCommand {
3378                                                bounding_box: BoundingBox::new(
3379                                                    current_bbox.x - scroll_offset_x + cursor_x_pos,
3380                                                    cursor_y,
3381                                                    2.0,
3382                                                    line_step,
3383                                                ),
3384                                                command_type: RenderCommandType::Rectangle,
3385                                                render_data: InternalRenderData::Rectangle {
3386                                                    background_color: ti_config.cursor_color,
3387                                                    corner_radius: CornerRadius::default(),
3388                                                },
3389                                                user_data: 0,
3390                                                id: hash_number(1003, elem_id).id,
3391                                                z_index: root.z_index,
3392                                                visual_rotation: None,
3393                                                shape_rotation: None,
3394                                                effects: Vec::new(),
3395                                            });
3396                                        }
3397                                    } else {
3398                                        // ── Single-line rendering ──
3399                                        let char_x_positions = if let Some(ref measure_fn) = self.measure_text_fn {
3400                                            crate::text_input::compute_char_x_positions(
3401                                                &disp_text,
3402                                                ti_config.font_asset,
3403                                                ti_config.font_size,
3404                                                measure_fn.as_ref(),
3405                                            )
3406                                        } else {
3407                                            vec![0.0]
3408                                        };
3409
3410                                        let scroll_offset = state.scroll_offset;
3411                                        let text_x = current_bbox.x - scroll_offset;
3412                                        let font_height = natural_font_height;
3413
3414                                        // Convert cursor/selection to raw positions for char_x_positions indexing
3415                                        #[cfg(feature = "text-styling")]
3416                                        let render_cursor_pos = if is_placeholder { 0 } else { state.cursor_pos_raw() };
3417                                        #[cfg(not(feature = "text-styling"))]
3418                                        let render_cursor_pos = if is_placeholder { 0 } else { state.cursor_pos };
3419
3420                                        #[cfg(feature = "text-styling")]
3421                                        let render_selection = if !is_placeholder { state.selection_range_raw() } else { None };
3422                                        #[cfg(not(feature = "text-styling"))]
3423                                        let render_selection = if !is_placeholder { state.selection_range() } else { None };
3424
3425                                        // Selection highlight
3426                                        if is_focused {
3427                                            if let Some((sel_start, sel_end)) = render_selection {
3428                                                let sel_start_x = char_x_positions.get(sel_start).copied().unwrap_or(0.0);
3429                                                let sel_end_x = char_x_positions.get(sel_end).copied().unwrap_or(0.0);
3430                                                let sel_width = sel_end_x - sel_start_x;
3431                                                if sel_width > 0.0 {
3432                                                    let sel_y = current_bbox.y + (current_bbox.height - font_height) / 2.0;
3433                                                    self.add_render_command(InternalRenderCommand {
3434                                                        bounding_box: BoundingBox::new(
3435                                                            text_x + sel_start_x,
3436                                                            sel_y,
3437                                                            sel_width,
3438                                                            font_height,
3439                                                        ),
3440                                                        command_type: RenderCommandType::Rectangle,
3441                                                        render_data: InternalRenderData::Rectangle {
3442                                                            background_color: ti_config.selection_color,
3443                                                            corner_radius: CornerRadius::default(),
3444                                                        },
3445                                                        user_data: 0,
3446                                                        id: hash_number(1001, elem_id).id,
3447                                                        z_index: root.z_index,
3448                                                        visual_rotation: None,
3449                                                        shape_rotation: None,
3450                                                        effects: Vec::new(),
3451                                                    });
3452                                                }
3453                                            }
3454                                        }
3455
3456                                        // Text
3457                                        if !disp_text.is_empty() {
3458                                            let text_width = char_x_positions.last().copied().unwrap_or(0.0);
3459                                            let text_y = current_bbox.y + (current_bbox.height - font_height) / 2.0;
3460                                            self.add_render_command(InternalRenderCommand {
3461                                                bounding_box: BoundingBox::new(
3462                                                    text_x,
3463                                                    text_y,
3464                                                    text_width,
3465                                                    font_height,
3466                                                ),
3467                                                command_type: RenderCommandType::Text,
3468                                                render_data: InternalRenderData::Text {
3469                                                    text: disp_text,
3470                                                    text_color,
3471                                                    font_size: ti_config.font_size,
3472                                                    letter_spacing: 0,
3473                                                    line_height: 0,
3474                                                    font_asset: ti_config.font_asset,
3475                                                },
3476                                                user_data: 0,
3477                                                id: hash_number(1002, elem_id).id,
3478                                                z_index: root.z_index,
3479                                                visual_rotation: None,
3480                                                shape_rotation: None,
3481                                                effects: Vec::new(),
3482                                            });
3483                                        }
3484
3485                                        // Cursor
3486                                        if is_focused && state.cursor_visible() {
3487                                            let cursor_x_pos = char_x_positions
3488                                                .get(render_cursor_pos)
3489                                                .copied()
3490                                                .unwrap_or(0.0);
3491                                            let cursor_y = current_bbox.y + (current_bbox.height - font_height) / 2.0;
3492                                            self.add_render_command(InternalRenderCommand {
3493                                                bounding_box: BoundingBox::new(
3494                                                    text_x + cursor_x_pos,
3495                                                    cursor_y,
3496                                                    2.0,
3497                                                    font_height,
3498                                                ),
3499                                                command_type: RenderCommandType::Rectangle,
3500                                                render_data: InternalRenderData::Rectangle {
3501                                                    background_color: ti_config.cursor_color,
3502                                                    corner_radius: CornerRadius::default(),
3503                                                },
3504                                                user_data: 0,
3505                                                id: hash_number(1003, elem_id).id,
3506                                                z_index: root.z_index,
3507                                                visual_rotation: None,
3508                                                shape_rotation: None,
3509                                                effects: Vec::new(),
3510                                            });
3511                                        }
3512                                    }
3513
3514                                    // End clipping
3515                                    self.add_render_command(InternalRenderCommand {
3516                                        bounding_box: current_bbox,
3517                                        command_type: RenderCommandType::ScissorEnd,
3518                                        render_data: InternalRenderData::None,
3519                                        user_data: 0,
3520                                        id: hash_number(1004, elem_id).id,
3521                                        z_index: root.z_index,
3522                                        visual_rotation: None,
3523                                        shape_rotation: None,
3524                                        effects: Vec::new(),
3525                                    });
3526                                }
3527                                // Background already emitted above; skip the default rectangle
3528                                emit_rectangle = false;
3529                            }
3530                        }
3531                    }
3532
3533                    if emit_rectangle {
3534                        self.add_render_command(InternalRenderCommand {
3535                            bounding_box: shape_draw_bbox,
3536                            command_type: RenderCommandType::Rectangle,
3537                            render_data: InternalRenderData::Rectangle {
3538                                background_color: shared.background_color,
3539                                corner_radius: shared.corner_radius,
3540                            },
3541                            user_data: shared.user_data,
3542                            id: elem_id,
3543                            z_index: root.z_index,
3544                            visual_rotation: None,
3545                            shape_rotation: elem_shape_rotation,
3546                            effects: elem_effects.clone(),
3547                        });
3548                    }
3549
3550                    // Setup child alignment
3551                    let is_text =
3552                        self.element_has_config(current_elem_idx, ElementConfigType::Text);
3553                    if !is_text {
3554                        let children_start =
3555                            self.layout_elements[current_elem_idx].children_start;
3556                        let children_length =
3557                            self.layout_elements[current_elem_idx].children_length as usize;
3558
3559                        if layout_config.layout_direction == LayoutDirection::LeftToRight {
3560                            let mut content_width: f32 = 0.0;
3561                            for ci in 0..children_length {
3562                                let child_idx =
3563                                    self.layout_element_children[children_start + ci] as usize;
3564                                content_width +=
3565                                    self.layout_elements[child_idx].dimensions.width;
3566                            }
3567                            content_width += children_length.saturating_sub(1) as f32
3568                                * layout_config.child_gap as f32;
3569                            let mut extra_space = self.layout_elements[current_elem_idx]
3570                                .dimensions
3571                                .width
3572                                - (layout_config.padding.left + layout_config.padding.right) as f32
3573                                - content_width;
3574                            match layout_config.child_alignment.x {
3575                                AlignX::Left => extra_space = 0.0,
3576                                AlignX::CenterX => extra_space /= 2.0,
3577                                _ => {} // Right - keep full extra_space
3578                            }
3579                            dfs_buffer[buf_idx].next_child_offset.x += extra_space;
3580                        } else {
3581                            let mut content_height: f32 = 0.0;
3582                            for ci in 0..children_length {
3583                                let child_idx =
3584                                    self.layout_element_children[children_start + ci] as usize;
3585                                content_height +=
3586                                    self.layout_elements[child_idx].dimensions.height;
3587                            }
3588                            content_height += children_length.saturating_sub(1) as f32
3589                                * layout_config.child_gap as f32;
3590                            let mut extra_space = self.layout_elements[current_elem_idx]
3591                                .dimensions
3592                                .height
3593                                - (layout_config.padding.top + layout_config.padding.bottom) as f32
3594                                - content_height;
3595                            match layout_config.child_alignment.y {
3596                                AlignY::Top => extra_space = 0.0,
3597                                AlignY::CenterY => extra_space /= 2.0,
3598                                _ => {}
3599                            }
3600                            dfs_buffer[buf_idx].next_child_offset.y += extra_space;
3601                        }
3602
3603                        // Update scroll container content size
3604                        if let Some(si) = _scroll_container_data_idx {
3605                            let child_gap_total = children_length.saturating_sub(1) as f32
3606                                * layout_config.child_gap as f32;
3607                            let lr_padding = (layout_config.padding.left + layout_config.padding.right) as f32;
3608                            let tb_padding = (layout_config.padding.top + layout_config.padding.bottom) as f32;
3609
3610                            let (content_w, content_h) = if layout_config.layout_direction == LayoutDirection::LeftToRight {
3611                                // LeftToRight: width = sum of children + gap, height = max of children
3612                                let w: f32 = (0..children_length)
3613                                    .map(|ci| {
3614                                        let idx = self.layout_element_children[children_start + ci] as usize;
3615                                        self.layout_elements[idx].dimensions.width
3616                                    })
3617                                    .sum::<f32>()
3618                                    + lr_padding + child_gap_total;
3619                                let h: f32 = (0..children_length)
3620                                    .map(|ci| {
3621                                        let idx = self.layout_element_children[children_start + ci] as usize;
3622                                        self.layout_elements[idx].dimensions.height
3623                                    })
3624                                    .fold(0.0_f32, |a, b| a.max(b))
3625                                    + tb_padding;
3626                                (w, h)
3627                            } else {
3628                                // TopToBottom: width = max of children, height = sum of children + gap
3629                                let w: f32 = (0..children_length)
3630                                    .map(|ci| {
3631                                        let idx = self.layout_element_children[children_start + ci] as usize;
3632                                        self.layout_elements[idx].dimensions.width
3633                                    })
3634                                    .fold(0.0_f32, |a, b| a.max(b))
3635                                    + lr_padding;
3636                                let h: f32 = (0..children_length)
3637                                    .map(|ci| {
3638                                        let idx = self.layout_element_children[children_start + ci] as usize;
3639                                        self.layout_elements[idx].dimensions.height
3640                                    })
3641                                    .sum::<f32>()
3642                                    + tb_padding + child_gap_total;
3643                                (w, h)
3644                            };
3645                            self.scroll_container_datas[si].content_size =
3646                                Dimensions::new(content_w, content_h);
3647                        }
3648                    }
3649                } else {
3650                    // Returning upward in DFS
3651
3652                    let mut close_clip = false;
3653
3654                    if self.element_has_config(current_elem_idx, ElementConfigType::Clip) {
3655                        close_clip = true;
3656                        if let Some(clip_cfg_idx) = self
3657                            .find_element_config_index(current_elem_idx, ElementConfigType::Clip)
3658                        {
3659                            let clip_config = self.clip_element_configs[clip_cfg_idx];
3660                            for si in 0..self.scroll_container_datas.len() {
3661                                if self.scroll_container_datas[si].layout_element_index
3662                                    == current_elem_idx as i32
3663                                {
3664                                    scroll_offset = clip_config.child_offset;
3665                                    break;
3666                                }
3667                            }
3668                        }
3669                    }
3670
3671                    // Generate border render commands
3672                    if self.element_has_config(current_elem_idx, ElementConfigType::Border) {
3673                        let border_elem_id = self.layout_elements[current_elem_idx].id;
3674                        if let Some(border_bbox) = self.layout_element_map.get(&border_elem_id).map(|item| item.bounding_box) {
3675                            let bbox = border_bbox;
3676                            if !self.element_is_offscreen(&bbox) {
3677                                let shared = self
3678                                    .find_element_config_index(
3679                                        current_elem_idx,
3680                                        ElementConfigType::Shared,
3681                                    )
3682                                    .map(|idx| self.shared_element_configs[idx])
3683                                    .unwrap_or_default();
3684                                let border_cfg_idx = self
3685                                    .find_element_config_index(
3686                                        current_elem_idx,
3687                                        ElementConfigType::Border,
3688                                    )
3689                                    .unwrap();
3690                                let border_config = self.border_element_configs[border_cfg_idx];
3691
3692                                let children_count =
3693                                    self.layout_elements[current_elem_idx].children_length;
3694                                self.add_render_command(InternalRenderCommand {
3695                                    bounding_box: bbox,
3696                                    command_type: RenderCommandType::Border,
3697                                    render_data: InternalRenderData::Border {
3698                                        color: border_config.color,
3699                                        corner_radius: shared.corner_radius,
3700                                        width: border_config.width,
3701                                    },
3702                                    user_data: shared.user_data,
3703                                    id: hash_number(
3704                                        self.layout_elements[current_elem_idx].id,
3705                                        children_count as u32,
3706                                    )
3707                                    .id,
3708                                    z_index: root.z_index,
3709                                    visual_rotation: None,
3710                                    shape_rotation: None,
3711                                    effects: Vec::new(),
3712                                });
3713
3714                                // between-children borders
3715                                if border_config.width.between_children > 0
3716                                    && border_config.color.a > 0.0
3717                                {
3718                                    let half_gap = layout_config.child_gap as f32 / 2.0;
3719                                    let children_start =
3720                                        self.layout_elements[current_elem_idx].children_start;
3721                                    let children_length = self.layout_elements[current_elem_idx]
3722                                        .children_length
3723                                        as usize;
3724
3725                                    if layout_config.layout_direction
3726                                        == LayoutDirection::LeftToRight
3727                                    {
3728                                        let mut border_offset_x =
3729                                            layout_config.padding.left as f32 - half_gap;
3730                                        for ci in 0..children_length {
3731                                            let child_idx = self.layout_element_children
3732                                                [children_start + ci]
3733                                                as usize;
3734                                            if ci > 0 {
3735                                                self.add_render_command(InternalRenderCommand {
3736                                                    bounding_box: BoundingBox::new(
3737                                                        bbox.x + border_offset_x + scroll_offset.x,
3738                                                        bbox.y + scroll_offset.y,
3739                                                        border_config.width.between_children as f32,
3740                                                        self.layout_elements[current_elem_idx]
3741                                                            .dimensions
3742                                                            .height,
3743                                                    ),
3744                                                    command_type: RenderCommandType::Rectangle,
3745                                                    render_data: InternalRenderData::Rectangle {
3746                                                        background_color: border_config.color,
3747                                                        corner_radius: CornerRadius::default(),
3748                                                    },
3749                                                    user_data: shared.user_data,
3750                                                    id: hash_number(
3751                                                        self.layout_elements[current_elem_idx].id,
3752                                                        children_count as u32 + 1 + ci as u32,
3753                                                    )
3754                                                    .id,
3755                                                    z_index: root.z_index,
3756                                                    visual_rotation: None,
3757                                                    shape_rotation: None,
3758                                                    effects: Vec::new(),
3759                                                });
3760                                            }
3761                                            border_offset_x +=
3762                                                self.layout_elements[child_idx].dimensions.width
3763                                                    + layout_config.child_gap as f32;
3764                                        }
3765                                    } else {
3766                                        let mut border_offset_y =
3767                                            layout_config.padding.top as f32 - half_gap;
3768                                        for ci in 0..children_length {
3769                                            let child_idx = self.layout_element_children
3770                                                [children_start + ci]
3771                                                as usize;
3772                                            if ci > 0 {
3773                                                self.add_render_command(InternalRenderCommand {
3774                                                    bounding_box: BoundingBox::new(
3775                                                        bbox.x + scroll_offset.x,
3776                                                        bbox.y + border_offset_y + scroll_offset.y,
3777                                                        self.layout_elements[current_elem_idx]
3778                                                            .dimensions
3779                                                            .width,
3780                                                        border_config.width.between_children as f32,
3781                                                    ),
3782                                                    command_type: RenderCommandType::Rectangle,
3783                                                    render_data: InternalRenderData::Rectangle {
3784                                                        background_color: border_config.color,
3785                                                        corner_radius: CornerRadius::default(),
3786                                                    },
3787                                                    user_data: shared.user_data,
3788                                                    id: hash_number(
3789                                                        self.layout_elements[current_elem_idx].id,
3790                                                        children_count as u32 + 1 + ci as u32,
3791                                                    )
3792                                                    .id,
3793                                                    z_index: root.z_index,
3794                                                    visual_rotation: None,
3795                                                    shape_rotation: None,
3796                                                    effects: Vec::new(),
3797                                                });
3798                                            }
3799                                            border_offset_y +=
3800                                                self.layout_elements[child_idx].dimensions.height
3801                                                    + layout_config.child_gap as f32;
3802                                        }
3803                                    }
3804                                }
3805                            }
3806                        }
3807                    }
3808
3809                    if close_clip {
3810                        let root_elem = &self.layout_elements[root_elem_idx];
3811                        self.add_render_command(InternalRenderCommand {
3812                            command_type: RenderCommandType::ScissorEnd,
3813                            id: hash_number(
3814                                self.layout_elements[current_elem_idx].id,
3815                                root_elem.children_length as u32 + 11,
3816                            )
3817                            .id,
3818                            ..Default::default()
3819                        });
3820                    }
3821
3822                    // Emit GroupEnd commands AFTER border and scissor (innermost first, outermost last)
3823                    let elem_shaders = self.element_shaders.get(current_elem_idx).cloned().unwrap_or_default();
3824                    let elem_visual_rotation = self.element_visual_rotations.get(current_elem_idx).cloned().flatten()
3825                        .filter(|vr| !vr.is_noop());
3826
3827                    // GroupEnd for each shader
3828                    for _shader in elem_shaders.iter() {
3829                        self.add_render_command(InternalRenderCommand {
3830                            command_type: RenderCommandType::GroupEnd,
3831                            id: self.layout_elements[current_elem_idx].id,
3832                            z_index: root.z_index,
3833                            ..Default::default()
3834                        });
3835                    }
3836                    // If no shaders but visual rotation was present, emit its GroupEnd
3837                    if elem_shaders.is_empty() && elem_visual_rotation.is_some() {
3838                        self.add_render_command(InternalRenderCommand {
3839                            command_type: RenderCommandType::GroupEnd,
3840                            id: self.layout_elements[current_elem_idx].id,
3841                            z_index: root.z_index,
3842                            ..Default::default()
3843                        });
3844                    }
3845
3846                    dfs_buffer.pop();
3847                    visited.pop();
3848                    continue;
3849                }
3850
3851                // Add children to DFS buffer (in reverse for correct traversal order)
3852                let is_text =
3853                    self.element_has_config(current_elem_idx, ElementConfigType::Text);
3854                if !is_text {
3855                    let children_start = self.layout_elements[current_elem_idx].children_start;
3856                    let children_length =
3857                        self.layout_elements[current_elem_idx].children_length as usize;
3858
3859                    // Pre-grow dfs_buffer and visited
3860                    let new_len = dfs_buffer.len() + children_length;
3861                    dfs_buffer.resize(new_len, LayoutElementTreeNode::default());
3862                    visited.resize(new_len, false);
3863
3864                    for ci in 0..children_length {
3865                        let child_idx =
3866                            self.layout_element_children[children_start + ci] as usize;
3867                        let child_layout_idx =
3868                            self.layout_elements[child_idx].layout_config_index;
3869
3870                        // Alignment along non-layout axis
3871                        let mut child_offset = dfs_buffer[buf_idx].next_child_offset;
3872                        if layout_config.layout_direction == LayoutDirection::LeftToRight {
3873                            child_offset.y = layout_config.padding.top as f32;
3874                            let whitespace = self.layout_elements[current_elem_idx].dimensions.height
3875                                - (layout_config.padding.top + layout_config.padding.bottom) as f32
3876                                - self.layout_elements[child_idx].dimensions.height;
3877                            match layout_config.child_alignment.y {
3878                                AlignY::Top => {}
3879                                AlignY::CenterY => {
3880                                    child_offset.y += whitespace / 2.0;
3881                                }
3882                                AlignY::Bottom => {
3883                                    child_offset.y += whitespace;
3884                                }
3885                            }
3886                        } else {
3887                            child_offset.x = layout_config.padding.left as f32;
3888                            let whitespace = self.layout_elements[current_elem_idx].dimensions.width
3889                                - (layout_config.padding.left + layout_config.padding.right) as f32
3890                                - self.layout_elements[child_idx].dimensions.width;
3891                            match layout_config.child_alignment.x {
3892                                AlignX::Left => {}
3893                                AlignX::CenterX => {
3894                                    child_offset.x += whitespace / 2.0;
3895                                }
3896                                AlignX::Right => {
3897                                    child_offset.x += whitespace;
3898                                }
3899                            }
3900                        }
3901
3902                        let child_position = Vector2::new(
3903                            dfs_buffer[buf_idx].position.x + child_offset.x + scroll_offset.x,
3904                            dfs_buffer[buf_idx].position.y + child_offset.y + scroll_offset.y,
3905                        );
3906
3907                        let new_node_index = new_len - 1 - ci;
3908                        let child_padding_left =
3909                            self.layout_configs[child_layout_idx].padding.left as f32;
3910                        let child_padding_top =
3911                            self.layout_configs[child_layout_idx].padding.top as f32;
3912                        dfs_buffer[new_node_index] = LayoutElementTreeNode {
3913                            layout_element_index: child_idx as i32,
3914                            position: child_position,
3915                            next_child_offset: Vector2::new(child_padding_left, child_padding_top),
3916                        };
3917                        visited[new_node_index] = false;
3918
3919                        // Update parent offset
3920                        if layout_config.layout_direction == LayoutDirection::LeftToRight {
3921                            dfs_buffer[buf_idx].next_child_offset.x +=
3922                                self.layout_elements[child_idx].dimensions.width
3923                                    + layout_config.child_gap as f32;
3924                        } else {
3925                            dfs_buffer[buf_idx].next_child_offset.y +=
3926                                self.layout_elements[child_idx].dimensions.height
3927                                    + layout_config.child_gap as f32;
3928                        }
3929                    }
3930                }
3931            }
3932
3933            // End clip
3934            if root.clip_element_id != 0 {
3935                let root_elem = &self.layout_elements[root_elem_idx];
3936                self.add_render_command(InternalRenderCommand {
3937                    command_type: RenderCommandType::ScissorEnd,
3938                    id: hash_number(root_elem.id, root_elem.children_length as u32 + 11).id,
3939                    ..Default::default()
3940                });
3941            }
3942        }
3943
3944        // Focus ring: render a border around the focused element (keyboard focus only)
3945        if self.focused_element_id != 0 && self.focus_from_keyboard {
3946            // Check if the element's accessibility config allows the ring
3947            let a11y = self.accessibility_configs.get(&self.focused_element_id);
3948            let show_ring = a11y.map_or(true, |c| c.show_ring);
3949            if show_ring {
3950                if let Some(item) = self.layout_element_map.get(&self.focused_element_id) {
3951                    let bbox = item.bounding_box;
3952                    if !self.element_is_offscreen(&bbox) {
3953                        let elem_idx = item.layout_element_index as usize;
3954                        let corner_radius = self
3955                            .find_element_config_index(elem_idx, ElementConfigType::Shared)
3956                            .map(|idx| self.shared_element_configs[idx].corner_radius)
3957                            .unwrap_or_default();
3958                        let ring_width = a11y.and_then(|c| c.ring_width).unwrap_or(2);
3959                        let ring_color = a11y.and_then(|c| c.ring_color).unwrap_or(Color::rgba(255.0, 60.0, 40.0, 255.0));
3960                        // Expand bounding box outward by ring width so the ring doesn't overlap content
3961                        let expanded_bbox = BoundingBox::new(
3962                            bbox.x - ring_width as f32,
3963                            bbox.y - ring_width as f32,
3964                            bbox.width + ring_width as f32 * 2.0,
3965                            bbox.height + ring_width as f32 * 2.0,
3966                        );
3967                        self.add_render_command(InternalRenderCommand {
3968                            bounding_box: expanded_bbox,
3969                            command_type: RenderCommandType::Border,
3970                            render_data: InternalRenderData::Border {
3971                                color: ring_color,
3972                                corner_radius: CornerRadius {
3973                                    top_left: corner_radius.top_left + ring_width as f32,
3974                                    top_right: corner_radius.top_right + ring_width as f32,
3975                                    bottom_left: corner_radius.bottom_left + ring_width as f32,
3976                                    bottom_right: corner_radius.bottom_right + ring_width as f32,
3977                                },
3978                                width: BorderWidth {
3979                                    left: ring_width,
3980                                    right: ring_width,
3981                                    top: ring_width,
3982                                    bottom: ring_width,
3983                                    between_children: 0,
3984                                },
3985                            },
3986                            id: hash_number(self.focused_element_id, 0xF0C5).id,
3987                            z_index: 32764, // just below debug panel
3988                            ..Default::default()
3989                        });
3990                    }
3991                }
3992            }
3993        }
3994    }
3995
3996    pub fn set_layout_dimensions(&mut self, dimensions: Dimensions) {
3997        self.layout_dimensions = dimensions;
3998    }
3999
4000    pub fn set_pointer_state(&mut self, position: Vector2, is_down: bool) {
4001        if self.boolean_warnings.max_elements_exceeded {
4002            return;
4003        }
4004        self.pointer_info.position = position;
4005        self.pointer_over_ids.clear();
4006
4007        // Check which elements are under the pointer
4008        for root_index in (0..self.layout_element_tree_roots.len()).rev() {
4009            let root = self.layout_element_tree_roots[root_index];
4010            let mut dfs: Vec<i32> = vec![root.layout_element_index];
4011            let mut vis: Vec<bool> = vec![false];
4012            let mut found = false;
4013
4014            while !dfs.is_empty() {
4015                let idx = dfs.len() - 1;
4016                if vis[idx] {
4017                    dfs.pop();
4018                    vis.pop();
4019                    continue;
4020                }
4021                vis[idx] = true;
4022                let current_idx = dfs[idx] as usize;
4023                let elem_id = self.layout_elements[current_idx].id;
4024
4025                // Copy data from map to avoid borrow issues with mutable access later
4026                let map_data = self.layout_element_map.get(&elem_id).map(|item| {
4027                    (item.bounding_box, item.element_id.clone(), item.on_hover_fn.is_some())
4028                });
4029                if let Some((raw_box, elem_id_copy, has_hover)) = map_data {
4030                    let mut elem_box = raw_box;
4031                    elem_box.x -= root.pointer_offset.x;
4032                    elem_box.y -= root.pointer_offset.y;
4033
4034                    let clip_id =
4035                        self.layout_element_clip_element_ids[current_idx] as u32;
4036                    let clip_ok = clip_id == 0
4037                        || self
4038                            .layout_element_map
4039                            .get(&clip_id)
4040                            .map(|ci| {
4041                                point_is_inside_rect(
4042                                    position,
4043                                    ci.bounding_box,
4044                                )
4045                            })
4046                            .unwrap_or(false);
4047
4048                    if point_is_inside_rect(position, elem_box) && clip_ok {
4049                        // Call hover callbacks
4050                        if has_hover {
4051                            let pointer_data = self.pointer_info;
4052                            if let Some(item) = self.layout_element_map.get_mut(&elem_id) {
4053                                if let Some(ref mut callback) = item.on_hover_fn {
4054                                    callback(elem_id_copy.clone(), pointer_data);
4055                                }
4056                            }
4057                        }
4058                        self.pointer_over_ids.push(elem_id_copy);
4059                        found = true;
4060                    }
4061
4062                    if self.element_has_config(current_idx, ElementConfigType::Text) {
4063                        dfs.pop();
4064                        vis.pop();
4065                        continue;
4066                    }
4067                    let children_start = self.layout_elements[current_idx].children_start;
4068                    let children_length =
4069                        self.layout_elements[current_idx].children_length as usize;
4070                    for ci in (0..children_length).rev() {
4071                        let child = self.layout_element_children[children_start + ci];
4072                        dfs.push(child);
4073                        vis.push(false);
4074                    }
4075                } else {
4076                    dfs.pop();
4077                    vis.pop();
4078                }
4079            }
4080
4081            if found {
4082                let root_elem_idx = root.layout_element_index as usize;
4083                if self.element_has_config(root_elem_idx, ElementConfigType::Floating) {
4084                    if let Some(cfg_idx) = self
4085                        .find_element_config_index(root_elem_idx, ElementConfigType::Floating)
4086                    {
4087                        if self.floating_element_configs[cfg_idx].pointer_capture_mode
4088                            == PointerCaptureMode::Capture
4089                        {
4090                            break;
4091                        }
4092                    }
4093                }
4094            }
4095        }
4096
4097        // Update pointer state
4098        if is_down {
4099            match self.pointer_info.state {
4100                PointerDataInteractionState::PressedThisFrame => {
4101                    self.pointer_info.state = PointerDataInteractionState::Pressed;
4102                }
4103                s if s != PointerDataInteractionState::Pressed => {
4104                    self.pointer_info.state = PointerDataInteractionState::PressedThisFrame;
4105                }
4106                _ => {}
4107            }
4108        } else {
4109            match self.pointer_info.state {
4110                PointerDataInteractionState::ReleasedThisFrame => {
4111                    self.pointer_info.state = PointerDataInteractionState::Released;
4112                }
4113                s if s != PointerDataInteractionState::Released => {
4114                    self.pointer_info.state = PointerDataInteractionState::ReleasedThisFrame;
4115                }
4116                _ => {}
4117            }
4118        }
4119
4120        // Fire on_press / on_release callbacks and track pressed element
4121        match self.pointer_info.state {
4122            PointerDataInteractionState::PressedThisFrame => {
4123                // Check if clicked element is a text input
4124                let clicked_text_input = self.pointer_over_ids.last()
4125                    .and_then(|top| self.layout_element_map.get(&top.id))
4126                    .map(|item| item.is_text_input)
4127                    .unwrap_or(false);
4128
4129                if clicked_text_input {
4130                    // Focus the text input (or keep focus if already focused)
4131                    self.focus_from_keyboard = false;
4132                    if let Some(top) = self.pointer_over_ids.last().cloned() {
4133                        if self.focused_element_id != top.id {
4134                            self.change_focus(top.id);
4135                        }
4136                        // Compute click x,y relative to the element's bounding box
4137                        if let Some(item) = self.layout_element_map.get(&top.id) {
4138                            let click_x = self.pointer_info.position.x - item.bounding_box.x;
4139                            let click_y = self.pointer_info.position.y - item.bounding_box.y;
4140                            // We can't check shift from here (no keyboard state);
4141                            // lib.rs will set shift via a dedicated method if needed.
4142                            self.pending_text_click = Some((top.id, click_x, click_y, false));
4143                        }
4144                        self.pressed_element_ids = self.pointer_over_ids.clone();
4145                    }
4146                } else {
4147                    // Check if any element in the pointer stack preserves focus
4148                    // (e.g. a toolbar button's child text element inherits the parent's preserve_focus)
4149                    let preserves = self.pointer_over_ids.iter().any(|eid| {
4150                        self.layout_element_map.get(&eid.id)
4151                            .map(|item| item.preserve_focus)
4152                            .unwrap_or(false)
4153                    });
4154
4155                    // Clear keyboard focus when the user clicks, unless the element preserves focus
4156                    if !preserves && self.focused_element_id != 0 {
4157                        self.change_focus(0);
4158                    }
4159
4160                    // Mark all hovered elements as pressed and fire on_press callbacks
4161                    self.pressed_element_ids = self.pointer_over_ids.clone();
4162                    for eid in self.pointer_over_ids.clone().iter() {
4163                        if let Some(item) = self.layout_element_map.get_mut(&eid.id) {
4164                            if let Some(ref mut callback) = item.on_press_fn {
4165                                callback(eid.clone(), self.pointer_info);
4166                            }
4167                        }
4168                    }
4169                }
4170            }
4171            PointerDataInteractionState::ReleasedThisFrame => {
4172                // Fire on_release for all elements that were in the pressed chain
4173                let pressed = std::mem::take(&mut self.pressed_element_ids);
4174                for eid in pressed.iter() {
4175                    if let Some(item) = self.layout_element_map.get_mut(&eid.id) {
4176                        if let Some(ref mut callback) = item.on_release_fn {
4177                            callback(eid.clone(), self.pointer_info);
4178                        }
4179                    }
4180                }
4181            }
4182            _ => {}
4183        }
4184    }
4185
4186    /// Physics constants for scroll momentum
4187    const SCROLL_DECEL: f32 = 5.0; // Exponential decay rate (reaches ~0.7% after 1s)
4188    const SCROLL_MIN_VELOCITY: f32 = 5.0; // px/s below which momentum stops
4189    const SCROLL_VELOCITY_SMOOTHING: f32 = 0.4; // EMA factor for velocity tracking
4190
4191    pub fn update_scroll_containers(
4192        &mut self,
4193        enable_drag_scrolling: bool,
4194        scroll_delta: Vector2,
4195        delta_time: f32,
4196    ) {
4197        let pointer = self.pointer_info.position;
4198        let dt = delta_time.max(0.0001); // Guard against zero/negative dt
4199
4200        // Remove containers that weren't open this frame, reset flag for next frame
4201        let mut i = 0;
4202        while i < self.scroll_container_datas.len() {
4203            if !self.scroll_container_datas[i].open_this_frame {
4204                self.scroll_container_datas.swap_remove(i);
4205                continue;
4206            }
4207            self.scroll_container_datas[i].open_this_frame = false;
4208            i += 1;
4209        }
4210
4211        // --- Drag scrolling ---
4212        if enable_drag_scrolling {
4213            let pointer_state = self.pointer_info.state;
4214
4215            match pointer_state {
4216                PointerDataInteractionState::PressedThisFrame => {
4217                    // Find the deepest scroll container under the pointer and start drag
4218                    let mut best: Option<usize> = None;
4219                    for si in 0..self.scroll_container_datas.len() {
4220                        let bb = self.scroll_container_datas[si].bounding_box;
4221                        if pointer.x >= bb.x
4222                            && pointer.x <= bb.x + bb.width
4223                            && pointer.y >= bb.y
4224                            && pointer.y <= bb.y + bb.height
4225                        {
4226                            best = Some(si);
4227                        }
4228                    }
4229                    if let Some(si) = best {
4230                        let scd = &mut self.scroll_container_datas[si];
4231                        scd.pointer_scroll_active = true;
4232                        scd.pointer_origin = pointer;
4233                        scd.scroll_origin = scd.scroll_position;
4234                        scd.scroll_momentum = Vector2::default();
4235                        scd.previous_delta = Vector2::default();
4236                    }
4237                }
4238                PointerDataInteractionState::Pressed => {
4239                    // Update drag: move scroll position to follow pointer
4240                    for si in 0..self.scroll_container_datas.len() {
4241                        let scd = &mut self.scroll_container_datas[si];
4242                        if !scd.pointer_scroll_active {
4243                            continue;
4244                        }
4245
4246                        let drag_delta = Vector2::new(
4247                            pointer.x - scd.pointer_origin.x,
4248                            pointer.y - scd.pointer_origin.y,
4249                        );
4250                        scd.scroll_position = Vector2::new(
4251                            scd.scroll_origin.x + drag_delta.x,
4252                            scd.scroll_origin.y + drag_delta.y,
4253                        );
4254
4255                        // Check if pointer actually moved this frame
4256                        let frame_delta = Vector2::new(
4257                            drag_delta.x - scd.previous_delta.x,
4258                            drag_delta.y - scd.previous_delta.y,
4259                        );
4260                        let moved = frame_delta.x.abs() > 0.5 || frame_delta.y.abs() > 0.5;
4261
4262                        if moved {
4263                            // Pointer moved — update velocity EMA and reset freshness timer
4264                            let instant_velocity = Vector2::new(
4265                                frame_delta.x / dt,
4266                                frame_delta.y / dt,
4267                            );
4268                            let s = Self::SCROLL_VELOCITY_SMOOTHING;
4269                            scd.scroll_momentum = Vector2::new(
4270                                scd.scroll_momentum.x * (1.0 - s) + instant_velocity.x * s,
4271                                scd.scroll_momentum.y * (1.0 - s) + instant_velocity.y * s,
4272                            );
4273                        }
4274                        scd.previous_delta = drag_delta;
4275                    }
4276                }
4277                PointerDataInteractionState::ReleasedThisFrame
4278                | PointerDataInteractionState::Released => {
4279                    for si in 0..self.scroll_container_datas.len() {
4280                        let scd = &mut self.scroll_container_datas[si];
4281                        if !scd.pointer_scroll_active {
4282                            continue;
4283                        }
4284                        scd.pointer_scroll_active = false;
4285                    }
4286                }
4287            }
4288        }
4289
4290        // --- Momentum scrolling (apply when not actively dragging) ---
4291        for si in 0..self.scroll_container_datas.len() {
4292            let scd = &mut self.scroll_container_datas[si];
4293            if scd.pointer_scroll_active {
4294                // Still dragging — skip momentum
4295            } else if scd.scroll_momentum.x.abs() > Self::SCROLL_MIN_VELOCITY
4296                || scd.scroll_momentum.y.abs() > Self::SCROLL_MIN_VELOCITY
4297            {
4298                // Apply momentum
4299                scd.scroll_position.x += scd.scroll_momentum.x * dt;
4300                scd.scroll_position.y += scd.scroll_momentum.y * dt;
4301
4302                // Exponential decay (frame-rate independent)
4303                let decay = (-Self::SCROLL_DECEL * dt).exp();
4304                scd.scroll_momentum.x *= decay;
4305                scd.scroll_momentum.y *= decay;
4306
4307                // Stop if below threshold
4308                if scd.scroll_momentum.x.abs() < Self::SCROLL_MIN_VELOCITY {
4309                    scd.scroll_momentum.x = 0.0;
4310                }
4311                if scd.scroll_momentum.y.abs() < Self::SCROLL_MIN_VELOCITY {
4312                    scd.scroll_momentum.y = 0.0;
4313                }
4314            }
4315        }
4316
4317        // --- Mouse wheel / external scroll delta ---
4318        if scroll_delta.x != 0.0 || scroll_delta.y != 0.0 {
4319            // Find the deepest (last in list) scroll container the pointer is inside
4320            let mut best: Option<usize> = None;
4321            for si in 0..self.scroll_container_datas.len() {
4322                let bb = self.scroll_container_datas[si].bounding_box;
4323                if pointer.x >= bb.x
4324                    && pointer.x <= bb.x + bb.width
4325                    && pointer.y >= bb.y
4326                    && pointer.y <= bb.y + bb.height
4327                {
4328                    best = Some(si);
4329                }
4330            }
4331            if let Some(si) = best {
4332                let scd = &mut self.scroll_container_datas[si];
4333                scd.scroll_position.y += scroll_delta.y;
4334                scd.scroll_position.x += scroll_delta.x;
4335                // Kill any active momentum when mouse wheel is used
4336                scd.scroll_momentum = Vector2::default();
4337            }
4338        }
4339
4340        // --- Clamp all scroll positions ---
4341        for si in 0..self.scroll_container_datas.len() {
4342            let scd = &mut self.scroll_container_datas[si];
4343            let max_scroll_y =
4344                -(scd.content_size.height - scd.bounding_box.height).max(0.0);
4345            let max_scroll_x =
4346                -(scd.content_size.width - scd.bounding_box.width).max(0.0);
4347            scd.scroll_position.y = scd.scroll_position.y.clamp(max_scroll_y, 0.0);
4348            scd.scroll_position.x = scd.scroll_position.x.clamp(max_scroll_x, 0.0);
4349
4350            // Also kill momentum at bounds
4351            if scd.scroll_position.y >= 0.0 || scd.scroll_position.y <= max_scroll_y {
4352                scd.scroll_momentum.y = 0.0;
4353            }
4354            if scd.scroll_position.x >= 0.0 || scd.scroll_position.x <= max_scroll_x {
4355                scd.scroll_momentum.x = 0.0;
4356            }
4357        }
4358    }
4359
4360    pub fn hovered(&self) -> bool {
4361        let open_idx = self.get_open_layout_element();
4362        let elem_id = self.layout_elements[open_idx].id;
4363        self.pointer_over_ids.iter().any(|eid| eid.id == elem_id)
4364    }
4365
4366    pub fn on_hover(&mut self, callback: Box<dyn FnMut(Id, PointerData)>) {
4367        let open_idx = self.get_open_layout_element();
4368        let elem_id = self.layout_elements[open_idx].id;
4369        if let Some(item) = self.layout_element_map.get_mut(&elem_id) {
4370            item.on_hover_fn = Some(callback);
4371        }
4372    }
4373
4374    pub fn pressed(&self) -> bool {
4375        let open_idx = self.get_open_layout_element();
4376        let elem_id = self.layout_elements[open_idx].id;
4377        self.pressed_element_ids.iter().any(|eid| eid.id == elem_id)
4378    }
4379
4380    pub fn set_press_callbacks(
4381        &mut self,
4382        on_press: Option<Box<dyn FnMut(Id, PointerData)>>,
4383        on_release: Option<Box<dyn FnMut(Id, PointerData)>>,
4384    ) {
4385        let open_idx = self.get_open_layout_element();
4386        let elem_id = self.layout_elements[open_idx].id;
4387        if let Some(item) = self.layout_element_map.get_mut(&elem_id) {
4388            item.on_press_fn = on_press;
4389            item.on_release_fn = on_release;
4390        }
4391    }
4392
4393    /// Returns true if the currently open element has focus.
4394    pub fn focused(&self) -> bool {
4395        let open_idx = self.get_open_layout_element();
4396        let elem_id = self.layout_elements[open_idx].id;
4397        self.focused_element_id == elem_id && elem_id != 0
4398    }
4399
4400    /// Returns the currently focused element's ID, or None.
4401    pub fn focused_element(&self) -> Option<Id> {
4402        if self.focused_element_id != 0 {
4403            self.layout_element_map
4404                .get(&self.focused_element_id)
4405                .map(|item| item.element_id.clone())
4406        } else {
4407            None
4408        }
4409    }
4410
4411    /// Sets focus to the element with the given ID, firing on_unfocus/on_focus callbacks.
4412    pub fn set_focus(&mut self, element_id: u32) {
4413        self.change_focus(element_id);
4414    }
4415
4416    /// Clears focus (no element is focused).
4417    pub fn clear_focus(&mut self) {
4418        self.change_focus(0);
4419    }
4420
4421    /// Internal: changes focus, firing on_unfocus on old and on_focus on new.
4422    pub(crate) fn change_focus(&mut self, new_id: u32) {
4423        let old_id = self.focused_element_id;
4424        if old_id == new_id {
4425            return;
4426        }
4427        self.focused_element_id = new_id;
4428        if new_id == 0 {
4429            self.focus_from_keyboard = false;
4430        }
4431
4432        // Fire on_unfocus on old element
4433        if old_id != 0 {
4434            if let Some(item) = self.layout_element_map.get_mut(&old_id) {
4435                let id_copy = item.element_id.clone();
4436                if let Some(ref mut callback) = item.on_unfocus_fn {
4437                    callback(id_copy);
4438                }
4439            }
4440        }
4441
4442        // Fire on_focus on new element
4443        if new_id != 0 {
4444            if let Some(item) = self.layout_element_map.get_mut(&new_id) {
4445                let id_copy = item.element_id.clone();
4446                if let Some(ref mut callback) = item.on_focus_fn {
4447                    callback(id_copy);
4448                }
4449            }
4450        }
4451    }
4452
4453    /// Fire the on_press callback for the element with the given u32 ID.
4454    /// Used by screen reader action handling.
4455    #[allow(dead_code)]
4456    pub(crate) fn fire_press(&mut self, element_id: u32) {
4457        if let Some(item) = self.layout_element_map.get_mut(&element_id) {
4458            let id_copy = item.element_id.clone();
4459            if let Some(ref mut callback) = item.on_press_fn {
4460                callback(id_copy, PointerData::default());
4461            }
4462        }
4463    }
4464
4465    pub fn set_focus_callbacks(
4466        &mut self,
4467        on_focus: Option<Box<dyn FnMut(Id)>>,
4468        on_unfocus: Option<Box<dyn FnMut(Id)>>,
4469    ) {
4470        let open_idx = self.get_open_layout_element();
4471        let elem_id = self.layout_elements[open_idx].id;
4472        if let Some(item) = self.layout_element_map.get_mut(&elem_id) {
4473            item.on_focus_fn = on_focus;
4474            item.on_unfocus_fn = on_unfocus;
4475        }
4476    }
4477
4478    /// Sets text input callbacks for the currently open element.
4479    pub fn set_text_input_callbacks(
4480        &mut self,
4481        on_changed: Option<Box<dyn FnMut(&str)>>,
4482        on_submit: Option<Box<dyn FnMut(&str)>>,
4483    ) {
4484        let open_idx = self.get_open_layout_element();
4485        let elem_id = self.layout_elements[open_idx].id;
4486        if let Some(item) = self.layout_element_map.get_mut(&elem_id) {
4487            item.on_text_changed_fn = on_changed;
4488            item.on_text_submit_fn = on_submit;
4489        }
4490    }
4491
4492    /// Returns true if the currently focused element is a text input.
4493    pub fn is_text_input_focused(&self) -> bool {
4494        if self.focused_element_id == 0 {
4495            return false;
4496        }
4497        self.text_edit_states.contains_key(&self.focused_element_id)
4498    }
4499
4500    /// Returns true if the currently focused text input is multiline.
4501    pub fn is_focused_text_input_multiline(&self) -> bool {
4502        if self.focused_element_id == 0 {
4503            return false;
4504        }
4505        self.text_input_element_ids.iter()
4506            .position(|&id| id == self.focused_element_id)
4507            .and_then(|idx| self.text_input_configs.get(idx))
4508            .map_or(false, |cfg| cfg.is_multiline)
4509    }
4510
4511    /// Returns the text value for a text input element, or empty string if not found.
4512    pub fn get_text_value(&self, element_id: u32) -> &str {
4513        self.text_edit_states
4514            .get(&element_id)
4515            .map(|state| state.text.as_str())
4516            .unwrap_or("")
4517    }
4518
4519    /// Sets the text value for a text input element.
4520    pub fn set_text_value(&mut self, element_id: u32, value: &str) {
4521        let state = self.text_edit_states
4522            .entry(element_id)
4523            .or_insert_with(crate::text_input::TextEditState::default);
4524        state.text = value.to_string();
4525        #[cfg(feature = "text-styling")]
4526        let max_pos = crate::text_input::styling::cursor_len(&state.text);
4527        #[cfg(not(feature = "text-styling"))]
4528        let max_pos = state.text.chars().count();
4529        if state.cursor_pos > max_pos {
4530            state.cursor_pos = max_pos;
4531        }
4532        state.selection_anchor = None;
4533        state.reset_blink();
4534    }
4535
4536    /// Returns the cursor position for a text input element, or 0 if not found.
4537    /// When text-styling is enabled, this returns the visual position.
4538    pub fn get_cursor_pos(&self, element_id: u32) -> usize {
4539        self.text_edit_states
4540            .get(&element_id)
4541            .map(|state| state.cursor_pos)
4542            .unwrap_or(0)
4543    }
4544
4545    /// Sets the cursor position for a text input element.
4546    /// When text-styling is enabled, `pos` is in visual space.
4547    /// Clamps to the text length and clears any selection.
4548    pub fn set_cursor_pos(&mut self, element_id: u32, pos: usize) {
4549        if let Some(state) = self.text_edit_states.get_mut(&element_id) {
4550            #[cfg(feature = "text-styling")]
4551            let max_pos = crate::text_input::styling::cursor_len(&state.text);
4552            #[cfg(not(feature = "text-styling"))]
4553            let max_pos = state.text.chars().count();
4554            state.cursor_pos = pos.min(max_pos);
4555            state.selection_anchor = None;
4556            state.reset_blink();
4557        }
4558    }
4559
4560    /// Returns the selection range (start, end) for a text input element, or None.
4561    /// When text-styling is enabled, these are visual positions.
4562    pub fn get_selection_range(&self, element_id: u32) -> Option<(usize, usize)> {
4563        self.text_edit_states
4564            .get(&element_id)
4565            .and_then(|state| state.selection_range())
4566    }
4567
4568    /// Sets the selection range for a text input element.
4569    /// `anchor` is where selection started, `cursor` is where it ends.
4570    /// When text-styling is enabled, these are visual positions.
4571    pub fn set_selection(&mut self, element_id: u32, anchor: usize, cursor: usize) {
4572        if let Some(state) = self.text_edit_states.get_mut(&element_id) {
4573            #[cfg(feature = "text-styling")]
4574            let max_pos = crate::text_input::styling::cursor_len(&state.text);
4575            #[cfg(not(feature = "text-styling"))]
4576            let max_pos = state.text.chars().count();
4577            state.selection_anchor = Some(anchor.min(max_pos));
4578            state.cursor_pos = cursor.min(max_pos);
4579            state.reset_blink();
4580        }
4581    }
4582
4583    /// Returns true if the given element ID is currently pressed.
4584    pub fn is_element_pressed(&self, element_id: u32) -> bool {
4585        self.pressed_element_ids.iter().any(|eid| eid.id == element_id)
4586    }
4587
4588    /// Process a character input event for the focused text input.
4589    /// Returns true if the character was consumed by a text input.
4590    pub fn process_text_input_char(&mut self, ch: char) -> bool {
4591        if !self.is_text_input_focused() {
4592            return false;
4593        }
4594        let elem_id = self.focused_element_id;
4595
4596        // Get max_length from current config (if available this frame)
4597        let max_length = self.text_input_element_ids.iter()
4598            .position(|&id| id == elem_id)
4599            .and_then(|idx| self.text_input_configs.get(idx))
4600            .and_then(|cfg| cfg.max_length);
4601
4602        if let Some(state) = self.text_edit_states.get_mut(&elem_id) {
4603            let old_text = state.text.clone();
4604            state.push_undo(crate::text_input::UndoActionKind::InsertChar);
4605            #[cfg(feature = "text-styling")]
4606            {
4607                state.insert_char_styled(ch, max_length);
4608            }
4609            #[cfg(not(feature = "text-styling"))]
4610            {
4611                state.insert_text(&ch.to_string(), max_length);
4612            }
4613            if state.text != old_text {
4614                let new_text = state.text.clone();
4615                // Fire on_changed callback
4616                if let Some(item) = self.layout_element_map.get_mut(&elem_id) {
4617                    if let Some(ref mut callback) = item.on_text_changed_fn {
4618                        callback(&new_text);
4619                    }
4620                }
4621            }
4622            true
4623        } else {
4624            false
4625        }
4626    }
4627
4628    /// Process a key event for the focused text input.
4629    /// `action` specifies which editing action to perform.
4630    /// Returns true if the key was consumed.
4631    pub fn process_text_input_action(&mut self, action: TextInputAction) -> bool {
4632        if !self.is_text_input_focused() {
4633            return false;
4634        }
4635        let elem_id = self.focused_element_id;
4636
4637        // Get config for the focused element
4638        let config_idx = self.text_input_element_ids.iter()
4639            .position(|&id| id == elem_id);
4640        let (max_length, is_multiline, font_asset, font_size) = config_idx
4641            .and_then(|idx| self.text_input_configs.get(idx))
4642            .map(|cfg| (cfg.max_length, cfg.is_multiline, cfg.font_asset, cfg.font_size))
4643            .unwrap_or((None, false, None, 16));
4644
4645        // For multiline visual navigation, compute visual lines
4646        let visual_lines_opt = if is_multiline {
4647            let visible_width = self.layout_element_map
4648                .get(&elem_id)
4649                .map(|item| item.bounding_box.width)
4650                .unwrap_or(0.0);
4651            if visible_width > 0.0 {
4652                if let Some(state) = self.text_edit_states.get(&elem_id) {
4653                    if let Some(ref measure_fn) = self.measure_text_fn {
4654                        Some(crate::text_input::wrap_lines(
4655                            &state.text,
4656                            visible_width,
4657                            font_asset,
4658                            font_size,
4659                            measure_fn.as_ref(),
4660                        ))
4661                    } else { None }
4662                } else { None }
4663            } else { None }
4664        } else { None };
4665
4666        if let Some(state) = self.text_edit_states.get_mut(&elem_id) {
4667            let old_text = state.text.clone();
4668
4669            // Push undo before text-modifying actions
4670            match &action {
4671                TextInputAction::Backspace => state.push_undo(crate::text_input::UndoActionKind::Backspace),
4672                TextInputAction::Delete => state.push_undo(crate::text_input::UndoActionKind::Delete),
4673                TextInputAction::BackspaceWord => state.push_undo(crate::text_input::UndoActionKind::DeleteWord),
4674                TextInputAction::DeleteWord => state.push_undo(crate::text_input::UndoActionKind::DeleteWord),
4675                TextInputAction::Cut => state.push_undo(crate::text_input::UndoActionKind::Cut),
4676                TextInputAction::Paste { .. } => state.push_undo(crate::text_input::UndoActionKind::Paste),
4677                TextInputAction::Submit if is_multiline => state.push_undo(crate::text_input::UndoActionKind::InsertChar),
4678                _ => {}
4679            }
4680
4681            match action {
4682                TextInputAction::MoveLeft { shift } => {
4683                    #[cfg(feature = "text-styling")]
4684                    { state.move_left_styled(shift); }
4685                    #[cfg(not(feature = "text-styling"))]
4686                    { state.move_left(shift); }
4687                }
4688                TextInputAction::MoveRight { shift } => {
4689                    #[cfg(feature = "text-styling")]
4690                    { state.move_right_styled(shift); }
4691                    #[cfg(not(feature = "text-styling"))]
4692                    { state.move_right(shift); }
4693                }
4694                TextInputAction::MoveWordLeft { shift } => {
4695                    #[cfg(feature = "text-styling")]
4696                    { state.move_word_left_styled(shift); }
4697                    #[cfg(not(feature = "text-styling"))]
4698                    { state.move_word_left(shift); }
4699                }
4700                TextInputAction::MoveWordRight { shift } => {
4701                    #[cfg(feature = "text-styling")]
4702                    { state.move_word_right_styled(shift); }
4703                    #[cfg(not(feature = "text-styling"))]
4704                    { state.move_word_right(shift); }
4705                }
4706                TextInputAction::MoveHome { shift } => {
4707                    // Multiline uses visual line navigation (raw positions)
4708                    #[cfg(not(feature = "text-styling"))]
4709                    {
4710                        if let Some(ref vl) = visual_lines_opt {
4711                            let new_pos = crate::text_input::visual_line_home(vl, state.cursor_pos);
4712                            if shift && state.selection_anchor.is_none() {
4713                                state.selection_anchor = Some(state.cursor_pos);
4714                            }
4715                            state.cursor_pos = new_pos;
4716                            if !shift { state.selection_anchor = None; }
4717                            else if state.selection_anchor == Some(state.cursor_pos) { state.selection_anchor = None; }
4718                            state.reset_blink();
4719                        } else {
4720                            state.move_home(shift);
4721                        }
4722                    }
4723                    #[cfg(feature = "text-styling")]
4724                    {
4725                        state.move_home_styled(shift);
4726                    }
4727                }
4728                TextInputAction::MoveEnd { shift } => {
4729                    #[cfg(not(feature = "text-styling"))]
4730                    {
4731                        if let Some(ref vl) = visual_lines_opt {
4732                            let new_pos = crate::text_input::visual_line_end(vl, state.cursor_pos);
4733                            if shift && state.selection_anchor.is_none() {
4734                                state.selection_anchor = Some(state.cursor_pos);
4735                            }
4736                            state.cursor_pos = new_pos;
4737                            if !shift { state.selection_anchor = None; }
4738                            else if state.selection_anchor == Some(state.cursor_pos) { state.selection_anchor = None; }
4739                            state.reset_blink();
4740                        } else {
4741                            state.move_end(shift);
4742                        }
4743                    }
4744                    #[cfg(feature = "text-styling")]
4745                    {
4746                        state.move_end_styled(shift);
4747                    }
4748                }
4749                TextInputAction::MoveUp { shift } => {
4750                    #[cfg(not(feature = "text-styling"))]
4751                    {
4752                        if let Some(ref vl) = visual_lines_opt {
4753                            let new_pos = crate::text_input::visual_move_up(vl, state.cursor_pos);
4754                            if shift && state.selection_anchor.is_none() {
4755                                state.selection_anchor = Some(state.cursor_pos);
4756                            }
4757                            state.cursor_pos = new_pos;
4758                            if !shift { state.selection_anchor = None; }
4759                            else if state.selection_anchor == Some(state.cursor_pos) { state.selection_anchor = None; }
4760                            state.reset_blink();
4761                        } else {
4762                            state.move_up(shift);
4763                        }
4764                    }
4765                    #[cfg(feature = "text-styling")]
4766                    {
4767                        state.move_up_styled(shift, visual_lines_opt.as_deref());
4768                    }
4769                }
4770                TextInputAction::MoveDown { shift } => {
4771                    #[cfg(not(feature = "text-styling"))]
4772                    {
4773                        if let Some(ref vl) = visual_lines_opt {
4774                            let text_len = state.text.chars().count();
4775                            let new_pos = crate::text_input::visual_move_down(vl, state.cursor_pos, text_len);
4776                            if shift && state.selection_anchor.is_none() {
4777                                state.selection_anchor = Some(state.cursor_pos);
4778                            }
4779                            state.cursor_pos = new_pos;
4780                            if !shift { state.selection_anchor = None; }
4781                            else if state.selection_anchor == Some(state.cursor_pos) { state.selection_anchor = None; }
4782                            state.reset_blink();
4783                        } else {
4784                            state.move_down(shift);
4785                        }
4786                    }
4787                    #[cfg(feature = "text-styling")]
4788                    {
4789                        state.move_down_styled(shift, visual_lines_opt.as_deref());
4790                    }
4791                }
4792                TextInputAction::Backspace => {
4793                    #[cfg(feature = "text-styling")]
4794                    { state.backspace_styled(); }
4795                    #[cfg(not(feature = "text-styling"))]
4796                    { state.backspace(); }
4797                }
4798                TextInputAction::Delete => {
4799                    #[cfg(feature = "text-styling")]
4800                    { state.delete_forward_styled(); }
4801                    #[cfg(not(feature = "text-styling"))]
4802                    { state.delete_forward(); }
4803                }
4804                TextInputAction::BackspaceWord => {
4805                    #[cfg(feature = "text-styling")]
4806                    { state.backspace_word_styled(); }
4807                    #[cfg(not(feature = "text-styling"))]
4808                    { state.backspace_word(); }
4809                }
4810                TextInputAction::DeleteWord => {
4811                    #[cfg(feature = "text-styling")]
4812                    { state.delete_word_forward_styled(); }
4813                    #[cfg(not(feature = "text-styling"))]
4814                    { state.delete_word_forward(); }
4815                }
4816                TextInputAction::SelectAll => {
4817                    #[cfg(feature = "text-styling")]
4818                    { state.select_all_styled(); }
4819                    #[cfg(not(feature = "text-styling"))]
4820                    { state.select_all(); }
4821                }
4822                TextInputAction::Copy => {
4823                    // Copying doesn't modify state; handled by lib.rs
4824                }
4825                TextInputAction::Cut => {
4826                    #[cfg(feature = "text-styling")]
4827                    { state.delete_selection_styled(); }
4828                    #[cfg(not(feature = "text-styling"))]
4829                    { state.delete_selection(); }
4830                }
4831                TextInputAction::Paste { text } => {
4832                    #[cfg(feature = "text-styling")]
4833                    {
4834                        let escaped = crate::text_input::styling::escape_str(&text);
4835                        state.insert_text_styled(&escaped, max_length);
4836                    }
4837                    #[cfg(not(feature = "text-styling"))]
4838                    {
4839                        state.insert_text(&text, max_length);
4840                    }
4841                }
4842                TextInputAction::Submit => {
4843                    if is_multiline {
4844                        #[cfg(feature = "text-styling")]
4845                        { state.insert_text_styled("\n", max_length); }
4846                        #[cfg(not(feature = "text-styling"))]
4847                        { state.insert_text("\n", max_length); }
4848                    } else {
4849                        let text = state.text.clone();
4850                        // Fire on_submit callback
4851                        if let Some(item) = self.layout_element_map.get_mut(&elem_id) {
4852                            if let Some(ref mut callback) = item.on_text_submit_fn {
4853                                callback(&text);
4854                            }
4855                        }
4856                        return true;
4857                    }
4858                }
4859                TextInputAction::Undo => {
4860                    state.undo();
4861                }
4862                TextInputAction::Redo => {
4863                    state.redo();
4864                }
4865            }
4866            if state.text != old_text {
4867                let new_text = state.text.clone();
4868                if let Some(item) = self.layout_element_map.get_mut(&elem_id) {
4869                    if let Some(ref mut callback) = item.on_text_changed_fn {
4870                        callback(&new_text);
4871                    }
4872                }
4873            }
4874            true
4875        } else {
4876            false
4877        }
4878    }
4879
4880    /// Update blink timers for all text input states.
4881    pub fn update_text_input_blink_timers(&mut self) {
4882        let dt = self.frame_delta_time as f64;
4883        for state in self.text_edit_states.values_mut() {
4884            state.cursor_blink_timer += dt;
4885        }
4886    }
4887
4888    /// Update scroll offsets for text inputs to ensure cursor visibility.
4889    pub fn update_text_input_scroll(&mut self) {
4890        let focused = self.focused_element_id;
4891        if focused == 0 {
4892            return;
4893        }
4894        // Get bounding box for the focused text input
4895        let (visible_width, visible_height) = self.layout_element_map
4896            .get(&focused)
4897            .map(|item| (item.bounding_box.width, item.bounding_box.height))
4898            .unwrap_or((0.0, 0.0));
4899        if visible_width <= 0.0 {
4900            return;
4901        }
4902
4903        // Get cursor x-position
4904        if let Some(state) = self.text_edit_states.get(&focused) {
4905            let config_idx = self.text_input_element_ids.iter()
4906                .position(|&id| id == focused);
4907            if let Some(idx) = config_idx {
4908                if let Some(cfg) = self.text_input_configs.get(idx) {
4909                    if let Some(ref measure_fn) = self.measure_text_fn {
4910                        let disp_text = crate::text_input::display_text(
4911                            &state.text,
4912                            &cfg.placeholder,
4913                            cfg.is_password && !cfg.is_multiline,
4914                        );
4915                        if !state.text.is_empty() {
4916                            if cfg.is_multiline {
4917                                // Multiline: use visual lines with word wrapping
4918                                let visual_lines = crate::text_input::wrap_lines(
4919                                    &disp_text,
4920                                    visible_width,
4921                                    cfg.font_asset,
4922                                    cfg.font_size,
4923                                    measure_fn.as_ref(),
4924                                );
4925                                #[cfg(feature = "text-styling")]
4926                                let raw_cursor = state.cursor_pos_raw();
4927                                #[cfg(not(feature = "text-styling"))]
4928                                let raw_cursor = state.cursor_pos;
4929                                let (cursor_line, cursor_col) = crate::text_input::cursor_to_visual_pos(&visual_lines, raw_cursor);
4930                                let vl_text = visual_lines.get(cursor_line).map(|vl| vl.text.as_str()).unwrap_or("");
4931                                let line_positions = crate::text_input::compute_char_x_positions(
4932                                    vl_text,
4933                                    cfg.font_asset,
4934                                    cfg.font_size,
4935                                    measure_fn.as_ref(),
4936                                );
4937                                let cursor_x = line_positions.get(cursor_col).copied().unwrap_or(0.0);
4938                                let cfg_font_asset = cfg.font_asset;
4939                                let cfg_font_size = cfg.font_size;
4940                                let cfg_line_height_val = cfg.line_height;
4941                                let natural_height = self.font_height(cfg_font_asset, cfg_font_size);
4942                                let line_height = if cfg_line_height_val > 0 { cfg_line_height_val as f32 } else { natural_height };
4943                                if let Some(state_mut) = self.text_edit_states.get_mut(&focused) {
4944                                    state_mut.ensure_cursor_visible(cursor_x, visible_width);
4945                                    state_mut.ensure_cursor_visible_vertical(cursor_line, line_height, visible_height);
4946                                }
4947                            } else {
4948                                let char_x_positions = crate::text_input::compute_char_x_positions(
4949                                    &disp_text,
4950                                    cfg.font_asset,
4951                                    cfg.font_size,
4952                                    measure_fn.as_ref(),
4953                                );
4954                                #[cfg(feature = "text-styling")]
4955                                let raw_cursor = state.cursor_pos_raw();
4956                                #[cfg(not(feature = "text-styling"))]
4957                                let raw_cursor = state.cursor_pos;
4958                                let cursor_x = char_x_positions
4959                                    .get(raw_cursor)
4960                                    .copied()
4961                                    .unwrap_or(0.0);
4962                                if let Some(state_mut) = self.text_edit_states.get_mut(&focused) {
4963                                    state_mut.ensure_cursor_visible(cursor_x, visible_width);
4964                                }
4965                            }
4966                        } else if let Some(state_mut) = self.text_edit_states.get_mut(&focused) {
4967                            state_mut.scroll_offset = 0.0;
4968                            state_mut.scroll_offset_y = 0.0;
4969                        }
4970                    }
4971                }
4972            }
4973        }
4974    }
4975
4976    /// Handle pointer-based scrolling for text inputs: scroll wheel and drag-to-scroll.
4977    /// Mobile-first: dragging scrolls the content rather than selecting text.
4978    /// `scroll_delta` contains (x, y) scroll wheel deltas. For single-line, both axes
4979    /// map to horizontal scroll. For multiline, y scrolls vertically.
4980    pub fn update_text_input_pointer_scroll(&mut self, scroll_delta: Vector2) -> bool {
4981        let mut consumed_scroll = false;
4982
4983        let focused = self.focused_element_id;
4984
4985        // --- Scroll wheel: scroll any hovered text input (even if unfocused) ---
4986        let has_scroll = scroll_delta.x.abs() > 0.01 || scroll_delta.y.abs() > 0.01;
4987        if has_scroll {
4988            let p = self.pointer_info.position;
4989            // Find the text input under the pointer
4990            let hovered_ti = self.text_input_element_ids.iter().enumerate().find(|&(_, &id)| {
4991                self.layout_element_map.get(&id)
4992                    .map(|item| {
4993                        let bb = item.bounding_box;
4994                        p.x >= bb.x && p.x <= bb.x + bb.width
4995                            && p.y >= bb.y && p.y <= bb.y + bb.height
4996                    })
4997                    .unwrap_or(false)
4998            });
4999            if let Some((idx, &elem_id)) = hovered_ti {
5000                let is_multiline = self.text_input_configs.get(idx)
5001                    .map(|cfg| cfg.is_multiline)
5002                    .unwrap_or(false);
5003                if let Some(state) = self.text_edit_states.get_mut(&elem_id) {
5004                    if is_multiline {
5005                        if scroll_delta.y.abs() > 0.01 {
5006                            state.scroll_offset_y -= scroll_delta.y;
5007                            if state.scroll_offset_y < 0.0 {
5008                                state.scroll_offset_y = 0.0;
5009                            }
5010                        }
5011                    } else {
5012                        let h_delta = if scroll_delta.x.abs() > scroll_delta.y.abs() {
5013                            scroll_delta.x
5014                        } else {
5015                            scroll_delta.y
5016                        };
5017                        if h_delta.abs() > 0.01 {
5018                            state.scroll_offset -= h_delta;
5019                            if state.scroll_offset < 0.0 {
5020                                state.scroll_offset = 0.0;
5021                            }
5022                        }
5023                    }
5024                    consumed_scroll = true;
5025                }
5026            }
5027        }
5028
5029        // --- Drag scrolling (focused text input only) ---
5030        if focused == 0 {
5031            if self.text_input_drag_active {
5032                let pointer_state = self.pointer_info.state;
5033                if matches!(pointer_state, PointerDataInteractionState::ReleasedThisFrame | PointerDataInteractionState::Released) {
5034                    self.text_input_drag_active = false;
5035                }
5036            }
5037            return consumed_scroll;
5038        }
5039
5040        let ti_info = self.text_input_element_ids.iter()
5041            .position(|&id| id == focused)
5042            .and_then(|idx| self.text_input_configs.get(idx).map(|cfg| cfg.is_multiline));
5043        let is_text_input = ti_info.is_some();
5044        let is_multiline = ti_info.unwrap_or(false);
5045
5046        if !is_text_input {
5047            if self.text_input_drag_active {
5048                let pointer_state = self.pointer_info.state;
5049                if matches!(pointer_state, PointerDataInteractionState::ReleasedThisFrame | PointerDataInteractionState::Released) {
5050                    self.text_input_drag_active = false;
5051                }
5052            }
5053            return consumed_scroll;
5054        }
5055
5056        let pointer_over_focused = self.layout_element_map.get(&focused)
5057            .map(|item| {
5058                let bb = item.bounding_box;
5059                let p = self.pointer_info.position;
5060                p.x >= bb.x && p.x <= bb.x + bb.width
5061                    && p.y >= bb.y && p.y <= bb.y + bb.height
5062            })
5063            .unwrap_or(false);
5064
5065        let pointer = self.pointer_info.position;
5066        let pointer_state = self.pointer_info.state;
5067
5068        match pointer_state {
5069            PointerDataInteractionState::PressedThisFrame => {
5070                if pointer_over_focused {
5071                    let (scroll_x, scroll_y) = self.text_edit_states.get(&focused)
5072                        .map(|s| (s.scroll_offset, s.scroll_offset_y))
5073                        .unwrap_or((0.0, 0.0));
5074                    self.text_input_drag_active = true;
5075                    self.text_input_drag_origin = pointer;
5076                    self.text_input_drag_scroll_origin = Vector2::new(scroll_x, scroll_y);
5077                    self.text_input_drag_element_id = focused;
5078                }
5079            }
5080            PointerDataInteractionState::Pressed => {
5081                if self.text_input_drag_active {
5082                    if let Some(state) = self.text_edit_states.get_mut(&self.text_input_drag_element_id) {
5083                        if is_multiline {
5084                            let drag_delta_y = self.text_input_drag_origin.y - pointer.y;
5085                            state.scroll_offset_y = (self.text_input_drag_scroll_origin.y + drag_delta_y).max(0.0);
5086                        } else {
5087                            let drag_delta_x = self.text_input_drag_origin.x - pointer.x;
5088                            state.scroll_offset = (self.text_input_drag_scroll_origin.x + drag_delta_x).max(0.0);
5089                        }
5090                    }
5091                }
5092            }
5093            PointerDataInteractionState::ReleasedThisFrame
5094            | PointerDataInteractionState::Released => {
5095                self.text_input_drag_active = false;
5096            }
5097        }
5098        consumed_scroll
5099    }
5100
5101    /// Clamp text input scroll offsets to valid ranges.
5102    /// For multiline: clamp scroll_offset_y to [0, total_height - visible_height].
5103    /// For single-line: clamp scroll_offset to [0, total_width - visible_width].
5104    pub fn clamp_text_input_scroll(&mut self) {
5105        for i in 0..self.text_input_element_ids.len() {
5106            let elem_id = self.text_input_element_ids[i];
5107            let cfg = match self.text_input_configs.get(i) {
5108                Some(c) => c,
5109                None => continue,
5110            };
5111
5112            let font_asset = cfg.font_asset;
5113            let font_size = cfg.font_size;
5114            let cfg_line_height = cfg.line_height;
5115            let is_multiline = cfg.is_multiline;
5116            let is_password = cfg.is_password;
5117
5118            let (visible_width, visible_height) = self.layout_element_map.get(&elem_id)
5119                .map(|item| (item.bounding_box.width, item.bounding_box.height))
5120                .unwrap_or((200.0, 0.0));
5121
5122            let text_empty = self.text_edit_states.get(&elem_id)
5123                .map(|s| s.text.is_empty())
5124                .unwrap_or(true);
5125
5126            if text_empty {
5127                if let Some(state_mut) = self.text_edit_states.get_mut(&elem_id) {
5128                    state_mut.scroll_offset = 0.0;
5129                    state_mut.scroll_offset_y = 0.0;
5130                }
5131                continue;
5132            }
5133
5134            if let Some(ref measure_fn) = self.measure_text_fn {
5135                let disp_text = self.text_edit_states.get(&elem_id)
5136                    .map(|s| crate::text_input::display_text(&s.text, "", is_password && !is_multiline))
5137                    .unwrap_or_default();
5138
5139                if is_multiline {
5140                    let visual_lines = crate::text_input::wrap_lines(
5141                        &disp_text,
5142                        visible_width,
5143                        font_asset,
5144                        font_size,
5145                        measure_fn.as_ref(),
5146                    );
5147                    let natural_height = self.font_height(font_asset, font_size);
5148                    let font_height = if cfg_line_height > 0 { cfg_line_height as f32 } else { natural_height };
5149                    let total_height = visual_lines.len() as f32 * font_height;
5150                    let max_scroll = (total_height - visible_height).max(0.0);
5151                    if let Some(state_mut) = self.text_edit_states.get_mut(&elem_id) {
5152                        if state_mut.scroll_offset_y > max_scroll {
5153                            state_mut.scroll_offset_y = max_scroll;
5154                        }
5155                    }
5156                } else {
5157                    // Single-line: clamp horizontal scroll
5158                    let char_x_positions = crate::text_input::compute_char_x_positions(
5159                        &disp_text,
5160                        font_asset,
5161                        font_size,
5162                        measure_fn.as_ref(),
5163                    );
5164                    let total_width = char_x_positions.last().copied().unwrap_or(0.0);
5165                    let max_scroll = (total_width - visible_width).max(0.0);
5166                    if let Some(state_mut) = self.text_edit_states.get_mut(&elem_id) {
5167                        if state_mut.scroll_offset > max_scroll {
5168                            state_mut.scroll_offset = max_scroll;
5169                        }
5170                    }
5171                }
5172            }
5173        }
5174    }
5175
5176    /// Cycle focus to the next (or previous, if `reverse` is true) focusable element.
5177    /// This is called when Tab (or Shift+Tab) is pressed.
5178    pub fn cycle_focus(&mut self, reverse: bool) {
5179        if self.focusable_elements.is_empty() {
5180            return;
5181        }
5182        self.focus_from_keyboard = true;
5183
5184        // Sort: explicit tab_index first (ascending), then insertion order
5185        let mut sorted: Vec<FocusableEntry> = self.focusable_elements.clone();
5186        sorted.sort_by(|a, b| {
5187            match (a.tab_index, b.tab_index) {
5188                (Some(ai), Some(bi)) => ai.cmp(&bi).then(a.insertion_order.cmp(&b.insertion_order)),
5189                (Some(_), None) => std::cmp::Ordering::Less,
5190                (None, Some(_)) => std::cmp::Ordering::Greater,
5191                (None, None) => a.insertion_order.cmp(&b.insertion_order),
5192            }
5193        });
5194
5195        // Find current focus position
5196        let current_pos = sorted
5197            .iter()
5198            .position(|e| e.element_id == self.focused_element_id);
5199
5200        let next_pos = match current_pos {
5201            Some(pos) => {
5202                if reverse {
5203                    if pos == 0 { sorted.len() - 1 } else { pos - 1 }
5204                } else {
5205                    if pos + 1 >= sorted.len() { 0 } else { pos + 1 }
5206                }
5207            }
5208            None => {
5209                // No current focus — go to first (or last if reverse)
5210                if reverse { sorted.len() - 1 } else { 0 }
5211            }
5212        };
5213
5214        self.change_focus(sorted[next_pos].element_id);
5215    }
5216
5217    /// Move focus based on arrow key direction, using `focus_left/right/up/down` overrides.
5218    pub fn arrow_focus(&mut self, direction: ArrowDirection) {
5219        if self.focused_element_id == 0 {
5220            return;
5221        }
5222        self.focus_from_keyboard = true;
5223        if let Some(config) = self.accessibility_configs.get(&self.focused_element_id) {
5224            let target = match direction {
5225                ArrowDirection::Left => config.focus_left,
5226                ArrowDirection::Right => config.focus_right,
5227                ArrowDirection::Up => config.focus_up,
5228                ArrowDirection::Down => config.focus_down,
5229            };
5230            if let Some(target_id) = target {
5231                self.change_focus(target_id);
5232            }
5233        }
5234    }
5235
5236    /// Handle keyboard activation (Enter/Space) on the focused element.
5237    pub fn handle_keyboard_activation(&mut self, pressed: bool, released: bool) {
5238        if self.focused_element_id == 0 {
5239            return;
5240        }
5241        if pressed {
5242            let id_copy = self
5243                .layout_element_map
5244                .get(&self.focused_element_id)
5245                .map(|item| item.element_id.clone());
5246            if let Some(id) = id_copy {
5247                self.pressed_element_ids = vec![id.clone()];
5248                if let Some(item) = self.layout_element_map.get_mut(&self.focused_element_id) {
5249                    if let Some(ref mut callback) = item.on_press_fn {
5250                        callback(id, PointerData::default());
5251                    }
5252                }
5253            }
5254        }
5255        if released {
5256            let pressed = std::mem::take(&mut self.pressed_element_ids);
5257            for eid in pressed.iter() {
5258                if let Some(item) = self.layout_element_map.get_mut(&eid.id) {
5259                    if let Some(ref mut callback) = item.on_release_fn {
5260                        callback(eid.clone(), PointerData::default());
5261                    }
5262                }
5263            }
5264        }
5265    }
5266
5267    pub fn pointer_over(&self, element_id: Id) -> bool {
5268        self.pointer_over_ids.iter().any(|eid| eid.id == element_id.id)
5269    }
5270
5271    pub fn get_pointer_over_ids(&self) -> &[Id] {
5272        &self.pointer_over_ids
5273    }
5274
5275    pub fn get_element_data(&self, id: Id) -> Option<BoundingBox> {
5276        self.layout_element_map
5277            .get(&id.id)
5278            .map(|item| item.bounding_box)
5279    }
5280
5281    pub fn get_scroll_container_data(&self, id: Id) -> ScrollContainerData {
5282        for scd in &self.scroll_container_datas {
5283            if scd.element_id == id.id {
5284                return ScrollContainerData {
5285                    scroll_position: scd.scroll_position,
5286                    scroll_container_dimensions: Dimensions::new(
5287                        scd.bounding_box.width,
5288                        scd.bounding_box.height,
5289                    ),
5290                    content_dimensions: scd.content_size,
5291                    horizontal: false,
5292                    vertical: false,
5293                    found: true,
5294                };
5295            }
5296        }
5297        ScrollContainerData::default()
5298    }
5299
5300    pub fn get_scroll_offset(&self) -> Vector2 {
5301        let open_idx = self.get_open_layout_element();
5302        let elem_id = self.layout_elements[open_idx].id;
5303        for scd in &self.scroll_container_datas {
5304            if scd.element_id == elem_id {
5305                return scd.scroll_position;
5306            }
5307        }
5308        Vector2::default()
5309    }
5310
5311    const DEBUG_VIEW_WIDTH: f32 = 400.0;
5312    const DEBUG_VIEW_ROW_HEIGHT: f32 = 30.0;
5313    const DEBUG_VIEW_OUTER_PADDING: u16 = 10;
5314    const DEBUG_VIEW_INDENT_WIDTH: u16 = 16;
5315
5316    const DEBUG_COLOR_1: Color = Color::rgba(58.0, 56.0, 52.0, 255.0);
5317    const DEBUG_COLOR_2: Color = Color::rgba(62.0, 60.0, 58.0, 255.0);
5318    const DEBUG_COLOR_3: Color = Color::rgba(141.0, 133.0, 135.0, 255.0);
5319    const DEBUG_COLOR_4: Color = Color::rgba(238.0, 226.0, 231.0, 255.0);
5320    #[allow(dead_code)]
5321    const DEBUG_COLOR_SELECTED_ROW: Color = Color::rgba(102.0, 80.0, 78.0, 255.0);
5322    const DEBUG_HIGHLIGHT_COLOR: Color = Color::rgba(168.0, 66.0, 28.0, 100.0);
5323
5324    /// Escape text-styling special characters (`{`, `}`, `|`, `\`) so that
5325    /// debug view strings are never interpreted as styling markup.
5326    #[cfg(feature = "text-styling")]
5327    fn debug_escape_str(s: &str) -> String {
5328        let mut result = String::with_capacity(s.len());
5329        for c in s.chars() {
5330            match c {
5331                '{' | '}' | '|' | '\\' => {
5332                    result.push('\\');
5333                    result.push(c);
5334                }
5335                _ => result.push(c),
5336            }
5337        }
5338        result
5339    }
5340
5341    /// Helper: emit a text element with a static string.
5342    /// When `text-styling` is enabled the string is escaped first so that
5343    /// braces and pipes are rendered literally.
5344    fn debug_text(&mut self, text: &'static str, config_index: usize) {
5345        #[cfg(feature = "text-styling")]
5346        {
5347            let escaped = Self::debug_escape_str(text);
5348            self.open_text_element(&escaped, config_index);
5349        }
5350        #[cfg(not(feature = "text-styling"))]
5351        {
5352            self.open_text_element(text, config_index);
5353        }
5354    }
5355
5356    /// Helper: emit a text element from a string (e.g. element IDs
5357    /// or text previews). Escapes text-styling characters when that feature is
5358    /// active.
5359    fn debug_raw_text(&mut self, text: &str, config_index: usize) {
5360        #[cfg(feature = "text-styling")]
5361        {
5362            let escaped = Self::debug_escape_str(text);
5363            self.open_text_element(&escaped, config_index);
5364        }
5365        #[cfg(not(feature = "text-styling"))]
5366        {
5367            self.open_text_element(text, config_index);
5368        }
5369    }
5370
5371    /// Helper: format a number as a string and emit a text element.
5372    fn debug_int_text(&mut self, value: f32, config_index: usize) {
5373        let s = format!("{}", value as i32);
5374        self.open_text_element(&s, config_index);
5375    }
5376
5377    /// Helper: format a float with 2 decimal places and emit a text element.
5378    fn debug_float_text(&mut self, value: f32, config_index: usize) {
5379        let s = format!("{:.2}", value);
5380        self.open_text_element(&s, config_index);
5381    }
5382
5383    /// Helper: open an element, configure, return nothing. Caller must close_element().
5384    fn debug_open(&mut self, decl: &ElementDeclaration<CustomElementData>) {
5385        self.open_element();
5386        self.configure_open_element(decl);
5387    }
5388
5389    /// Helper: open a named element, configure. Caller must close_element().
5390    fn debug_open_id(&mut self, name: &str, decl: &ElementDeclaration<CustomElementData>) {
5391        self.open_element_with_id(&hash_string(name, 0));
5392        self.configure_open_element(decl);
5393    }
5394
5395    /// Helper: open a named+indexed element, configure. Caller must close_element().
5396    fn debug_open_idi(&mut self, name: &str, offset: u32, decl: &ElementDeclaration<CustomElementData>) {
5397        self.open_element_with_id(&hash_string_with_offset(name, offset, 0));
5398        self.configure_open_element(decl);
5399    }
5400
5401    fn debug_get_config_type_label(config_type: ElementConfigType) -> (&'static str, Color) {
5402        match config_type {
5403            ElementConfigType::Shared => ("Shared", Color::rgba(243.0, 134.0, 48.0, 255.0)),
5404            ElementConfigType::Text => ("Text", Color::rgba(105.0, 210.0, 231.0, 255.0)),
5405            ElementConfigType::Aspect => ("Aspect", Color::rgba(101.0, 149.0, 194.0, 255.0)),
5406            ElementConfigType::Image => ("Image", Color::rgba(121.0, 189.0, 154.0, 255.0)),
5407            ElementConfigType::Floating => ("Floating", Color::rgba(250.0, 105.0, 0.0, 255.0)),
5408            ElementConfigType::Clip => ("Overflow", Color::rgba(242.0, 196.0, 90.0, 255.0)),
5409            ElementConfigType::Border => ("Border", Color::rgba(108.0, 91.0, 123.0, 255.0)),
5410            ElementConfigType::Custom => ("Custom", Color::rgba(11.0, 72.0, 107.0, 255.0)),
5411            ElementConfigType::TextInput => ("TextInput", Color::rgba(52.0, 152.0, 219.0, 255.0)),
5412        }
5413    }
5414
5415    /// Render the debug view sizing info for one axis.
5416    fn render_debug_layout_sizing(&mut self, sizing: SizingAxis, config_index: usize) {
5417        let label = match sizing.type_ {
5418            SizingType::Fit => "FIT",
5419            SizingType::Grow => "GROW",
5420            SizingType::Percent => "PERCENT",
5421            SizingType::Fixed => "FIXED",
5422            // Default handled by Grow arm above
5423        };
5424        self.debug_text(label, config_index);
5425        if matches!(sizing.type_, SizingType::Grow | SizingType::Fit | SizingType::Fixed) {
5426            self.debug_text("(", config_index);
5427            if sizing.min_max.min != 0.0 {
5428                self.debug_text("min: ", config_index);
5429                self.debug_int_text(sizing.min_max.min, config_index);
5430                if sizing.min_max.max != MAXFLOAT {
5431                    self.debug_text(", ", config_index);
5432                }
5433            }
5434            if sizing.min_max.max != MAXFLOAT {
5435                self.debug_text("max: ", config_index);
5436                self.debug_int_text(sizing.min_max.max, config_index);
5437            }
5438            self.debug_text(")", config_index);
5439        } else if sizing.type_ == SizingType::Percent {
5440            self.debug_text("(", config_index);
5441            self.debug_int_text(sizing.percent * 100.0, config_index);
5442            self.debug_text("%)", config_index);
5443        }
5444    }
5445
5446    /// Render a config type header in the selected element detail panel.
5447    fn render_debug_view_element_config_header(
5448        &mut self,
5449        element_id_string: StringId,
5450        config_type: ElementConfigType,
5451        _info_title_config: usize,
5452    ) {
5453        let (label, label_color) = Self::debug_get_config_type_label(config_type);
5454        self.render_debug_view_category_header(label, label_color, element_id_string);
5455    }
5456
5457    /// Render a category header badge with arbitrary label and color.
5458    fn render_debug_view_category_header(
5459        &mut self,
5460        label: &str,
5461        label_color: Color,
5462        element_id_string: StringId,
5463    ) {
5464        let bg = Color::rgba(label_color.r, label_color.g, label_color.b, 90.0);
5465        self.debug_open(&ElementDeclaration {
5466            layout: LayoutConfig {
5467                sizing: SizingConfig {
5468                    width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
5469                    ..Default::default()
5470                },
5471                padding: PaddingConfig {
5472                    left: Self::DEBUG_VIEW_OUTER_PADDING,
5473                    right: Self::DEBUG_VIEW_OUTER_PADDING,
5474                    top: Self::DEBUG_VIEW_OUTER_PADDING,
5475                    bottom: Self::DEBUG_VIEW_OUTER_PADDING,
5476                },
5477                child_alignment: ChildAlignmentConfig { x: AlignX::Left, y: AlignY::CenterY },
5478                ..Default::default()
5479            },
5480            ..Default::default()
5481        });
5482        {
5483            // Badge
5484            self.debug_open(&ElementDeclaration {
5485                layout: LayoutConfig {
5486                    padding: PaddingConfig { left: 8, right: 8, top: 2, bottom: 2 },
5487                    ..Default::default()
5488                },
5489                background_color: bg,
5490                corner_radius: CornerRadius { top_left: 4.0, top_right: 4.0, bottom_left: 4.0, bottom_right: 4.0 },
5491                border: BorderConfig {
5492                    color: label_color,
5493                    width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
5494                },
5495                ..Default::default()
5496            });
5497            {
5498                let tc = self.store_text_element_config(TextConfig {
5499                    color: Self::DEBUG_COLOR_4,
5500                    font_size: 16,
5501                    ..Default::default()
5502                });
5503                self.debug_raw_text(label, tc);
5504            }
5505            self.close_element();
5506            // Spacer
5507            self.debug_open(&ElementDeclaration {
5508                layout: LayoutConfig {
5509                    sizing: SizingConfig {
5510                        width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
5511                        ..Default::default()
5512                    },
5513                    ..Default::default()
5514                },
5515                ..Default::default()
5516            });
5517            self.close_element();
5518            // Element ID string
5519            let tc = self.store_text_element_config(TextConfig {
5520                color: Self::DEBUG_COLOR_3,
5521                font_size: 16,
5522                wrap_mode: WrapMode::None,
5523                ..Default::default()
5524            });
5525            if !element_id_string.is_empty() {
5526                self.debug_raw_text(element_id_string.as_str(), tc);
5527            }
5528        }
5529        self.close_element();
5530    }
5531
5532    /// Render a color value in the debug view.
5533    fn render_debug_view_color(&mut self, color: Color, config_index: usize) {
5534        self.debug_open(&ElementDeclaration {
5535            layout: LayoutConfig {
5536                child_alignment: ChildAlignmentConfig { x: AlignX::Left, y: AlignY::CenterY },
5537                ..Default::default()
5538            },
5539            ..Default::default()
5540        });
5541        {
5542            self.debug_text("{ r: ", config_index);
5543            self.debug_int_text(color.r, config_index);
5544            self.debug_text(", g: ", config_index);
5545            self.debug_int_text(color.g, config_index);
5546            self.debug_text(", b: ", config_index);
5547            self.debug_int_text(color.b, config_index);
5548            self.debug_text(", a: ", config_index);
5549            self.debug_int_text(color.a, config_index);
5550            self.debug_text(" }", config_index);
5551            // Spacer
5552            self.debug_open(&ElementDeclaration {
5553                layout: LayoutConfig {
5554                    sizing: SizingConfig {
5555                        width: SizingAxis {
5556                            type_: SizingType::Fixed,
5557                            min_max: SizingMinMax { min: 10.0, max: 10.0 },
5558                            ..Default::default()
5559                        },
5560                        ..Default::default()
5561                    },
5562                    ..Default::default()
5563                },
5564                ..Default::default()
5565            });
5566            self.close_element();
5567            // Color swatch
5568            let swatch_size = Self::DEBUG_VIEW_ROW_HEIGHT - 8.0;
5569            self.debug_open(&ElementDeclaration {
5570                layout: LayoutConfig {
5571                    sizing: SizingConfig {
5572                        width: SizingAxis {
5573                            type_: SizingType::Fixed,
5574                            min_max: SizingMinMax { min: swatch_size, max: swatch_size },
5575                            ..Default::default()
5576                        },
5577                        height: SizingAxis {
5578                            type_: SizingType::Fixed,
5579                            min_max: SizingMinMax { min: swatch_size, max: swatch_size },
5580                            ..Default::default()
5581                        },
5582                    },
5583                    ..Default::default()
5584                },
5585                background_color: color,
5586                corner_radius: CornerRadius { top_left: 4.0, top_right: 4.0, bottom_left: 4.0, bottom_right: 4.0 },
5587                border: BorderConfig {
5588                    color: Self::DEBUG_COLOR_4,
5589                    width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
5590                },
5591                ..Default::default()
5592            });
5593            self.close_element();
5594        }
5595        self.close_element();
5596    }
5597
5598    /// Render a corner radius value in the debug view.
5599    fn render_debug_view_corner_radius(&mut self, cr: CornerRadius, config_index: usize) {
5600        self.debug_open(&ElementDeclaration {
5601            layout: LayoutConfig {
5602                child_alignment: ChildAlignmentConfig { x: AlignX::Left, y: AlignY::CenterY },
5603                ..Default::default()
5604            },
5605            ..Default::default()
5606        });
5607        {
5608            self.debug_text("{ topLeft: ", config_index);
5609            self.debug_int_text(cr.top_left, config_index);
5610            self.debug_text(", topRight: ", config_index);
5611            self.debug_int_text(cr.top_right, config_index);
5612            self.debug_text(", bottomLeft: ", config_index);
5613            self.debug_int_text(cr.bottom_left, config_index);
5614            self.debug_text(", bottomRight: ", config_index);
5615            self.debug_int_text(cr.bottom_right, config_index);
5616            self.debug_text(" }", config_index);
5617        }
5618        self.close_element();
5619    }
5620
5621    /// Render a shader uniform value in the debug view.
5622    fn render_debug_shader_uniform_value(&mut self, value: &crate::shaders::ShaderUniformValue, config_index: usize) {
5623        use crate::shaders::ShaderUniformValue;
5624        match value {
5625            ShaderUniformValue::Float(v) => {
5626                self.debug_float_text(*v, config_index);
5627            }
5628            ShaderUniformValue::Vec2(v) => {
5629                self.debug_text("(", config_index);
5630                self.debug_float_text(v[0], config_index);
5631                self.debug_text(", ", config_index);
5632                self.debug_float_text(v[1], config_index);
5633                self.debug_text(")", config_index);
5634            }
5635            ShaderUniformValue::Vec3(v) => {
5636                self.debug_text("(", config_index);
5637                self.debug_float_text(v[0], config_index);
5638                self.debug_text(", ", config_index);
5639                self.debug_float_text(v[1], config_index);
5640                self.debug_text(", ", config_index);
5641                self.debug_float_text(v[2], config_index);
5642                self.debug_text(")", config_index);
5643            }
5644            ShaderUniformValue::Vec4(v) => {
5645                self.debug_text("(", config_index);
5646                self.debug_float_text(v[0], config_index);
5647                self.debug_text(", ", config_index);
5648                self.debug_float_text(v[1], config_index);
5649                self.debug_text(", ", config_index);
5650                self.debug_float_text(v[2], config_index);
5651                self.debug_text(", ", config_index);
5652                self.debug_float_text(v[3], config_index);
5653                self.debug_text(")", config_index);
5654            }
5655            ShaderUniformValue::Int(v) => {
5656                self.debug_int_text(*v as f32, config_index);
5657            }
5658            ShaderUniformValue::Mat4(_) => {
5659                self.debug_text("[mat4]", config_index);
5660            }
5661        }
5662    }
5663
5664    /// Render the debug layout elements tree list. Returns (row_count, selected_element_row_index).
5665    fn render_debug_layout_elements_list(
5666        &mut self,
5667        initial_roots_length: usize,
5668        highlighted_row: i32,
5669    ) -> (i32, i32) {
5670        let row_height = Self::DEBUG_VIEW_ROW_HEIGHT;
5671        let indent_width = Self::DEBUG_VIEW_INDENT_WIDTH;
5672        let mut row_count: i32 = 0;
5673        let mut selected_element_row_index: i32 = 0;
5674        let mut highlighted_element_id: u32 = 0;
5675
5676        let scroll_item_layout = LayoutConfig {
5677            sizing: SizingConfig {
5678                height: SizingAxis {
5679                    type_: SizingType::Fixed,
5680                    min_max: SizingMinMax { min: row_height, max: row_height },
5681                    ..Default::default()
5682                },
5683                ..Default::default()
5684            },
5685            child_gap: 6,
5686            child_alignment: ChildAlignmentConfig { x: AlignX::Left, y: AlignY::CenterY },
5687            ..Default::default()
5688        };
5689
5690        let name_text_config = TextConfig {
5691            color: Self::DEBUG_COLOR_4,
5692            font_size: 16,
5693            wrap_mode: WrapMode::None,
5694            ..Default::default()
5695        };
5696
5697        for root_index in 0..initial_roots_length {
5698            let mut dfs_buffer: Vec<i32> = Vec::new();
5699            let root_layout_index = self.layout_element_tree_roots[root_index].layout_element_index;
5700            dfs_buffer.push(root_layout_index);
5701            let mut visited: Vec<bool> = vec![false; self.layout_elements.len()];
5702
5703            // Separator between roots
5704            if root_index > 0 {
5705                self.debug_open_idi("Ply__DebugView_EmptyRowOuter", root_index as u32, &ElementDeclaration {
5706                    layout: LayoutConfig {
5707                        sizing: SizingConfig {
5708                            width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
5709                            ..Default::default()
5710                        },
5711                        padding: PaddingConfig { left: indent_width / 2, right: 0, top: 0, bottom: 0 },
5712                        ..Default::default()
5713                    },
5714                    ..Default::default()
5715                });
5716                {
5717                    self.debug_open_idi("Ply__DebugView_EmptyRow", root_index as u32, &ElementDeclaration {
5718                        layout: LayoutConfig {
5719                            sizing: SizingConfig {
5720                                width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
5721                                height: SizingAxis {
5722                                    type_: SizingType::Fixed,
5723                                    min_max: SizingMinMax { min: row_height, max: row_height },
5724                                    ..Default::default()
5725                                },
5726                            },
5727                            ..Default::default()
5728                        },
5729                        border: BorderConfig {
5730                            color: Self::DEBUG_COLOR_3,
5731                            width: BorderWidth { top: 1, ..Default::default() },
5732                        },
5733                        ..Default::default()
5734                    });
5735                    self.close_element();
5736                }
5737                self.close_element();
5738                row_count += 1;
5739            }
5740
5741            while !dfs_buffer.is_empty() {
5742                let current_element_index = *dfs_buffer.last().unwrap() as usize;
5743                let depth = dfs_buffer.len() - 1;
5744
5745                if visited[depth] {
5746                    // Closing: pop from stack and close containers if non-text with children
5747                    let is_text = self.element_has_config(current_element_index, ElementConfigType::Text);
5748                    let children_len = self.layout_elements[current_element_index].children_length;
5749                    if !is_text && children_len > 0 {
5750                        self.close_element();
5751                        self.close_element();
5752                        self.close_element();
5753                    }
5754                    dfs_buffer.pop();
5755                    continue;
5756                }
5757
5758                // Check if this row is highlighted
5759                if highlighted_row == row_count {
5760                    if self.pointer_info.state == PointerDataInteractionState::PressedThisFrame {
5761                        let elem_id = self.layout_elements[current_element_index].id;
5762                        if self.debug_selected_element_id == elem_id {
5763                            self.debug_selected_element_id = 0; // Deselect on re-click
5764                        } else {
5765                            self.debug_selected_element_id = elem_id;
5766                        }
5767                    }
5768                    highlighted_element_id = self.layout_elements[current_element_index].id;
5769                }
5770
5771                visited[depth] = true;
5772                let current_elem_id = self.layout_elements[current_element_index].id;
5773
5774                // Get bounding box and collision info from hash map
5775                let bounding_box = self.layout_element_map
5776                    .get(&current_elem_id)
5777                    .map(|item| item.bounding_box)
5778                    .unwrap_or_default();
5779                let collision = self.layout_element_map
5780                    .get(&current_elem_id)
5781                    .map(|item| item.collision)
5782                    .unwrap_or(false);
5783                let collapsed = self.layout_element_map
5784                    .get(&current_elem_id)
5785                    .map(|item| item.collapsed)
5786                    .unwrap_or(false);
5787
5788                let offscreen = self.element_is_offscreen(&bounding_box);
5789
5790                if self.debug_selected_element_id == current_elem_id {
5791                    selected_element_row_index = row_count;
5792                }
5793
5794                // Row for this element
5795                let row_bg = if self.debug_selected_element_id == current_elem_id {
5796                    Color::rgba(217.0, 91.0, 67.0, 40.0) // Slight red for selected
5797                } else {
5798                    Color::rgba(0.0, 0.0, 0.0, 0.0)
5799                };
5800                self.debug_open_idi("Ply__DebugView_ElementOuter", current_elem_id, &ElementDeclaration {
5801                    layout: scroll_item_layout,
5802                    background_color: row_bg,
5803                    ..Default::default()
5804                });
5805                {
5806                    let is_text = self.element_has_config(current_element_index, ElementConfigType::Text);
5807                    let children_len = self.layout_elements[current_element_index].children_length;
5808
5809                    // Collapse icon / button or dot
5810                    if !is_text && children_len > 0 {
5811                        // Collapse button
5812                        self.debug_open_idi("Ply__DebugView_CollapseElement", current_elem_id, &ElementDeclaration {
5813                            layout: LayoutConfig {
5814                                sizing: SizingConfig {
5815                                    width: SizingAxis { type_: SizingType::Fixed, min_max: SizingMinMax { min: 16.0, max: 16.0 }, ..Default::default() },
5816                                    height: SizingAxis { type_: SizingType::Fixed, min_max: SizingMinMax { min: 16.0, max: 16.0 }, ..Default::default() },
5817                                },
5818                                child_alignment: ChildAlignmentConfig { x: AlignX::CenterX, y: AlignY::CenterY },
5819                                ..Default::default()
5820                            },
5821                            corner_radius: CornerRadius { top_left: 4.0, top_right: 4.0, bottom_left: 4.0, bottom_right: 4.0 },
5822                            border: BorderConfig {
5823                                color: Self::DEBUG_COLOR_3,
5824                                width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
5825                            },
5826                            ..Default::default()
5827                        });
5828                        {
5829                            let tc = self.store_text_element_config(TextConfig {
5830                                color: Self::DEBUG_COLOR_4,
5831                                font_size: 16,
5832                                ..Default::default()
5833                            });
5834                            if collapsed {
5835                                self.debug_text("+", tc);
5836                            } else {
5837                                self.debug_text("-", tc);
5838                            }
5839                        }
5840                        self.close_element();
5841                    } else {
5842                        // Empty dot for leaf elements
5843                        self.debug_open(&ElementDeclaration {
5844                            layout: LayoutConfig {
5845                                sizing: SizingConfig {
5846                                    width: SizingAxis { type_: SizingType::Fixed, min_max: SizingMinMax { min: 16.0, max: 16.0 }, ..Default::default() },
5847                                    height: SizingAxis { type_: SizingType::Fixed, min_max: SizingMinMax { min: 16.0, max: 16.0 }, ..Default::default() },
5848                                },
5849                                child_alignment: ChildAlignmentConfig { x: AlignX::CenterX, y: AlignY::CenterY },
5850                                ..Default::default()
5851                            },
5852                            ..Default::default()
5853                        });
5854                        {
5855                            self.debug_open(&ElementDeclaration {
5856                                layout: LayoutConfig {
5857                                    sizing: SizingConfig {
5858                                        width: SizingAxis { type_: SizingType::Fixed, min_max: SizingMinMax { min: 8.0, max: 8.0 }, ..Default::default() },
5859                                        height: SizingAxis { type_: SizingType::Fixed, min_max: SizingMinMax { min: 8.0, max: 8.0 }, ..Default::default() },
5860                                    },
5861                                    ..Default::default()
5862                                },
5863                                background_color: Self::DEBUG_COLOR_3,
5864                                corner_radius: CornerRadius { top_left: 2.0, top_right: 2.0, bottom_left: 2.0, bottom_right: 2.0 },
5865                                ..Default::default()
5866                            });
5867                            self.close_element();
5868                        }
5869                        self.close_element();
5870                    }
5871
5872                    // Collision warning badge
5873                    if collision {
5874                        self.debug_open(&ElementDeclaration {
5875                            layout: LayoutConfig {
5876                                padding: PaddingConfig { left: 8, right: 8, top: 2, bottom: 2 },
5877                                ..Default::default()
5878                            },
5879                            border: BorderConfig {
5880                                color: Color::rgba(177.0, 147.0, 8.0, 255.0),
5881                                width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
5882                            },
5883                            ..Default::default()
5884                        });
5885                        {
5886                            let tc = self.store_text_element_config(TextConfig {
5887                                color: Self::DEBUG_COLOR_3,
5888                                font_size: 16,
5889                                ..Default::default()
5890                            });
5891                            self.debug_text("Duplicate ID", tc);
5892                        }
5893                        self.close_element();
5894                    }
5895
5896                    // Offscreen badge
5897                    if offscreen {
5898                        self.debug_open(&ElementDeclaration {
5899                            layout: LayoutConfig {
5900                                padding: PaddingConfig { left: 8, right: 8, top: 2, bottom: 2 },
5901                                ..Default::default()
5902                            },
5903                            border: BorderConfig {
5904                                color: Self::DEBUG_COLOR_3,
5905                                width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
5906                            },
5907                            ..Default::default()
5908                        });
5909                        {
5910                            let tc = self.store_text_element_config(TextConfig {
5911                                color: Self::DEBUG_COLOR_3,
5912                                font_size: 16,
5913                                ..Default::default()
5914                            });
5915                            self.debug_text("Offscreen", tc);
5916                        }
5917                        self.close_element();
5918                    }
5919
5920                    // Element name
5921                    let id_string = if current_element_index < self.layout_element_id_strings.len() {
5922                        self.layout_element_id_strings[current_element_index].clone()
5923                    } else {
5924                        StringId::empty()
5925                    };
5926                    if !id_string.is_empty() {
5927                        let tc = if offscreen {
5928                            self.store_text_element_config(TextConfig {
5929                                color: Self::DEBUG_COLOR_3,
5930                                font_size: 16,
5931                                ..Default::default()
5932                            })
5933                        } else {
5934                            self.store_text_element_config(name_text_config.clone())
5935                        };
5936                        self.debug_raw_text(id_string.as_str(), tc);
5937                    }
5938
5939                    // Config type badges
5940                    let configs_start = self.layout_elements[current_element_index].element_configs.start;
5941                    let configs_len = self.layout_elements[current_element_index].element_configs.length;
5942                    for ci in 0..configs_len {
5943                        let ec = self.element_configs[configs_start + ci as usize];
5944                        if ec.config_type == ElementConfigType::Shared {
5945                            let shared = self.shared_element_configs[ec.config_index];
5946                            let label_color = Color::rgba(243.0, 134.0, 48.0, 90.0);
5947                            if shared.background_color.a > 0.0 {
5948                                self.debug_open(&ElementDeclaration {
5949                                    layout: LayoutConfig {
5950                                        padding: PaddingConfig { left: 8, right: 8, top: 2, bottom: 2 },
5951                                        ..Default::default()
5952                                    },
5953                                    background_color: label_color,
5954                                    corner_radius: CornerRadius { top_left: 4.0, top_right: 4.0, bottom_left: 4.0, bottom_right: 4.0 },
5955                                    border: BorderConfig {
5956                                        color: label_color,
5957                                        width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
5958                                    },
5959                                    ..Default::default()
5960                                });
5961                                {
5962                                    let tc = self.store_text_element_config(TextConfig {
5963                                        color: if offscreen { Self::DEBUG_COLOR_3 } else { Self::DEBUG_COLOR_4 },
5964                                        font_size: 16,
5965                                        ..Default::default()
5966                                    });
5967                                    self.debug_text("Color", tc);
5968                                }
5969                                self.close_element();
5970                            }
5971                            if shared.corner_radius.bottom_left > 0.0 {
5972                                let radius_color = Color::rgba(26.0, 188.0, 156.0, 90.0);
5973                                self.debug_open(&ElementDeclaration {
5974                                    layout: LayoutConfig {
5975                                        padding: PaddingConfig { left: 8, right: 8, top: 2, bottom: 2 },
5976                                        ..Default::default()
5977                                    },
5978                                    background_color: radius_color,
5979                                    corner_radius: CornerRadius { top_left: 4.0, top_right: 4.0, bottom_left: 4.0, bottom_right: 4.0 },
5980                                    border: BorderConfig {
5981                                        color: radius_color,
5982                                        width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
5983                                    },
5984                                    ..Default::default()
5985                                });
5986                                {
5987                                    let tc = self.store_text_element_config(TextConfig {
5988                                        color: if offscreen { Self::DEBUG_COLOR_3 } else { Self::DEBUG_COLOR_4 },
5989                                        font_size: 16,
5990                                        ..Default::default()
5991                                    });
5992                                    self.debug_text("Radius", tc);
5993                                }
5994                                self.close_element();
5995                            }
5996                            continue;
5997                        }
5998                        let (label, label_color) = Self::debug_get_config_type_label(ec.config_type);
5999                        let bg = Color::rgba(label_color.r, label_color.g, label_color.b, 90.0);
6000                        self.debug_open(&ElementDeclaration {
6001                            layout: LayoutConfig {
6002                                padding: PaddingConfig { left: 8, right: 8, top: 2, bottom: 2 },
6003                                ..Default::default()
6004                            },
6005                            background_color: bg,
6006                            corner_radius: CornerRadius { top_left: 4.0, top_right: 4.0, bottom_left: 4.0, bottom_right: 4.0 },
6007                            border: BorderConfig {
6008                                color: label_color,
6009                                width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
6010                            },
6011                            ..Default::default()
6012                        });
6013                        {
6014                            let tc = self.store_text_element_config(TextConfig {
6015                                color: if offscreen { Self::DEBUG_COLOR_3 } else { Self::DEBUG_COLOR_4 },
6016                                font_size: 16,
6017                                ..Default::default()
6018                            });
6019                            self.debug_text(label, tc);
6020                        }
6021                        self.close_element();
6022                    }
6023
6024                    // Shader badge
6025                    let has_shaders = self.element_shaders.get(current_element_index)
6026                        .map_or(false, |s| !s.is_empty());
6027                    if has_shaders {
6028                        let badge_color = Color::rgba(155.0, 89.0, 182.0, 90.0);
6029                        self.debug_open(&ElementDeclaration {
6030                            layout: LayoutConfig {
6031                                padding: PaddingConfig { left: 8, right: 8, top: 2, bottom: 2 },
6032                                ..Default::default()
6033                            },
6034                            background_color: badge_color,
6035                            corner_radius: CornerRadius { top_left: 4.0, top_right: 4.0, bottom_left: 4.0, bottom_right: 4.0 },
6036                            border: BorderConfig {
6037                                color: badge_color,
6038                                width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
6039                            },
6040                            ..Default::default()
6041                        });
6042                        {
6043                            let tc = self.store_text_element_config(TextConfig {
6044                                color: if offscreen { Self::DEBUG_COLOR_3 } else { Self::DEBUG_COLOR_4 },
6045                                font_size: 16,
6046                                ..Default::default()
6047                            });
6048                            self.debug_text("Shader", tc);
6049                        }
6050                        self.close_element();
6051                    }
6052
6053                    // Effect badge
6054                    let has_effects = self.element_effects.get(current_element_index)
6055                        .map_or(false, |e| !e.is_empty());
6056                    if has_effects {
6057                        let badge_color = Color::rgba(155.0, 89.0, 182.0, 90.0);
6058                        self.debug_open(&ElementDeclaration {
6059                            layout: LayoutConfig {
6060                                padding: PaddingConfig { left: 8, right: 8, top: 2, bottom: 2 },
6061                                ..Default::default()
6062                            },
6063                            background_color: badge_color,
6064                            corner_radius: CornerRadius { top_left: 4.0, top_right: 4.0, bottom_left: 4.0, bottom_right: 4.0 },
6065                            border: BorderConfig {
6066                                color: badge_color,
6067                                width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
6068                            },
6069                            ..Default::default()
6070                        });
6071                        {
6072                            let tc = self.store_text_element_config(TextConfig {
6073                                color: if offscreen { Self::DEBUG_COLOR_3 } else { Self::DEBUG_COLOR_4 },
6074                                font_size: 16,
6075                                ..Default::default()
6076                            });
6077                            self.debug_text("Effect", tc);
6078                        }
6079                        self.close_element();
6080                    }
6081
6082                    // Visual Rotation badge
6083                    let has_visual_rot = self.element_visual_rotations.get(current_element_index)
6084                        .map_or(false, |r| r.is_some());
6085                    if has_visual_rot {
6086                        let badge_color = Color::rgba(155.0, 89.0, 182.0, 90.0);
6087                        self.debug_open(&ElementDeclaration {
6088                            layout: LayoutConfig {
6089                                padding: PaddingConfig { left: 8, right: 8, top: 2, bottom: 2 },
6090                                ..Default::default()
6091                            },
6092                            background_color: badge_color,
6093                            corner_radius: CornerRadius { top_left: 4.0, top_right: 4.0, bottom_left: 4.0, bottom_right: 4.0 },
6094                            border: BorderConfig {
6095                                color: badge_color,
6096                                width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
6097                            },
6098                            ..Default::default()
6099                        });
6100                        {
6101                            let tc = self.store_text_element_config(TextConfig {
6102                                color: if offscreen { Self::DEBUG_COLOR_3 } else { Self::DEBUG_COLOR_4 },
6103                                font_size: 16,
6104                                ..Default::default()
6105                            });
6106                            self.debug_text("VisualRot", tc);
6107                        }
6108                        self.close_element();
6109                    }
6110
6111                    // Shape Rotation badge
6112                    let has_shape_rot = self.element_shape_rotations.get(current_element_index)
6113                        .map_or(false, |r| r.is_some());
6114                    if has_shape_rot {
6115                        let badge_color = Color::rgba(26.0, 188.0, 156.0, 90.0);
6116                        self.debug_open(&ElementDeclaration {
6117                            layout: LayoutConfig {
6118                                padding: PaddingConfig { left: 8, right: 8, top: 2, bottom: 2 },
6119                                ..Default::default()
6120                            },
6121                            background_color: badge_color,
6122                            corner_radius: CornerRadius { top_left: 4.0, top_right: 4.0, bottom_left: 4.0, bottom_right: 4.0 },
6123                            border: BorderConfig {
6124                                color: badge_color,
6125                                width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
6126                            },
6127                            ..Default::default()
6128                        });
6129                        {
6130                            let tc = self.store_text_element_config(TextConfig {
6131                                color: if offscreen { Self::DEBUG_COLOR_3 } else { Self::DEBUG_COLOR_4 },
6132                                font_size: 16,
6133                                ..Default::default()
6134                            });
6135                            self.debug_text("ShapeRot", tc);
6136                        }
6137                        self.close_element();
6138                    }
6139                }
6140                self.close_element(); // ElementOuter row
6141
6142                // Text element content row
6143                let is_text = self.element_has_config(current_element_index, ElementConfigType::Text);
6144                let children_len = self.layout_elements[current_element_index].children_length;
6145                if is_text {
6146                    row_count += 1;
6147                    let text_data_idx = self.layout_elements[current_element_index].text_data_index;
6148                    let text_content = if text_data_idx >= 0 {
6149                        self.text_element_data[text_data_idx as usize].text.clone()
6150                    } else {
6151                        String::new()
6152                    };
6153                    let raw_tc_idx = if offscreen {
6154                        self.store_text_element_config(TextConfig {
6155                            color: Self::DEBUG_COLOR_3,
6156                            font_size: 16,
6157                            ..Default::default()
6158                        })
6159                    } else {
6160                        self.store_text_element_config(name_text_config.clone())
6161                    };
6162                    self.debug_open(&ElementDeclaration {
6163                        layout: LayoutConfig {
6164                            sizing: SizingConfig {
6165                                height: SizingAxis {
6166                                    type_: SizingType::Fixed,
6167                                    min_max: SizingMinMax { min: row_height, max: row_height },
6168                                    ..Default::default()
6169                                },
6170                                ..Default::default()
6171                            },
6172                            child_alignment: ChildAlignmentConfig { x: AlignX::Left, y: AlignY::CenterY },
6173                            ..Default::default()
6174                        },
6175                        ..Default::default()
6176                    });
6177                    {
6178                        // Indent spacer
6179                        self.debug_open(&ElementDeclaration {
6180                            layout: LayoutConfig {
6181                                sizing: SizingConfig {
6182                                    width: SizingAxis {
6183                                        type_: SizingType::Fixed,
6184                                        min_max: SizingMinMax {
6185                                            min: (indent_width + 16) as f32,
6186                                            max: (indent_width + 16) as f32,
6187                                        },
6188                                        ..Default::default()
6189                                    },
6190                                    ..Default::default()
6191                                },
6192                                ..Default::default()
6193                            },
6194                            ..Default::default()
6195                        });
6196                        self.close_element();
6197                        self.debug_text("\"", raw_tc_idx);
6198                        if text_content.len() > 40 {
6199                            let mut end = 40;
6200                            while !text_content.is_char_boundary(end) { end -= 1; }
6201                            self.debug_raw_text(&text_content[..end], raw_tc_idx);
6202                            self.debug_text("...", raw_tc_idx);
6203                        } else if !text_content.is_empty() {
6204                            self.debug_raw_text(&text_content, raw_tc_idx);
6205                        }
6206                        self.debug_text("\"", raw_tc_idx);
6207                    }
6208                    self.close_element();
6209                } else if children_len > 0 {
6210                    // Open containers for child indentation
6211                    self.open_element();
6212                    self.configure_open_element(&ElementDeclaration {
6213                        layout: LayoutConfig {
6214                            padding: PaddingConfig { left: 8, ..Default::default() },
6215                            ..Default::default()
6216                        },
6217                        ..Default::default()
6218                    });
6219                    self.open_element();
6220                    self.configure_open_element(&ElementDeclaration {
6221                        layout: LayoutConfig {
6222                            padding: PaddingConfig { left: indent_width, ..Default::default() },
6223                            ..Default::default()
6224                        },
6225                        border: BorderConfig {
6226                            color: Self::DEBUG_COLOR_3,
6227                            width: BorderWidth { left: 1, ..Default::default() },
6228                        },
6229                        ..Default::default()
6230                    });
6231                    self.open_element();
6232                    self.configure_open_element(&ElementDeclaration {
6233                        layout: LayoutConfig {
6234                            layout_direction: LayoutDirection::TopToBottom,
6235                            ..Default::default()
6236                        },
6237                        ..Default::default()
6238                    });
6239                }
6240
6241                row_count += 1;
6242
6243                // Push children in reverse order for DFS (if not text and not collapsed)
6244                if !is_text && !collapsed {
6245                    let children_start = self.layout_elements[current_element_index].children_start;
6246                    let children_length = self.layout_elements[current_element_index].children_length as usize;
6247                    for i in (0..children_length).rev() {
6248                        let child_idx = self.layout_element_children[children_start + i];
6249                        dfs_buffer.push(child_idx);
6250                        // Ensure visited vec is large enough
6251                        while visited.len() <= dfs_buffer.len() {
6252                            visited.push(false);
6253                        }
6254                        visited[dfs_buffer.len() - 1] = false;
6255                    }
6256                }
6257            }
6258        }
6259
6260        // Handle collapse button clicks
6261        if self.pointer_info.state == PointerDataInteractionState::PressedThisFrame {
6262            let collapse_base_id = hash_string("Ply__DebugView_CollapseElement", 0).base_id;
6263            for i in (0..self.pointer_over_ids.len()).rev() {
6264                let element_id = self.pointer_over_ids[i].clone();
6265                if element_id.base_id == collapse_base_id {
6266                    if let Some(item) = self.layout_element_map.get_mut(&element_id.offset) {
6267                        item.collapsed = !item.collapsed;
6268                    }
6269                    break;
6270                }
6271            }
6272        }
6273
6274        // Render highlight on hovered or selected element
6275        // When an element is selected, show its bounding box; otherwise show hovered
6276        let highlight_target = if self.debug_selected_element_id != 0 {
6277            self.debug_selected_element_id
6278        } else {
6279            highlighted_element_id
6280        };
6281        if highlight_target != 0 {
6282            self.debug_open_id("Ply__DebugView_ElementHighlight", &ElementDeclaration {
6283                layout: LayoutConfig {
6284                    sizing: SizingConfig {
6285                        width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
6286                        height: SizingAxis { type_: SizingType::Grow, ..Default::default() },
6287                    },
6288                    ..Default::default()
6289                },
6290                floating: FloatingConfig {
6291                    parent_id: highlight_target,
6292                    z_index: 32767,
6293                    pointer_capture_mode: PointerCaptureMode::Passthrough,
6294                    attach_to: FloatingAttachToElement::ElementWithId,
6295                    ..Default::default()
6296                },
6297                ..Default::default()
6298            });
6299            {
6300                self.debug_open_id("Ply__DebugView_ElementHighlightRectangle", &ElementDeclaration {
6301                    layout: LayoutConfig {
6302                        sizing: SizingConfig {
6303                            width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
6304                            height: SizingAxis { type_: SizingType::Grow, ..Default::default() },
6305                        },
6306                        ..Default::default()
6307                    },
6308                    background_color: Self::DEBUG_HIGHLIGHT_COLOR,
6309                    ..Default::default()
6310                });
6311                self.close_element();
6312            }
6313            self.close_element();
6314        }
6315
6316        (row_count, selected_element_row_index)
6317    }
6318
6319    /// Main debug view rendering. Called from end_layout() when debug mode is enabled.
6320    fn render_debug_view(&mut self) {
6321        let initial_roots_length = self.layout_element_tree_roots.len();
6322        let initial_elements_length = self.layout_elements.len();
6323        let row_height = Self::DEBUG_VIEW_ROW_HEIGHT;
6324        let outer_padding = Self::DEBUG_VIEW_OUTER_PADDING;
6325        let debug_width = Self::DEBUG_VIEW_WIDTH;
6326
6327        let info_text_config = self.store_text_element_config(TextConfig {
6328            color: Self::DEBUG_COLOR_4,
6329            font_size: 16,
6330            wrap_mode: WrapMode::None,
6331            ..Default::default()
6332        });
6333        let info_title_config = self.store_text_element_config(TextConfig {
6334            color: Self::DEBUG_COLOR_3,
6335            font_size: 16,
6336            wrap_mode: WrapMode::None,
6337            ..Default::default()
6338        });
6339
6340        // Determine scroll offset for the debug scroll pane
6341        let scroll_id = hash_string("Ply__DebugViewOuterScrollPane", 0);
6342        let mut scroll_y_offset: f32 = 0.0;
6343        // Only exclude the bottom 300px from tree interaction when the detail panel is shown
6344        let detail_panel_height = if self.debug_selected_element_id != 0 { 300.0 } else { 0.0 };
6345        let mut pointer_in_debug_view = self.pointer_info.position.y < self.layout_dimensions.height - detail_panel_height;
6346        for scd in &self.scroll_container_datas {
6347            if scd.element_id == scroll_id.id {
6348                if !self.external_scroll_handling_enabled {
6349                    scroll_y_offset = scd.scroll_position.y;
6350                } else {
6351                    pointer_in_debug_view = self.pointer_info.position.y + scd.scroll_position.y
6352                        < self.layout_dimensions.height - detail_panel_height;
6353                }
6354                break;
6355            }
6356        }
6357
6358        let highlighted_row = if pointer_in_debug_view {
6359            ((self.pointer_info.position.y - scroll_y_offset) / row_height) as i32 - 1
6360        } else {
6361            -1
6362        };
6363        let highlighted_row = if self.pointer_info.position.x < self.layout_dimensions.width - debug_width {
6364            -1
6365        } else {
6366            highlighted_row
6367        };
6368
6369        // Main debug view panel (floating)
6370        self.debug_open_id("Ply__DebugView", &ElementDeclaration {
6371            layout: LayoutConfig {
6372                sizing: SizingConfig {
6373                    width: SizingAxis {
6374                        type_: SizingType::Fixed,
6375                        min_max: SizingMinMax { min: debug_width, max: debug_width },
6376                        ..Default::default()
6377                    },
6378                    height: SizingAxis {
6379                        type_: SizingType::Fixed,
6380                        min_max: SizingMinMax { min: self.layout_dimensions.height, max: self.layout_dimensions.height },
6381                        ..Default::default()
6382                    },
6383                },
6384                layout_direction: LayoutDirection::TopToBottom,
6385                ..Default::default()
6386            },
6387            floating: FloatingConfig {
6388                z_index: 32765,
6389                attach_points: FloatingAttachPoints {
6390                    element_x: AlignX::Right,
6391                    element_y: AlignY::CenterY,
6392                    parent_x: AlignX::Right,
6393                    parent_y: AlignY::CenterY,
6394                },
6395                attach_to: FloatingAttachToElement::Root,
6396                clip_to: FloatingClipToElement::AttachedParent,
6397                ..Default::default()
6398            },
6399            border: BorderConfig {
6400                color: Self::DEBUG_COLOR_3,
6401                width: BorderWidth { bottom: 1, ..Default::default() },
6402            },
6403            ..Default::default()
6404        });
6405        {
6406            // Header bar
6407            self.debug_open(&ElementDeclaration {
6408                layout: LayoutConfig {
6409                    sizing: SizingConfig {
6410                        width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
6411                        height: SizingAxis {
6412                            type_: SizingType::Fixed,
6413                            min_max: SizingMinMax { min: row_height, max: row_height },
6414                            ..Default::default()
6415                        },
6416                    },
6417                    padding: PaddingConfig { left: outer_padding, right: outer_padding, top: 0, bottom: 0 },
6418                    child_alignment: ChildAlignmentConfig { x: AlignX::Left, y: AlignY::CenterY },
6419                    ..Default::default()
6420                },
6421                background_color: Self::DEBUG_COLOR_2,
6422                ..Default::default()
6423            });
6424            {
6425                self.debug_text("Ply Debug Tools", info_text_config);
6426                // Spacer
6427                self.debug_open(&ElementDeclaration {
6428                    layout: LayoutConfig {
6429                        sizing: SizingConfig {
6430                            width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
6431                            ..Default::default()
6432                        },
6433                        ..Default::default()
6434                    },
6435                    ..Default::default()
6436                });
6437                self.close_element();
6438                // Close button
6439                let close_size = row_height - 10.0;
6440                self.debug_open_id("Ply__DebugView_CloseButton", &ElementDeclaration {
6441                    layout: LayoutConfig {
6442                        sizing: SizingConfig {
6443                            width: SizingAxis { type_: SizingType::Fixed, min_max: SizingMinMax { min: close_size, max: close_size }, ..Default::default() },
6444                            height: SizingAxis { type_: SizingType::Fixed, min_max: SizingMinMax { min: close_size, max: close_size }, ..Default::default() },
6445                        },
6446                        child_alignment: ChildAlignmentConfig { x: AlignX::CenterX, y: AlignY::CenterY },
6447                        ..Default::default()
6448                    },
6449                    background_color: Color::rgba(217.0, 91.0, 67.0, 80.0),
6450                    corner_radius: CornerRadius { top_left: 4.0, top_right: 4.0, bottom_left: 4.0, bottom_right: 4.0 },
6451                    border: BorderConfig {
6452                        color: Color::rgba(217.0, 91.0, 67.0, 255.0),
6453                        width: BorderWidth { left: 1, right: 1, top: 1, bottom: 1, between_children: 0 },
6454                    },
6455                    ..Default::default()
6456                });
6457                {
6458                    let tc = self.store_text_element_config(TextConfig {
6459                        color: Self::DEBUG_COLOR_4,
6460                        font_size: 16,
6461                        ..Default::default()
6462                    });
6463                    self.debug_text("x", tc);
6464                }
6465                self.close_element();
6466            }
6467            self.close_element();
6468
6469            // Separator line
6470            self.debug_open(&ElementDeclaration {
6471                layout: LayoutConfig {
6472                    sizing: SizingConfig {
6473                        width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
6474                        height: SizingAxis { type_: SizingType::Fixed, min_max: SizingMinMax { min: 1.0, max: 1.0 }, ..Default::default() },
6475                    },
6476                    ..Default::default()
6477                },
6478                background_color: Self::DEBUG_COLOR_3,
6479                ..Default::default()
6480            });
6481            self.close_element();
6482
6483            // Scroll pane
6484            self.open_element_with_id(&scroll_id);
6485            self.configure_open_element(&ElementDeclaration {
6486                layout: LayoutConfig {
6487                    sizing: SizingConfig {
6488                        width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
6489                        height: SizingAxis { type_: SizingType::Grow, ..Default::default() },
6490                    },
6491                    ..Default::default()
6492                },
6493                clip: ClipConfig {
6494                    horizontal: true,
6495                    vertical: true,
6496                    scroll_x: true,
6497                    scroll_y: true,
6498                    child_offset: self.get_scroll_offset(),
6499                },
6500                ..Default::default()
6501            });
6502            {
6503                let alt_bg = if (initial_elements_length + initial_roots_length) & 1 == 0 {
6504                    Self::DEBUG_COLOR_2
6505                } else {
6506                    Self::DEBUG_COLOR_1
6507                };
6508                // Content container — Fit height so it extends beyond the scroll pane
6509                self.debug_open(&ElementDeclaration {
6510                    layout: LayoutConfig {
6511                        sizing: SizingConfig {
6512                            width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
6513                            ..Default::default() // height defaults to Fit
6514                        },
6515                        padding: PaddingConfig {
6516                            left: outer_padding,
6517                            right: outer_padding,
6518                            top: 0,
6519                            bottom: 0,
6520                        },
6521                        layout_direction: LayoutDirection::TopToBottom,
6522                        ..Default::default()
6523                    },
6524                    background_color: alt_bg,
6525                    ..Default::default()
6526                });
6527                {
6528                    let _layout_data = self.render_debug_layout_elements_list(
6529                        initial_roots_length,
6530                        highlighted_row,
6531                    );
6532                }
6533                self.close_element(); // content container
6534            }
6535            self.close_element(); // scroll pane
6536
6537            // Separator
6538            self.debug_open(&ElementDeclaration {
6539                layout: LayoutConfig {
6540                    sizing: SizingConfig {
6541                        width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
6542                        height: SizingAxis { type_: SizingType::Fixed, min_max: SizingMinMax { min: 1.0, max: 1.0 }, ..Default::default() },
6543                    },
6544                    ..Default::default()
6545                },
6546                background_color: Self::DEBUG_COLOR_3,
6547                ..Default::default()
6548            });
6549            self.close_element();
6550
6551            // Selected element detail panel
6552            if self.debug_selected_element_id != 0 {
6553                self.render_debug_selected_element_panel(info_text_config, info_title_config);
6554            }
6555        }
6556        self.close_element(); // Ply__DebugView
6557
6558        // Handle close button click
6559        if self.pointer_info.state == PointerDataInteractionState::PressedThisFrame {
6560            let close_base_id = hash_string("Ply__DebugView_CloseButton", 0).id;
6561            let header_base_id = hash_string("Ply__DebugView_LayoutConfigHeader", 0).id;
6562            for i in (0..self.pointer_over_ids.len()).rev() {
6563                let id = self.pointer_over_ids[i].id;
6564                if id == close_base_id {
6565                    self.debug_mode_enabled = false;
6566                    break;
6567                }
6568                if id == header_base_id {
6569                    self.debug_selected_element_id = 0;
6570                    break;
6571                }
6572            }
6573        }
6574    }
6575
6576    /// Render the selected element detail panel in the debug view.
6577    fn render_debug_selected_element_panel(
6578        &mut self,
6579        info_text_config: usize,
6580        info_title_config: usize,
6581    ) {
6582        let row_height = Self::DEBUG_VIEW_ROW_HEIGHT;
6583        let outer_padding = Self::DEBUG_VIEW_OUTER_PADDING;
6584        let attr_padding = PaddingConfig {
6585            left: outer_padding,
6586            right: outer_padding,
6587            top: 8,
6588            bottom: 8,
6589        };
6590
6591        let selected_id = self.debug_selected_element_id;
6592        let selected_item = match self.layout_element_map.get(&selected_id) {
6593            Some(item) => item.clone(),
6594            None => return,
6595        };
6596        let layout_elem_idx = selected_item.layout_element_index as usize;
6597        if layout_elem_idx >= self.layout_elements.len() {
6598            return;
6599        }
6600
6601        let layout_config_index = self.layout_elements[layout_elem_idx].layout_config_index;
6602        let layout_config = self.layout_configs[layout_config_index];
6603
6604        self.debug_open(&ElementDeclaration {
6605            layout: LayoutConfig {
6606                sizing: SizingConfig {
6607                    width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
6608                    height: SizingAxis {
6609                        type_: SizingType::Fixed,
6610                        min_max: SizingMinMax { min: 316.0, max: 316.0 },
6611                        ..Default::default()
6612                    },
6613                },
6614                layout_direction: LayoutDirection::TopToBottom,
6615                ..Default::default()
6616            },
6617            background_color: Self::DEBUG_COLOR_2,
6618            clip: ClipConfig {
6619                vertical: true,
6620                scroll_y: true,
6621                child_offset: self.get_scroll_offset(),
6622                ..Default::default()
6623            },
6624            border: BorderConfig {
6625                color: Self::DEBUG_COLOR_3,
6626                width: BorderWidth { between_children: 1, ..Default::default() },
6627            },
6628            ..Default::default()
6629        });
6630        {
6631            // Header: "Layout Config" + element ID
6632            self.debug_open_id("Ply__DebugView_LayoutConfigHeader", &ElementDeclaration {
6633                layout: LayoutConfig {
6634                    sizing: SizingConfig {
6635                        width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
6636                        height: SizingAxis {
6637                            type_: SizingType::Fixed,
6638                            min_max: SizingMinMax { min: row_height + 8.0, max: row_height + 8.0 },
6639                            ..Default::default()
6640                        },
6641                    },
6642                    padding: PaddingConfig { left: outer_padding, right: outer_padding, top: 0, bottom: 0 },
6643                    child_alignment: ChildAlignmentConfig { x: AlignX::Left, y: AlignY::CenterY },
6644                    ..Default::default()
6645                },
6646                ..Default::default()
6647            });
6648            {
6649                self.debug_text("Layout Config", info_text_config);
6650                // Spacer
6651                self.debug_open(&ElementDeclaration {
6652                    layout: LayoutConfig {
6653                        sizing: SizingConfig {
6654                            width: SizingAxis { type_: SizingType::Grow, ..Default::default() },
6655                            ..Default::default()
6656                        },
6657                        ..Default::default()
6658                    },
6659                    ..Default::default()
6660                });
6661                self.close_element();
6662                // Element ID string
6663                let sid = selected_item.element_id.string_id.clone();
6664                if !sid.is_empty() {
6665                    self.debug_raw_text(sid.as_str(), info_title_config);
6666                    if selected_item.element_id.offset != 0 {
6667                        self.debug_text(" (", info_title_config);
6668                        self.debug_int_text(selected_item.element_id.offset as f32, info_title_config);
6669                        self.debug_text(")", info_title_config);
6670                    }
6671                }
6672            }
6673            self.close_element();
6674
6675            // Layout config details
6676            self.debug_open(&ElementDeclaration {
6677                layout: LayoutConfig {
6678                    padding: attr_padding,
6679                    child_gap: 8,
6680                    layout_direction: LayoutDirection::TopToBottom,
6681                    ..Default::default()
6682                },
6683                ..Default::default()
6684            });
6685            {
6686                // Bounding Box
6687                self.debug_text("Bounding Box", info_title_config);
6688                self.debug_open(&ElementDeclaration::default());
6689                {
6690                    self.debug_text("{ x: ", info_text_config);
6691                    self.debug_int_text(selected_item.bounding_box.x, info_text_config);
6692                    self.debug_text(", y: ", info_text_config);
6693                    self.debug_int_text(selected_item.bounding_box.y, info_text_config);
6694                    self.debug_text(", width: ", info_text_config);
6695                    self.debug_int_text(selected_item.bounding_box.width, info_text_config);
6696                    self.debug_text(", height: ", info_text_config);
6697                    self.debug_int_text(selected_item.bounding_box.height, info_text_config);
6698                    self.debug_text(" }", info_text_config);
6699                }
6700                self.close_element();
6701
6702                // Layout Direction
6703                self.debug_text("Layout Direction", info_title_config);
6704                if layout_config.layout_direction == LayoutDirection::TopToBottom {
6705                    self.debug_text("TOP_TO_BOTTOM", info_text_config);
6706                } else {
6707                    self.debug_text("LEFT_TO_RIGHT", info_text_config);
6708                }
6709
6710                // Sizing
6711                self.debug_text("Sizing", info_title_config);
6712                self.debug_open(&ElementDeclaration::default());
6713                {
6714                    self.debug_text("width: ", info_text_config);
6715                    self.render_debug_layout_sizing(layout_config.sizing.width, info_text_config);
6716                }
6717                self.close_element();
6718                self.debug_open(&ElementDeclaration::default());
6719                {
6720                    self.debug_text("height: ", info_text_config);
6721                    self.render_debug_layout_sizing(layout_config.sizing.height, info_text_config);
6722                }
6723                self.close_element();
6724
6725                // Padding
6726                self.debug_text("Padding", info_title_config);
6727                self.debug_open_id("Ply__DebugViewElementInfoPadding", &ElementDeclaration::default());
6728                {
6729                    self.debug_text("{ left: ", info_text_config);
6730                    self.debug_int_text(layout_config.padding.left as f32, info_text_config);
6731                    self.debug_text(", right: ", info_text_config);
6732                    self.debug_int_text(layout_config.padding.right as f32, info_text_config);
6733                    self.debug_text(", top: ", info_text_config);
6734                    self.debug_int_text(layout_config.padding.top as f32, info_text_config);
6735                    self.debug_text(", bottom: ", info_text_config);
6736                    self.debug_int_text(layout_config.padding.bottom as f32, info_text_config);
6737                    self.debug_text(" }", info_text_config);
6738                }
6739                self.close_element();
6740
6741                // Child Gap
6742                self.debug_text("Child Gap", info_title_config);
6743                self.debug_int_text(layout_config.child_gap as f32, info_text_config);
6744
6745                // Child Alignment
6746                self.debug_text("Child Alignment", info_title_config);
6747                self.debug_open(&ElementDeclaration::default());
6748                {
6749                    self.debug_text("{ x: ", info_text_config);
6750                    let align_x = Self::align_x_name(layout_config.child_alignment.x);
6751                    self.debug_text(align_x, info_text_config);
6752                    self.debug_text(", y: ", info_text_config);
6753                    let align_y = Self::align_y_name(layout_config.child_alignment.y);
6754                    self.debug_text(align_y, info_text_config);
6755                    self.debug_text(" }", info_text_config);
6756                }
6757                self.close_element();
6758            }
6759            self.close_element(); // layout config details
6760
6761            // ── Collect data for grouped categories ──
6762            let configs_start = self.layout_elements[layout_elem_idx].element_configs.start;
6763            let configs_len = self.layout_elements[layout_elem_idx].element_configs.length;
6764            let elem_id_string = selected_item.element_id.string_id.clone();
6765
6766            // Shared data (split into Color + Shape)
6767            let mut shared_bg_color: Option<Color> = None;
6768            let mut shared_corner_radius: Option<CornerRadius> = None;
6769            for ci in 0..configs_len {
6770                let ec = self.element_configs[configs_start + ci as usize];
6771                if ec.config_type == ElementConfigType::Shared {
6772                    let shared = self.shared_element_configs[ec.config_index];
6773                    shared_bg_color = Some(shared.background_color);
6774                    shared_corner_radius = Some(shared.corner_radius);
6775                }
6776            }
6777
6778            // Per-element data (not in element_configs system)
6779            let shape_rot = self.element_shape_rotations.get(layout_elem_idx).copied().flatten();
6780            let visual_rot = self.element_visual_rotations.get(layout_elem_idx).cloned().flatten();
6781            let effects = self.element_effects.get(layout_elem_idx).cloned().unwrap_or_default();
6782            let shaders = self.element_shaders.get(layout_elem_idx).cloned().unwrap_or_default();
6783
6784            // ── [Color] section ──
6785            let has_color = shared_bg_color.map_or(false, |c| c.a > 0.0);
6786            if has_color {
6787                let color_label_color = Color::rgba(243.0, 134.0, 48.0, 255.0);
6788                self.render_debug_view_category_header("Color", color_label_color, elem_id_string.clone());
6789                self.debug_open(&ElementDeclaration {
6790                    layout: LayoutConfig {
6791                        padding: attr_padding,
6792                        child_gap: 8,
6793                        layout_direction: LayoutDirection::TopToBottom,
6794                        ..Default::default()
6795                    },
6796                    ..Default::default()
6797                });
6798                {
6799                    self.debug_text("Background Color", info_title_config);
6800                    self.render_debug_view_color(shared_bg_color.unwrap(), info_text_config);
6801                }
6802                self.close_element();
6803            }
6804
6805            // ── [Shape] section (Corner Radius + Shape Rotation) ──
6806            let has_corner_radius = shared_corner_radius.map_or(false, |cr| !cr.is_zero());
6807            let has_shape_rot = shape_rot.is_some();
6808            if has_corner_radius || has_shape_rot {
6809                let shape_label_color = Color::rgba(26.0, 188.0, 156.0, 255.0);
6810                self.render_debug_view_category_header("Shape", shape_label_color, elem_id_string.clone());
6811                self.debug_open(&ElementDeclaration {
6812                    layout: LayoutConfig {
6813                        padding: attr_padding,
6814                        child_gap: 8,
6815                        layout_direction: LayoutDirection::TopToBottom,
6816                        ..Default::default()
6817                    },
6818                    ..Default::default()
6819                });
6820                {
6821                    if let Some(cr) = shared_corner_radius {
6822                        if !cr.is_zero() {
6823                            self.debug_text("Corner Radius", info_title_config);
6824                            self.render_debug_view_corner_radius(cr, info_text_config);
6825                        }
6826                    }
6827                    if let Some(sr) = shape_rot {
6828                        self.debug_text("Shape Rotation", info_title_config);
6829                        self.debug_open(&ElementDeclaration::default());
6830                        {
6831                            self.debug_text("angle: ", info_text_config);
6832                            self.debug_float_text(sr.rotation_radians, info_text_config);
6833                            self.debug_text(" rad", info_text_config);
6834                        }
6835                        self.close_element();
6836                        self.debug_open(&ElementDeclaration::default());
6837                        {
6838                            self.debug_text("flip_x: ", info_text_config);
6839                            self.debug_text(if sr.flip_x { "true" } else { "false" }, info_text_config);
6840                            self.debug_text(", flip_y: ", info_text_config);
6841                            self.debug_text(if sr.flip_y { "true" } else { "false" }, info_text_config);
6842                        }
6843                        self.close_element();
6844                    }
6845                }
6846                self.close_element();
6847            }
6848
6849            // ── Config-type sections (Text, Image, Floating, Clip, Border, etc.) ──
6850            for ci in 0..configs_len {
6851                let ec = self.element_configs[configs_start + ci as usize];
6852                match ec.config_type {
6853                    ElementConfigType::Shared => {} // handled above as [Color] + [Shape]
6854                    ElementConfigType::Text => {
6855                        self.render_debug_view_element_config_header(elem_id_string.clone(), ec.config_type, info_title_config);
6856                        let text_config = self.text_element_configs[ec.config_index].clone();
6857                        self.debug_open(&ElementDeclaration {
6858                            layout: LayoutConfig {
6859                                padding: attr_padding,
6860                                child_gap: 8,
6861                                layout_direction: LayoutDirection::TopToBottom,
6862                                ..Default::default()
6863                            },
6864                            ..Default::default()
6865                        });
6866                        {
6867                            self.debug_text("Font Size", info_title_config);
6868                            self.debug_int_text(text_config.font_size as f32, info_text_config);
6869                            self.debug_text("Font", info_title_config);
6870                            {
6871                                let label = if let Some(asset) = text_config.font_asset {
6872                                    asset.key().to_string()
6873                                } else {
6874                                    format!("default ({})", self.default_font_key)
6875                                };
6876                                self.open_text_element(&label, info_text_config);
6877                            }
6878                            self.debug_text("Line Height", info_title_config);
6879                            if text_config.line_height == 0 {
6880                                self.debug_text("auto", info_text_config);
6881                            } else {
6882                                self.debug_int_text(text_config.line_height as f32, info_text_config);
6883                            }
6884                            self.debug_text("Letter Spacing", info_title_config);
6885                            self.debug_int_text(text_config.letter_spacing as f32, info_text_config);
6886                            self.debug_text("Wrap Mode", info_title_config);
6887                            let wrap = match text_config.wrap_mode {
6888                                WrapMode::None => "NONE",
6889                                WrapMode::Newline => "NEWLINES",
6890                                _ => "WORDS",
6891                            };
6892                            self.debug_text(wrap, info_text_config);
6893                            self.debug_text("Text Alignment", info_title_config);
6894                            let align = match text_config.alignment {
6895                                AlignX::CenterX => "CENTER",
6896                                AlignX::Right => "RIGHT",
6897                                _ => "LEFT",
6898                            };
6899                            self.debug_text(align, info_text_config);
6900                            self.debug_text("Text Color", info_title_config);
6901                            self.render_debug_view_color(text_config.color, info_text_config);
6902                        }
6903                        self.close_element();
6904                    }
6905                    ElementConfigType::Image => {
6906                        let image_label_color = Color::rgba(121.0, 189.0, 154.0, 255.0);
6907                        self.render_debug_view_category_header("Image", image_label_color, elem_id_string.clone());
6908                        let image_data = self.image_element_configs[ec.config_index].clone();
6909                        self.debug_open(&ElementDeclaration {
6910                            layout: LayoutConfig {
6911                                padding: attr_padding,
6912                                child_gap: 8,
6913                                layout_direction: LayoutDirection::TopToBottom,
6914                                ..Default::default()
6915                            },
6916                            ..Default::default()
6917                        });
6918                        {
6919                            self.debug_text("Source", info_title_config);
6920                            let name = image_data.get_name();
6921                            self.debug_raw_text(name, info_text_config);
6922                        }
6923                        self.close_element();
6924                    }
6925                    ElementConfigType::Clip => {
6926                        self.render_debug_view_element_config_header(elem_id_string.clone(), ec.config_type, info_title_config);
6927                        let clip_config = self.clip_element_configs[ec.config_index];
6928                        self.debug_open(&ElementDeclaration {
6929                            layout: LayoutConfig {
6930                                padding: attr_padding,
6931                                child_gap: 8,
6932                                layout_direction: LayoutDirection::TopToBottom,
6933                                ..Default::default()
6934                            },
6935                            ..Default::default()
6936                        });
6937                        {
6938                            self.debug_text("Overflow", info_title_config);
6939                            self.debug_open(&ElementDeclaration::default());
6940                            {
6941                                let x_label = if clip_config.scroll_x {
6942                                    "SCROLL"
6943                                } else if clip_config.horizontal {
6944                                    "CLIP"
6945                                } else {
6946                                    "OVERFLOW"
6947                                };
6948                                let y_label = if clip_config.scroll_y {
6949                                    "SCROLL"
6950                                } else if clip_config.vertical {
6951                                    "CLIP"
6952                                } else {
6953                                    "OVERFLOW"
6954                                };
6955                                self.debug_text("{ x: ", info_text_config);
6956                                self.debug_text(x_label, info_text_config);
6957                                self.debug_text(", y: ", info_text_config);
6958                                self.debug_text(y_label, info_text_config);
6959                                self.debug_text(" }", info_text_config);
6960                            }
6961                            self.close_element();
6962                        }
6963                        self.close_element();
6964                    }
6965                    ElementConfigType::Floating => {
6966                        self.render_debug_view_element_config_header(elem_id_string.clone(), ec.config_type, info_title_config);
6967                        let float_config = self.floating_element_configs[ec.config_index];
6968                        self.debug_open(&ElementDeclaration {
6969                            layout: LayoutConfig {
6970                                padding: attr_padding,
6971                                child_gap: 8,
6972                                layout_direction: LayoutDirection::TopToBottom,
6973                                ..Default::default()
6974                            },
6975                            ..Default::default()
6976                        });
6977                        {
6978                            self.debug_text("Offset", info_title_config);
6979                            self.debug_open(&ElementDeclaration::default());
6980                            {
6981                                self.debug_text("{ x: ", info_text_config);
6982                                self.debug_int_text(float_config.offset.x, info_text_config);
6983                                self.debug_text(", y: ", info_text_config);
6984                                self.debug_int_text(float_config.offset.y, info_text_config);
6985                                self.debug_text(" }", info_text_config);
6986                            }
6987                            self.close_element();
6988
6989                            self.debug_text("z-index", info_title_config);
6990                            self.debug_int_text(float_config.z_index as f32, info_text_config);
6991
6992                            self.debug_text("Parent", info_title_config);
6993                            let parent_name = self.layout_element_map
6994                                .get(&float_config.parent_id)
6995                                .map(|item| item.element_id.string_id.clone())
6996                                .unwrap_or(StringId::empty());
6997                            if !parent_name.is_empty() {
6998                                self.debug_raw_text(parent_name.as_str(), info_text_config);
6999                            }
7000
7001                            self.debug_text("Attach Points", info_title_config);
7002                            self.debug_open(&ElementDeclaration::default());
7003                            {
7004                                self.debug_text("{ element: (", info_text_config);
7005                                self.debug_text(Self::align_x_name(float_config.attach_points.element_x), info_text_config);
7006                                self.debug_text(", ", info_text_config);
7007                                self.debug_text(Self::align_y_name(float_config.attach_points.element_y), info_text_config);
7008                                self.debug_text("), parent: (", info_text_config);
7009                                self.debug_text(Self::align_x_name(float_config.attach_points.parent_x), info_text_config);
7010                                self.debug_text(", ", info_text_config);
7011                                self.debug_text(Self::align_y_name(float_config.attach_points.parent_y), info_text_config);
7012                                self.debug_text(") }", info_text_config);
7013                            }
7014                            self.close_element();
7015
7016                            self.debug_text("Pointer Capture Mode", info_title_config);
7017                            let pcm = if float_config.pointer_capture_mode == PointerCaptureMode::Passthrough {
7018                                "PASSTHROUGH"
7019                            } else {
7020                                "NONE"
7021                            };
7022                            self.debug_text(pcm, info_text_config);
7023
7024                            self.debug_text("Attach To", info_title_config);
7025                            let at = match float_config.attach_to {
7026                                FloatingAttachToElement::Parent => "PARENT",
7027                                FloatingAttachToElement::ElementWithId => "ELEMENT_WITH_ID",
7028                                FloatingAttachToElement::Root => "ROOT",
7029                                _ => "NONE",
7030                            };
7031                            self.debug_text(at, info_text_config);
7032
7033                            self.debug_text("Clip To", info_title_config);
7034                            let ct = if float_config.clip_to == FloatingClipToElement::None {
7035                                "NONE"
7036                            } else {
7037                                "ATTACHED_PARENT"
7038                            };
7039                            self.debug_text(ct, info_text_config);
7040                        }
7041                        self.close_element();
7042                    }
7043                    ElementConfigType::Border => {
7044                        self.render_debug_view_element_config_header(elem_id_string.clone(), ec.config_type, info_title_config);
7045                        let border_config = self.border_element_configs[ec.config_index];
7046                        self.debug_open_id("Ply__DebugViewElementInfoBorderBody", &ElementDeclaration {
7047                            layout: LayoutConfig {
7048                                padding: attr_padding,
7049                                child_gap: 8,
7050                                layout_direction: LayoutDirection::TopToBottom,
7051                                ..Default::default()
7052                            },
7053                            ..Default::default()
7054                        });
7055                        {
7056                            self.debug_text("Border Widths", info_title_config);
7057                            self.debug_open(&ElementDeclaration::default());
7058                            {
7059                                self.debug_text("{ left: ", info_text_config);
7060                                self.debug_int_text(border_config.width.left as f32, info_text_config);
7061                                self.debug_text(", right: ", info_text_config);
7062                                self.debug_int_text(border_config.width.right as f32, info_text_config);
7063                                self.debug_text(", top: ", info_text_config);
7064                                self.debug_int_text(border_config.width.top as f32, info_text_config);
7065                                self.debug_text(", bottom: ", info_text_config);
7066                                self.debug_int_text(border_config.width.bottom as f32, info_text_config);
7067                                self.debug_text(" }", info_text_config);
7068                            }
7069                            self.close_element();
7070                            self.debug_text("Border Color", info_title_config);
7071                            self.render_debug_view_color(border_config.color, info_text_config);
7072                        }
7073                        self.close_element();
7074                    }
7075                    ElementConfigType::TextInput => {
7076                        // ── [Input] section for text input config ──
7077                        let input_label_color = Color::rgba(52.0, 152.0, 219.0, 255.0);
7078                        self.render_debug_view_category_header("Input", input_label_color, elem_id_string.clone());
7079                        let ti_cfg = self.text_input_configs[ec.config_index].clone();
7080                        self.debug_open(&ElementDeclaration {
7081                            layout: LayoutConfig {
7082                                padding: attr_padding,
7083                                child_gap: 8,
7084                                layout_direction: LayoutDirection::TopToBottom,
7085                                ..Default::default()
7086                            },
7087                            ..Default::default()
7088                        });
7089                        {
7090                            if !ti_cfg.placeholder.is_empty() {
7091                                self.debug_text("Placeholder", info_title_config);
7092                                self.debug_raw_text(&ti_cfg.placeholder, info_text_config);
7093                            }
7094                            self.debug_text("Max Length", info_title_config);
7095                            if let Some(max_len) = ti_cfg.max_length {
7096                                self.debug_int_text(max_len as f32, info_text_config);
7097                            } else {
7098                                self.debug_text("unlimited", info_text_config);
7099                            }
7100                            self.debug_text("Password", info_title_config);
7101                            self.debug_text(if ti_cfg.is_password { "true" } else { "false" }, info_text_config);
7102                            self.debug_text("Multiline", info_title_config);
7103                            self.debug_text(if ti_cfg.is_multiline { "true" } else { "false" }, info_text_config);
7104                            self.debug_text("Font", info_title_config);
7105                            self.debug_open(&ElementDeclaration::default());
7106                            {
7107                                let label = if let Some(asset) = ti_cfg.font_asset {
7108                                    asset.key().to_string()
7109                                } else {
7110                                    format!("default ({})", self.default_font_key)
7111                                };
7112                                self.open_text_element(&label, info_text_config);
7113                                self.debug_text(", size: ", info_text_config);
7114                                self.debug_int_text(ti_cfg.font_size as f32, info_text_config);
7115                            }
7116                            self.close_element();
7117                            self.debug_text("Text Color", info_title_config);
7118                            self.render_debug_view_color(ti_cfg.text_color, info_text_config);
7119                            self.debug_text("Cursor Color", info_title_config);
7120                            self.render_debug_view_color(ti_cfg.cursor_color, info_text_config);
7121                            self.debug_text("Selection Color", info_title_config);
7122                            self.render_debug_view_color(ti_cfg.selection_color, info_text_config);
7123                            // Show current text value
7124                            let state_data = self.text_edit_states.get(&selected_id)
7125                                .map(|s| (s.text.clone(), s.cursor_pos));
7126                            if let Some((text_val, cursor_pos)) = state_data {
7127                                self.debug_text("Value", info_title_config);
7128                                let preview = if text_val.len() > 40 {
7129                                    let mut end = 40;
7130                                    while !text_val.is_char_boundary(end) { end -= 1; }
7131                                    format!("\"{}...\"", &text_val[..end])
7132                                } else {
7133                                    format!("\"{}\"", &text_val)
7134                                };
7135                                self.debug_raw_text(&preview, info_text_config);
7136                                self.debug_text("Cursor Position", info_title_config);
7137                                self.debug_int_text(cursor_pos as f32, info_text_config);
7138                            }
7139                        }
7140                        self.close_element();
7141                    }
7142                    _ => {}
7143                }
7144            }
7145
7146            // ── [Effects] section (Visual Rotation + Shaders + Effects) ──
7147            let has_visual_rot = visual_rot.is_some();
7148            let has_effects = !effects.is_empty();
7149            let has_shaders = !shaders.is_empty();
7150            if has_visual_rot || has_effects || has_shaders {
7151                let effects_label_color = Color::rgba(155.0, 89.0, 182.0, 255.0);
7152                self.render_debug_view_category_header("Effects", effects_label_color, elem_id_string.clone());
7153                self.debug_open(&ElementDeclaration {
7154                    layout: LayoutConfig {
7155                        padding: attr_padding,
7156                        child_gap: 8,
7157                        layout_direction: LayoutDirection::TopToBottom,
7158                        ..Default::default()
7159                    },
7160                    ..Default::default()
7161                });
7162                {
7163                    if let Some(vr) = visual_rot {
7164                        self.debug_text("Visual Rotation", info_title_config);
7165                        self.debug_open(&ElementDeclaration::default());
7166                        {
7167                            self.debug_text("angle: ", info_text_config);
7168                            self.debug_float_text(vr.rotation_radians, info_text_config);
7169                            self.debug_text(" rad", info_text_config);
7170                        }
7171                        self.close_element();
7172                        self.debug_open(&ElementDeclaration::default());
7173                        {
7174                            self.debug_text("pivot: (", info_text_config);
7175                            self.debug_float_text(vr.pivot_x, info_text_config);
7176                            self.debug_text(", ", info_text_config);
7177                            self.debug_float_text(vr.pivot_y, info_text_config);
7178                            self.debug_text(")", info_text_config);
7179                        }
7180                        self.close_element();
7181                        self.debug_open(&ElementDeclaration::default());
7182                        {
7183                            self.debug_text("flip_x: ", info_text_config);
7184                            self.debug_text(if vr.flip_x { "true" } else { "false" }, info_text_config);
7185                            self.debug_text(", flip_y: ", info_text_config);
7186                            self.debug_text(if vr.flip_y { "true" } else { "false" }, info_text_config);
7187                        }
7188                        self.close_element();
7189                    }
7190                    for (i, effect) in effects.iter().enumerate() {
7191                        let label = format!("Effect {}", i + 1);
7192                        self.debug_text("Effect", info_title_config);
7193                        self.debug_open(&ElementDeclaration::default());
7194                        {
7195                            self.debug_raw_text(&label, info_text_config);
7196                            self.debug_text(": ", info_text_config);
7197                            self.debug_raw_text(&effect.name, info_text_config);
7198                        }
7199                        self.close_element();
7200                        for uniform in &effect.uniforms {
7201                            self.debug_open(&ElementDeclaration::default());
7202                            {
7203                                self.debug_text("  ", info_text_config);
7204                                self.debug_raw_text(&uniform.name, info_text_config);
7205                                self.debug_text(": ", info_text_config);
7206                                self.render_debug_shader_uniform_value(&uniform.value, info_text_config);
7207                            }
7208                            self.close_element();
7209                        }
7210                    }
7211                    for (i, shader) in shaders.iter().enumerate() {
7212                        let label = format!("Shader {}", i + 1);
7213                        self.debug_text("Shader", info_title_config);
7214                        self.debug_open(&ElementDeclaration::default());
7215                        {
7216                            self.debug_raw_text(&label, info_text_config);
7217                            self.debug_text(": ", info_text_config);
7218                            self.debug_raw_text(&shader.name, info_text_config);
7219                        }
7220                        self.close_element();
7221                        for uniform in &shader.uniforms {
7222                            self.debug_open(&ElementDeclaration::default());
7223                            {
7224                                self.debug_text("  ", info_text_config);
7225                                self.debug_raw_text(&uniform.name, info_text_config);
7226                                self.debug_text(": ", info_text_config);
7227                                self.render_debug_shader_uniform_value(&uniform.value, info_text_config);
7228                            }
7229                            self.close_element();
7230                        }
7231                    }
7232                }
7233                self.close_element();
7234            }
7235        }
7236        self.close_element(); // detail panel
7237    }
7238
7239    fn align_x_name(value: AlignX) -> &'static str {
7240        match value {
7241            AlignX::Left => "LEFT",
7242            AlignX::CenterX => "CENTER",
7243            AlignX::Right => "RIGHT",
7244        }
7245    }
7246
7247    fn align_y_name(value: AlignY) -> &'static str {
7248        match value {
7249            AlignY::Top => "TOP",
7250            AlignY::CenterY => "CENTER",
7251            AlignY::Bottom => "BOTTOM",
7252        }
7253    }
7254
7255    pub fn set_max_element_count(&mut self, count: i32) {
7256        self.max_element_count = count;
7257    }
7258
7259    pub fn set_max_measure_text_cache_word_count(&mut self, count: i32) {
7260        self.max_measure_text_cache_word_count = count;
7261    }
7262
7263    pub fn set_debug_mode_enabled(&mut self, enabled: bool) {
7264        self.debug_mode_enabled = enabled;
7265    }
7266
7267    pub fn is_debug_mode_enabled(&self) -> bool {
7268        self.debug_mode_enabled
7269    }
7270
7271    pub fn set_culling_enabled(&mut self, enabled: bool) {
7272        self.culling_disabled = !enabled;
7273    }
7274
7275    pub fn set_measure_text_function(
7276        &mut self,
7277        f: Box<dyn Fn(&str, &TextConfig) -> Dimensions>,
7278    ) {
7279        self.measure_text_fn = Some(f);
7280        // Invalidate the font height cache since the measurement function changed.
7281        self.font_height_cache.clear();
7282    }
7283}