Skip to main content

ply_engine/
engine.rs

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