Skip to main content

blinc_layout/widgets/
text_area.rs

1//! Ready-to-use TextArea widget
2//!
3//! Multi-line text area with:
4//! - Multi-line text editing
5//! - Row/column sizing (like HTML textarea)
6//! - Cursor and selection
7//! - Visual states: idle, hovered, focused
8//! - Built-in styling that just works
9//! - Inherits ALL Div methods for full layout control via Deref
10
11use std::sync::{
12    atomic::{AtomicU64, Ordering},
13    Arc, Mutex,
14};
15
16use blinc_core::reactive::SignalId;
17use blinc_core::Color;
18use blinc_theme::{ColorToken, ThemeState};
19
20use crate::canvas::canvas;
21use crate::css_parser::{active_stylesheet, ElementState, Stylesheet};
22use crate::div::{div, Div, ElementBuilder};
23use crate::element::RenderProps;
24use crate::stateful::{
25    refresh_stateful, SharedState, StateTransitions, Stateful, StatefulInner, TextFieldState,
26};
27use crate::text::text;
28use crate::tree::{LayoutNodeId, LayoutTree};
29use crate::widgets::cursor::{cursor_state, CursorAnimation, SharedCursorState};
30use crate::widgets::scroll::{Scroll, ScrollDirection, ScrollPhysics, SharedScrollPhysics};
31use crate::widgets::text_input::{
32    elapsed_ms, increment_focus_count, request_continuous_redraw_pub, set_focused_text_area,
33};
34
35/// Position in a multi-line text (line and column)
36#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
37pub struct TextPosition {
38    /// Line index (0-based)
39    pub line: usize,
40    /// Column index (character offset within line, 0-based)
41    pub column: usize,
42}
43
44impl TextPosition {
45    pub fn new(line: usize, column: usize) -> Self {
46        Self { line, column }
47    }
48}
49
50// =============================================================================
51// CSS Override Resolution
52// =============================================================================
53
54/// Apply CSS stylesheet overrides to a TextAreaConfig.
55fn apply_css_overrides_textarea(
56    cfg: &mut TextAreaConfig,
57    stylesheet: &Stylesheet,
58    element_id: &str,
59    visual: &TextFieldState,
60) {
61    // 1. Apply base style
62    if let Some(base) = stylesheet.get(element_id) {
63        apply_style_to_textarea_config(cfg, base, visual);
64    }
65
66    // 2. Layer state-specific style
67    let state = match visual {
68        TextFieldState::Hovered | TextFieldState::FocusedHovered => Some(ElementState::Hover),
69        TextFieldState::Focused => Some(ElementState::Focus),
70        TextFieldState::Disabled => Some(ElementState::Disabled),
71        TextFieldState::Idle => None,
72    };
73    if matches!(visual, TextFieldState::FocusedHovered) {
74        if let Some(focus_style) = stylesheet.get_with_state(element_id, ElementState::Focus) {
75            apply_style_to_textarea_config(cfg, focus_style, visual);
76        }
77    }
78    if let Some(s) = state {
79        if let Some(state_style) = stylesheet.get_with_state(element_id, s) {
80            apply_style_to_textarea_config(cfg, state_style, visual);
81        }
82    }
83
84    // 3. Apply ::placeholder style
85    if let Some(placeholder_style) = stylesheet.get_placeholder_style(element_id) {
86        if let Some(color) = placeholder_style.text_color {
87            cfg.placeholder_color = color;
88        }
89        if let Some(color) = placeholder_style.placeholder_color {
90            cfg.placeholder_color = color;
91        }
92    }
93}
94
95fn apply_style_to_textarea_config(
96    cfg: &mut TextAreaConfig,
97    style: &crate::element_style::ElementStyle,
98    visual: &TextFieldState,
99) {
100    if let Some(blinc_core::Brush::Solid(color)) = style.background.as_ref() {
101        match visual {
102            TextFieldState::Idle => cfg.bg_color = *color,
103            TextFieldState::Hovered => cfg.hover_bg_color = *color,
104            TextFieldState::Focused | TextFieldState::FocusedHovered => {
105                cfg.focused_bg_color = *color;
106            }
107            TextFieldState::Disabled => {}
108        }
109    }
110    if let Some(color) = style.border_color {
111        match visual {
112            TextFieldState::Idle => cfg.border_color = color,
113            TextFieldState::Hovered => cfg.hover_border_color = color,
114            TextFieldState::Focused | TextFieldState::FocusedHovered => {
115                cfg.focused_border_color = color;
116            }
117            TextFieldState::Disabled => {}
118        }
119    }
120    if let Some(w) = style.border_width {
121        cfg.border_width = w;
122    }
123    if let Some(cr) = style.corner_radius {
124        cfg.corner_radius = cr.top_left;
125    }
126    if let Some(color) = style.text_color {
127        cfg.text_color = color;
128    }
129    if let Some(size) = style.font_size {
130        cfg.font_size = size;
131    }
132    if let Some(color) = style.caret_color {
133        cfg.cursor_color = color;
134    }
135    if let Some(color) = style.selection_color {
136        cfg.selection_color = color;
137    }
138    if let Some(color) = style.placeholder_color {
139        cfg.placeholder_color = color;
140    }
141}
142
143/// Extract outline properties from stylesheet for the current state.
144fn extract_outline_from_textarea_stylesheet(
145    stylesheet: &Stylesheet,
146    element_id: &str,
147    visual: &TextFieldState,
148) -> Option<(f32, Color, f32)> {
149    let mut width = None;
150    let mut color = None;
151    let mut offset = None;
152
153    if let Some(base) = stylesheet.get(element_id) {
154        if let Some(w) = base.outline_width {
155            width = Some(w);
156        }
157        if let Some(c) = base.outline_color {
158            color = Some(c);
159        }
160        if let Some(o) = base.outline_offset {
161            offset = Some(o);
162        }
163    }
164
165    let state = match visual {
166        TextFieldState::Hovered | TextFieldState::FocusedHovered => Some(ElementState::Hover),
167        TextFieldState::Focused => Some(ElementState::Focus),
168        TextFieldState::Disabled => Some(ElementState::Disabled),
169        TextFieldState::Idle => None,
170    };
171    if matches!(visual, TextFieldState::FocusedHovered) {
172        if let Some(focus_style) = stylesheet.get_with_state(element_id, ElementState::Focus) {
173            if let Some(w) = focus_style.outline_width {
174                width = Some(w);
175            }
176            if let Some(c) = focus_style.outline_color {
177                color = Some(c);
178            }
179            if let Some(o) = focus_style.outline_offset {
180                offset = Some(o);
181            }
182        }
183    }
184    if let Some(s) = state {
185        if let Some(state_style) = stylesheet.get_with_state(element_id, s) {
186            if let Some(w) = state_style.outline_width {
187                width = Some(w);
188            }
189            if let Some(c) = state_style.outline_color {
190                color = Some(c);
191            }
192            if let Some(o) = state_style.outline_offset {
193                offset = Some(o);
194            }
195        }
196    }
197
198    width.map(|w| {
199        (
200            w,
201            color.unwrap_or(Color::rgba(0.23, 0.51, 0.97, 0.5)),
202            offset.unwrap_or(0.0),
203        )
204    })
205}
206
207/// TextArea configuration
208#[derive(Clone)]
209pub struct TextAreaConfig {
210    /// Placeholder text shown when empty
211    pub placeholder: String,
212    /// Width of the text area (can be overridden by cols)
213    pub width: f32,
214    /// Height of the text area (can be overridden by rows)
215    pub height: f32,
216    /// Number of visible rows (overrides height if set)
217    pub rows: Option<usize>,
218    /// Number of visible columns/character width (overrides width if set)
219    pub cols: Option<usize>,
220    /// Font size
221    pub font_size: f32,
222    /// Line height multiplier
223    pub line_height: f32,
224    /// Approximate character width in ems (for cols calculation)
225    pub char_width_ratio: f32,
226    /// Text color
227    pub text_color: Color,
228    /// Placeholder text color
229    pub placeholder_color: Color,
230    /// Background color
231    pub bg_color: Color,
232    /// Hovered background color
233    pub hover_bg_color: Color,
234    /// Focused background color
235    pub focused_bg_color: Color,
236    /// Border color
237    pub border_color: Color,
238    /// Hovered border color
239    pub hover_border_color: Color,
240    /// Focused border color
241    pub focused_border_color: Color,
242    /// Border width
243    pub border_width: f32,
244    /// Corner radius
245    pub corner_radius: f32,
246    /// Horizontal padding
247    pub padding_x: f32,
248    /// Vertical padding
249    pub padding_y: f32,
250    /// Cursor color
251    pub cursor_color: Color,
252    /// Selection color
253    pub selection_color: Color,
254    /// Whether the text area is disabled
255    pub disabled: bool,
256    /// Maximum character count (0 = unlimited)
257    pub max_length: usize,
258    /// Whether text wraps at container bounds (default: true)
259    /// When true, long lines wrap to the next visual line.
260    /// When false, content scrolls horizontally.
261    pub wrap: bool,
262}
263
264impl Default for TextAreaConfig {
265    fn default() -> Self {
266        let theme = ThemeState::get();
267        Self {
268            placeholder: String::new(),
269            width: 300.0,
270            height: 120.0,
271            rows: None,
272            cols: None,
273            font_size: 14.0,
274            line_height: 1.4,
275            char_width_ratio: 0.6,
276            text_color: theme.color(ColorToken::TextPrimary),
277            placeholder_color: theme.color(ColorToken::TextTertiary),
278            bg_color: theme.color(ColorToken::InputBg),
279            hover_bg_color: theme.color(ColorToken::InputBgHover),
280            focused_bg_color: theme.color(ColorToken::InputBgFocus),
281            border_color: theme.color(ColorToken::BorderSecondary),
282            hover_border_color: theme.color(ColorToken::BorderHover),
283            focused_border_color: theme.color(ColorToken::BorderFocus),
284            border_width: 1.5,
285            corner_radius: 8.0,
286            padding_x: 12.0,
287            padding_y: 10.0,
288            cursor_color: theme.color(ColorToken::Accent),
289            selection_color: theme.color(ColorToken::Selection),
290            disabled: false,
291            max_length: 0,
292            // Default to wrapping since we now have visual lines computed for proper
293            // cursor tracking. Visual lines are computed in the callback and used for
294            // both rendering and cursor positioning to ensure they match.
295            wrap: true,
296        }
297    }
298}
299
300impl TextAreaConfig {
301    /// Calculate the effective width based on cols or explicit width
302    pub fn effective_width(&self) -> f32 {
303        if let Some(cols) = self.cols {
304            let char_width = self.font_size * self.char_width_ratio;
305            cols as f32 * char_width + self.padding_x * 2.0 + self.border_width * 2.0
306        } else {
307            self.width
308        }
309    }
310
311    /// Calculate the effective height based on rows or explicit height
312    pub fn effective_height(&self) -> f32 {
313        if let Some(rows) = self.rows {
314            let single_line_height = self.font_size * self.line_height;
315            rows as f32 * single_line_height + self.padding_y * 2.0 + self.border_width * 2.0
316        } else {
317            self.height
318        }
319    }
320}
321
322/// A visual line segment - represents a portion of a logical line that fits on one visual line
323#[derive(Clone, Debug)]
324pub struct VisualLine {
325    /// Index of the logical line this visual line belongs to
326    pub logical_line: usize,
327    /// Start character index within the logical line
328    pub start_char: usize,
329    /// End character index within the logical line (exclusive)
330    pub end_char: usize,
331    /// The text content of this visual line (cached for rendering)
332    pub text: String,
333    /// Width of this visual line in pixels
334    pub width: f32,
335}
336
337/// TextArea widget state
338#[derive(Clone)]
339pub struct TextAreaState {
340    /// Lines of text
341    pub lines: Vec<String>,
342    /// Cursor position
343    pub cursor: TextPosition,
344    /// Selection start position (if selecting)
345    pub selection_start: Option<TextPosition>,
346    /// Visual state for styling
347    pub visual: TextFieldState,
348    /// Placeholder text
349    pub placeholder: String,
350    /// Whether disabled
351    pub disabled: bool,
352    /// Time when focus was gained (for cursor blinking)
353    /// Stored as milliseconds since some epoch (e.g., app start)
354    pub focus_time_ms: u64,
355    /// Cursor blink interval in milliseconds
356    pub cursor_blink_interval_ms: u64,
357    /// Canvas-based cursor state for smooth animation
358    pub cursor_state: SharedCursorState,
359    /// Shared scroll physics for vertical scrolling
360    pub(crate) scroll_physics: SharedScrollPhysics,
361    /// Cached viewport height for scroll calculations
362    pub(crate) viewport_height: f32,
363    /// Cached line height for scroll calculations
364    pub(crate) line_height: f32,
365    /// Cached font size for scroll calculations with wrapping
366    pub(crate) font_size: f32,
367    /// Cached available width for text (for wrapping calculations)
368    pub(crate) available_width: f32,
369    /// Cached wrap enabled flag
370    pub(crate) wrap_enabled: bool,
371    /// Computed visual lines (recomputed when text or width changes)
372    /// Each VisualLine represents one rendered row of text
373    pub(crate) visual_lines: Vec<VisualLine>,
374    /// Reference to the Stateful's shared state for triggering incremental updates
375    pub(crate) stateful_state: Option<SharedState<TextFieldState>>,
376    /// Last clicked visual line index (set by line click handlers, read by main handler)
377    /// This is used to accurately determine which line was clicked when local_y
378    /// is relative to the clicked line element rather than the whole text area.
379    pub(crate) clicked_visual_line: Option<usize>,
380    /// Change version counter - increments on each text change
381    /// Use this with `signal_id()` to depend on text changes from external components
382    pub(crate) change_version: Arc<AtomicU64>,
383    /// Signal ID for the change version (set externally when binding to reactive system)
384    pub(crate) change_signal_id: Option<SignalId>,
385    /// Layout bounds storage - updated after layout to get actual rendered dimensions
386    pub layout_bounds_storage: crate::renderer::LayoutBoundsStorage,
387    /// CSS element ID for stylesheet matching (set via TextArea::id())
388    pub(crate) css_element_id: Option<String>,
389}
390
391impl std::fmt::Debug for TextAreaState {
392    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
393        f.debug_struct("TextAreaState")
394            .field("lines", &self.lines)
395            .field("cursor", &self.cursor)
396            .field("selection_start", &self.selection_start)
397            .field("visual", &self.visual)
398            .field("placeholder", &self.placeholder)
399            .field("disabled", &self.disabled)
400            .field("focus_time_ms", &self.focus_time_ms)
401            .field("cursor_blink_interval_ms", &self.cursor_blink_interval_ms)
402            // Skip stateful_state since StatefulInner doesn't implement Debug
403            .finish()
404    }
405}
406
407impl Default for TextAreaState {
408    fn default() -> Self {
409        Self {
410            lines: vec![String::new()],
411            cursor: TextPosition::default(),
412            selection_start: None,
413            visual: TextFieldState::Idle,
414            placeholder: String::new(),
415            disabled: false,
416            focus_time_ms: 0,
417            cursor_blink_interval_ms: 530, // Standard cursor blink rate (~530ms)
418            cursor_state: cursor_state(),
419            scroll_physics: Arc::new(Mutex::new(ScrollPhysics::default())),
420            viewport_height: 120.0,   // Default height from TextAreaConfig
421            line_height: 14.0 * 1.4,  // Default font_size * line_height
422            font_size: 14.0,          // Default font size
423            available_width: 276.0,   // Default width minus padding/borders
424            wrap_enabled: true,       // Default to wrapping (visual lines handle cursor tracking)
425            visual_lines: Vec::new(), // Computed on first layout
426            stateful_state: None,
427            clicked_visual_line: None, // Set by line click handlers
428            change_version: Arc::new(AtomicU64::new(0)),
429            change_signal_id: None,
430            layout_bounds_storage: Arc::new(Mutex::new(None)),
431            css_element_id: None,
432        }
433    }
434}
435
436impl TextAreaState {
437    /// Create new text area state
438    pub fn new() -> Self {
439        Self::default()
440    }
441
442    /// Create with initial value
443    pub fn with_value(value: impl Into<String>) -> Self {
444        let value = value.into();
445        let lines: Vec<String> = if value.is_empty() {
446            vec![String::new()]
447        } else {
448            value.lines().map(|s| s.to_string()).collect()
449        };
450        let cursor = TextPosition::new(
451            lines.len().saturating_sub(1),
452            lines.last().map(|l| l.chars().count()).unwrap_or(0),
453        );
454        Self {
455            lines,
456            cursor,
457            ..Default::default()
458        }
459    }
460
461    /// Create with placeholder
462    pub fn with_placeholder(placeholder: impl Into<String>) -> Self {
463        Self {
464            placeholder: placeholder.into(),
465            ..Default::default()
466        }
467    }
468
469    /// Get the full text value
470    pub fn value(&self) -> String {
471        self.lines.join("\n")
472    }
473
474    /// Set the text value
475    pub fn set_value(&mut self, value: &str) {
476        self.lines = if value.is_empty() {
477            vec![String::new()]
478        } else {
479            value.lines().map(|s| s.to_string()).collect()
480        };
481        self.cursor = TextPosition::new(
482            self.lines.len().saturating_sub(1),
483            self.lines.last().map(|l| l.chars().count()).unwrap_or(0),
484        );
485        self.selection_start = None;
486    }
487
488    /// Get number of lines
489    pub fn line_count(&self) -> usize {
490        self.lines.len()
491    }
492
493    /// Get a specific line
494    pub fn get_line(&self, index: usize) -> Option<&str> {
495        self.lines.get(index).map(|s| s.as_str())
496    }
497
498    /// Is empty?
499    pub fn is_empty(&self) -> bool {
500        self.lines.len() == 1 && self.lines[0].is_empty()
501    }
502
503    /// Is focused?
504    pub fn is_focused(&self) -> bool {
505        self.visual.is_focused()
506    }
507
508    /// Get the signal ID for text change notifications
509    ///
510    /// Use this to depend on text changes from external components.
511    /// Returns None if no signal has been set.
512    ///
513    /// # Example
514    ///
515    /// ```ignore
516    /// let text_state = ctx.use_state_keyed("editor", || Arc::new(Mutex::new(TextAreaState::new())));
517    /// let state = text_state.get();
518    ///
519    /// // Set up a signal for the text area
520    /// let change_signal = ctx.use_signal(0u64);
521    /// state.lock().unwrap().set_change_signal(change_signal.id());
522    ///
523    /// // Use the signal as a dependency for live preview
524    /// stateful(preview_state)
525    ///     .deps(&[state.lock().unwrap().signal_id().unwrap()])
526    ///     .child(markdown_light(&state.lock().unwrap().value()))
527    /// ```
528    pub fn signal_id(&self) -> Option<SignalId> {
529        self.change_signal_id
530    }
531
532    /// Set the signal ID for text change notifications
533    ///
534    /// When text changes, this signal will be notified (via check_stateful_deps).
535    pub fn set_change_signal(&mut self, signal_id: SignalId) {
536        self.change_signal_id = Some(signal_id);
537    }
538
539    /// Get the current change version
540    ///
541    /// This increments each time text is modified.
542    pub fn change_version(&self) -> u64 {
543        self.change_version.load(Ordering::SeqCst)
544    }
545
546    /// Check if cursor should be visible based on current time
547    /// Returns true if cursor is in the "on" phase of blinking
548    pub fn is_cursor_visible(&self, current_time_ms: u64) -> bool {
549        if self.cursor_blink_interval_ms == 0 {
550            return true; // No blinking, always visible
551        }
552        let elapsed = current_time_ms.saturating_sub(self.focus_time_ms);
553        let phase = (elapsed / self.cursor_blink_interval_ms) % 2;
554        phase == 0
555    }
556
557    /// Reset cursor blink (call when focus gained or cursor moved)
558    pub fn reset_cursor_blink(&mut self) {
559        self.focus_time_ms = elapsed_ms();
560        // Also reset the canvas cursor state for smooth animation
561        if let Ok(mut cs) = self.cursor_state.lock() {
562            cs.reset_blink();
563        }
564    }
565
566    /// Insert text at cursor
567    pub fn insert(&mut self, text: &str) {
568        self.delete_selection();
569
570        if text.contains('\n') {
571            for (i, part) in text.split('\n').enumerate() {
572                if i > 0 {
573                    self.insert_newline_internal();
574                }
575                self.insert_text(part);
576            }
577        } else {
578            self.insert_text(text);
579        }
580    }
581
582    fn insert_text(&mut self, text: &str) {
583        let line_idx = self.cursor.line.min(self.lines.len().saturating_sub(1));
584        let byte_pos = char_to_byte_pos(&self.lines[line_idx], self.cursor.column);
585        self.lines[line_idx].insert_str(byte_pos, text);
586        self.cursor.column += text.chars().count();
587    }
588
589    /// Insert a newline at cursor
590    pub fn insert_newline(&mut self) {
591        self.insert_newline_internal();
592    }
593
594    /// Internal newline insertion (no notify)
595    fn insert_newline_internal(&mut self) {
596        self.delete_selection();
597
598        let line_idx = self.cursor.line.min(self.lines.len().saturating_sub(1));
599        let byte_pos = char_to_byte_pos(&self.lines[line_idx], self.cursor.column);
600
601        let after = self.lines[line_idx].split_off(byte_pos);
602        self.lines.insert(line_idx + 1, after);
603
604        self.cursor.line += 1;
605        self.cursor.column = 0;
606    }
607
608    /// Delete character before cursor (backspace)
609    pub fn delete_backward(&mut self) {
610        if self.delete_selection() {
611            return;
612        }
613
614        if self.cursor.column > 0 {
615            let start_byte =
616                char_to_byte_pos(&self.lines[self.cursor.line], self.cursor.column - 1);
617            let end_byte = char_to_byte_pos(&self.lines[self.cursor.line], self.cursor.column);
618            self.lines[self.cursor.line].replace_range(start_byte..end_byte, "");
619            self.cursor.column -= 1;
620        } else if self.cursor.line > 0 {
621            let current_line = self.lines.remove(self.cursor.line);
622            self.cursor.line -= 1;
623            self.cursor.column = self.lines[self.cursor.line].chars().count();
624            self.lines[self.cursor.line].push_str(&current_line);
625        }
626    }
627
628    /// Delete character after cursor (delete)
629    pub fn delete_forward(&mut self) {
630        if self.delete_selection() {
631            return;
632        }
633
634        let line_len = self.lines[self.cursor.line].chars().count();
635        if self.cursor.column < line_len {
636            let start_byte = char_to_byte_pos(&self.lines[self.cursor.line], self.cursor.column);
637            let end_byte = char_to_byte_pos(&self.lines[self.cursor.line], self.cursor.column + 1);
638            self.lines[self.cursor.line].replace_range(start_byte..end_byte, "");
639        } else if self.cursor.line < self.lines.len() - 1 {
640            let next_line = self.lines.remove(self.cursor.line + 1);
641            self.lines[self.cursor.line].push_str(&next_line);
642        }
643    }
644
645    /// Delete selected text
646    fn delete_selection(&mut self) -> bool {
647        if let Some(start) = self.selection_start {
648            let (from, to) = self.order_positions(start, self.cursor);
649
650            if from != to {
651                if from.line == to.line {
652                    let start_byte = char_to_byte_pos(&self.lines[from.line], from.column);
653                    let end_byte = char_to_byte_pos(&self.lines[from.line], to.column);
654                    self.lines[from.line].replace_range(start_byte..end_byte, "");
655                } else {
656                    let from_byte = char_to_byte_pos(&self.lines[from.line], from.column);
657                    self.lines[from.line].truncate(from_byte);
658
659                    let to_byte = char_to_byte_pos(&self.lines[to.line], to.column);
660                    let after_text = self.lines[to.line][to_byte..].to_string();
661                    self.lines[from.line].push_str(&after_text);
662
663                    for _ in from.line + 1..=to.line {
664                        if from.line + 1 < self.lines.len() {
665                            self.lines.remove(from.line + 1);
666                        }
667                    }
668                }
669
670                self.cursor = from;
671                self.selection_start = None;
672                return true;
673            }
674        }
675        self.selection_start = None;
676        false
677    }
678
679    /// Order two positions (returns (earlier, later))
680    fn order_positions(&self, a: TextPosition, b: TextPosition) -> (TextPosition, TextPosition) {
681        if a.line < b.line || (a.line == b.line && a.column <= b.column) {
682            (a, b)
683        } else {
684            (b, a)
685        }
686    }
687
688    /// Move cursor left
689    pub fn move_left(&mut self, select: bool) {
690        if select && self.selection_start.is_none() {
691            self.selection_start = Some(self.cursor);
692        } else if !select {
693            if let Some(start) = self.selection_start {
694                let (from, _) = self.order_positions(start, self.cursor);
695                self.cursor = from;
696                self.selection_start = None;
697                return;
698            }
699        }
700
701        if self.cursor.column > 0 {
702            self.cursor.column -= 1;
703        } else if self.cursor.line > 0 {
704            self.cursor.line -= 1;
705            self.cursor.column = self.lines[self.cursor.line].chars().count();
706        }
707
708        if !select {
709            self.selection_start = None;
710        }
711    }
712
713    /// Move cursor right
714    pub fn move_right(&mut self, select: bool) {
715        if select && self.selection_start.is_none() {
716            self.selection_start = Some(self.cursor);
717        } else if !select {
718            if let Some(start) = self.selection_start {
719                let (_, to) = self.order_positions(start, self.cursor);
720                self.cursor = to;
721                self.selection_start = None;
722                return;
723            }
724        }
725
726        let line_len = self.lines[self.cursor.line].chars().count();
727        if self.cursor.column < line_len {
728            self.cursor.column += 1;
729        } else if self.cursor.line < self.lines.len() - 1 {
730            self.cursor.line += 1;
731            self.cursor.column = 0;
732        }
733
734        if !select {
735            self.selection_start = None;
736        }
737    }
738
739    /// Move cursor up (handles visual lines for wrapped text)
740    pub fn move_up(&mut self, select: bool) {
741        if select && self.selection_start.is_none() {
742            self.selection_start = Some(self.cursor);
743        } else if !select {
744            self.selection_start = None;
745        }
746
747        // If we have visual lines, use them for navigation
748        if !self.visual_lines.is_empty() && self.wrap_enabled {
749            let current_visual_idx = self.visual_line_for_cursor();
750            if current_visual_idx > 0 {
751                // Move to the visual line above
752                let prev_visual_idx = current_visual_idx - 1;
753                let prev_vl = &self.visual_lines[prev_visual_idx];
754
755                // Calculate cursor x position to maintain horizontal position
756                let cursor_x = self.cursor_x_in_visual_line();
757
758                // Find the column in the previous visual line that best matches our x position
759                let column = self.find_column_at_x(prev_visual_idx, cursor_x);
760
761                self.cursor.line = prev_vl.logical_line;
762                self.cursor.column = column;
763            }
764        } else {
765            // Fallback: simple logical line navigation
766            if self.cursor.line > 0 {
767                self.cursor.line -= 1;
768                let line_len = self.lines[self.cursor.line].chars().count();
769                self.cursor.column = self.cursor.column.min(line_len);
770            }
771        }
772    }
773
774    /// Move cursor down (handles visual lines for wrapped text)
775    pub fn move_down(&mut self, select: bool) {
776        if select && self.selection_start.is_none() {
777            self.selection_start = Some(self.cursor);
778        } else if !select {
779            self.selection_start = None;
780        }
781
782        // If we have visual lines, use them for navigation
783        if !self.visual_lines.is_empty() && self.wrap_enabled {
784            let current_visual_idx = self.visual_line_for_cursor();
785            if current_visual_idx < self.visual_lines.len() - 1 {
786                // Move to the visual line below
787                let next_visual_idx = current_visual_idx + 1;
788                let next_vl = &self.visual_lines[next_visual_idx];
789
790                // Calculate cursor x position to maintain horizontal position
791                let cursor_x = self.cursor_x_in_visual_line();
792
793                // Find the column in the next visual line that best matches our x position
794                let column = self.find_column_at_x(next_visual_idx, cursor_x);
795
796                self.cursor.line = next_vl.logical_line;
797                self.cursor.column = column;
798            }
799        } else {
800            // Fallback: simple logical line navigation
801            if self.cursor.line < self.lines.len() - 1 {
802                self.cursor.line += 1;
803                let line_len = self.lines[self.cursor.line].chars().count();
804                self.cursor.column = self.cursor.column.min(line_len);
805            }
806        }
807    }
808
809    /// Find the column in a visual line that best matches a given x position
810    fn find_column_at_x(&self, visual_line_idx: usize, target_x: f32) -> usize {
811        if visual_line_idx >= self.visual_lines.len() {
812            return 0;
813        }
814
815        let vl = &self.visual_lines[visual_line_idx];
816        if vl.text.is_empty() {
817            return vl.start_char;
818        }
819
820        let char_count = vl.text.chars().count();
821        let mut best_pos = 0;
822        let mut min_dist = f32::MAX;
823
824        // Find character position that best matches target_x
825        for i in 0..=char_count {
826            let prefix: String = vl.text.chars().take(i).collect();
827            let prefix_width = crate::text_measure::measure_text(&prefix, self.font_size).width;
828
829            let dist = (prefix_width - target_x).abs();
830            if dist < min_dist {
831                min_dist = dist;
832                best_pos = i;
833            }
834        }
835
836        vl.start_char + best_pos
837    }
838
839    /// Move to start of line
840    pub fn move_to_line_start(&mut self, select: bool) {
841        if select && self.selection_start.is_none() {
842            self.selection_start = Some(self.cursor);
843        } else if !select {
844            self.selection_start = None;
845        }
846        self.cursor.column = 0;
847    }
848
849    /// Move to end of line
850    pub fn move_to_line_end(&mut self, select: bool) {
851        if select && self.selection_start.is_none() {
852            self.selection_start = Some(self.cursor);
853        } else if !select {
854            self.selection_start = None;
855        }
856        self.cursor.column = self.lines[self.cursor.line].chars().count();
857    }
858
859    /// Move to start of text
860    pub fn move_to_start(&mut self, select: bool) {
861        if select && self.selection_start.is_none() {
862            self.selection_start = Some(self.cursor);
863        } else if !select {
864            self.selection_start = None;
865        }
866        self.cursor = TextPosition::new(0, 0);
867    }
868
869    /// Move to end of text
870    pub fn move_to_end(&mut self, select: bool) {
871        if select && self.selection_start.is_none() {
872            self.selection_start = Some(self.cursor);
873        } else if !select {
874            self.selection_start = None;
875        }
876        let last_line = self.lines.len().saturating_sub(1);
877        self.cursor = TextPosition::new(last_line, self.lines[last_line].chars().count());
878    }
879
880    /// Select all text
881    pub fn select_all(&mut self) {
882        self.selection_start = Some(TextPosition::new(0, 0));
883        let last_line = self.lines.len().saturating_sub(1);
884        self.cursor = TextPosition::new(last_line, self.lines[last_line].chars().count());
885    }
886
887    /// Get selected text
888    pub fn selected_text(&self) -> Option<String> {
889        self.selection_start.map(|start| {
890            let (from, to) = self.order_positions(start, self.cursor);
891
892            if from.line == to.line {
893                self.lines[from.line]
894                    .chars()
895                    .skip(from.column)
896                    .take(to.column - from.column)
897                    .collect()
898            } else {
899                let mut result = String::new();
900                result.extend(self.lines[from.line].chars().skip(from.column));
901
902                for line in &self.lines[from.line + 1..to.line] {
903                    result.push('\n');
904                    result.push_str(line);
905                }
906
907                if to.line > from.line {
908                    result.push('\n');
909                    result.extend(self.lines[to.line].chars().take(to.column));
910                }
911
912                result
913            }
914        })
915    }
916
917    /// Calculate the number of visual lines a text line takes when wrapped
918    ///
919    /// Returns 1 for short lines, more for lines that wrap.
920    fn visual_lines_for_text(text: &str, font_size: f32, available_width: f32) -> usize {
921        if text.is_empty() || available_width <= 0.0 {
922            return 1;
923        }
924
925        let metrics = crate::text_measure::measure_text(text, font_size);
926        if metrics.width <= available_width {
927            return 1;
928        }
929
930        // Estimate number of lines by dividing total width by available width
931        // Add 1 because integer division rounds down
932        ((metrics.width / available_width).ceil() as usize).max(1)
933    }
934
935    /// Compute visual lines for all text content
936    ///
937    /// This creates a list of VisualLine entries that map logical lines to
938    /// visual lines, accounting for word wrapping. Each VisualLine contains:
939    /// - The logical line index it belongs to
940    /// - Start and end character indices within that logical line
941    /// - The actual text content and its measured width
942    ///
943    /// Call this whenever:
944    /// - Text content changes
945    /// - Available width changes (e.g., container resize)
946    /// - Font size changes
947    pub fn compute_visual_lines(&mut self) {
948        self.visual_lines.clear();
949
950        let font_size = self.font_size;
951        let available_width = self.available_width;
952        let wrap_enabled = self.wrap_enabled;
953
954        for (logical_line_idx, line_text) in self.lines.iter().enumerate() {
955            if line_text.is_empty() {
956                // Empty line still takes up one visual line
957                self.visual_lines.push(VisualLine {
958                    logical_line: logical_line_idx,
959                    start_char: 0,
960                    end_char: 0,
961                    text: String::new(),
962                    width: 0.0,
963                });
964                continue;
965            }
966
967            if !wrap_enabled || available_width <= 0.0 {
968                // No wrapping - entire logical line is one visual line
969                let width = crate::text_measure::measure_text(line_text, font_size).width;
970                self.visual_lines.push(VisualLine {
971                    logical_line: logical_line_idx,
972                    start_char: 0,
973                    end_char: line_text.chars().count(),
974                    text: line_text.clone(),
975                    width,
976                });
977                continue;
978            }
979
980            // Wrapping enabled - split line into visual lines
981            // Use word-based wrapping: try to break at word boundaries
982            let chars: Vec<char> = line_text.chars().collect();
983            let char_count = chars.len();
984            let mut start_char = 0;
985
986            while start_char < char_count {
987                // Find how many characters fit in available_width
988                let mut end_char = start_char;
989                let mut last_word_break = start_char;
990                let mut current_width = 0.0;
991
992                // Scan forward to find break point
993                while end_char < char_count {
994                    // Check width up to this character (inclusive)
995                    let test_end = end_char + 1;
996                    let test_text: String = chars[start_char..test_end].iter().collect();
997                    let test_width = crate::text_measure::measure_text(&test_text, font_size).width;
998
999                    if test_width > available_width && end_char > start_char {
1000                        // This character would exceed width, break here
1001                        break;
1002                    }
1003
1004                    current_width = test_width;
1005                    end_char = test_end;
1006
1007                    // Track word boundary (space character)
1008                    if end_char < char_count && chars[end_char - 1].is_whitespace() {
1009                        last_word_break = end_char;
1010                    }
1011                }
1012
1013                // If we found a word break, use it (unless it's at the start)
1014                if last_word_break > start_char && end_char < char_count {
1015                    end_char = last_word_break;
1016                    let text: String = chars[start_char..end_char].iter().collect();
1017                    current_width = crate::text_measure::measure_text(&text, font_size).width;
1018                }
1019
1020                // Create visual line
1021                let text: String = chars[start_char..end_char].iter().collect();
1022                self.visual_lines.push(VisualLine {
1023                    logical_line: logical_line_idx,
1024                    start_char,
1025                    end_char,
1026                    text,
1027                    width: current_width,
1028                });
1029
1030                start_char = end_char;
1031            }
1032
1033            // If line ended exactly at boundary, we should have at least one visual line
1034            if self.visual_lines.last().map(|vl| vl.logical_line) != Some(logical_line_idx) {
1035                self.visual_lines.push(VisualLine {
1036                    logical_line: logical_line_idx,
1037                    start_char: char_count,
1038                    end_char: char_count,
1039                    text: String::new(),
1040                    width: 0.0,
1041                });
1042            }
1043        }
1044    }
1045
1046    /// Calculate cursor position from a known visual line index and x coordinate
1047    ///
1048    /// This is used when a line element's click handler has already determined
1049    /// which visual line was clicked. The x coordinate is relative to the line element.
1050    pub fn cursor_position_from_visual_line(&self, visual_line_idx: usize, x: f32) -> TextPosition {
1051        if visual_line_idx >= self.visual_lines.len() {
1052            return TextPosition::default();
1053        }
1054
1055        let vl = &self.visual_lines[visual_line_idx];
1056        let column = self.char_position_in_visual_line(visual_line_idx, x, self.font_size);
1057
1058        TextPosition::new(vl.logical_line, column)
1059    }
1060
1061    /// Get the visual line index for a given cursor position
1062    ///
1063    /// Returns the index into visual_lines where the cursor is located.
1064    pub fn visual_line_for_cursor(&self) -> usize {
1065        let cursor_line = self.cursor.line;
1066        let cursor_col = self.cursor.column;
1067
1068        for (idx, vl) in self.visual_lines.iter().enumerate() {
1069            if vl.logical_line == cursor_line {
1070                // Check if cursor is within this visual line's range
1071                if cursor_col >= vl.start_char && cursor_col <= vl.end_char {
1072                    return idx;
1073                }
1074                // If cursor is at the end of a wrapped line, it might be at start of next visual line
1075                if cursor_col == vl.end_char {
1076                    // Check if there's another visual line for same logical line
1077                    if idx + 1 < self.visual_lines.len()
1078                        && self.visual_lines[idx + 1].logical_line == cursor_line
1079                        && self.visual_lines[idx + 1].start_char == cursor_col
1080                    {
1081                        return idx + 1;
1082                    }
1083                    return idx;
1084                }
1085            }
1086        }
1087
1088        // Fallback: return last visual line
1089        self.visual_lines.len().saturating_sub(1)
1090    }
1091
1092    /// Get cursor X position within its visual line
1093    ///
1094    /// Returns the pixel offset from the left edge of the visual line to the cursor.
1095    pub fn cursor_x_in_visual_line(&self) -> f32 {
1096        let cursor_line = self.cursor.line;
1097        let cursor_col = self.cursor.column;
1098
1099        // Find the visual line containing the cursor
1100        for vl in &self.visual_lines {
1101            if vl.logical_line == cursor_line
1102                && cursor_col >= vl.start_char
1103                && cursor_col <= vl.end_char
1104            {
1105                // Measure text from start of visual line to cursor
1106                let local_col = cursor_col - vl.start_char;
1107                if local_col == 0 {
1108                    return 0.0;
1109                }
1110                let text_before: String = vl.text.chars().take(local_col).collect();
1111                return crate::text_measure::measure_text(&text_before, self.font_size).width;
1112            }
1113        }
1114
1115        0.0
1116    }
1117
1118    /// Get cursor position (x, visual_y) using computed visual lines
1119    ///
1120    /// Returns (cursor_x, cursor_visual_y) for positioning the cursor element.
1121    pub fn cursor_position_from_visual_lines(&self) -> (f32, f32) {
1122        let visual_line_idx = self.visual_line_for_cursor();
1123        let cursor_x = self.cursor_x_in_visual_line();
1124        let cursor_visual_y = visual_line_idx as f32 * self.line_height;
1125        (cursor_x, cursor_visual_y)
1126    }
1127
1128    /// Get total visual line count
1129    pub fn visual_line_count(&self) -> usize {
1130        if self.visual_lines.is_empty() {
1131            // Fallback when visual lines not computed yet
1132            self.lines.len()
1133        } else {
1134            self.visual_lines.len()
1135        }
1136    }
1137
1138    /// Get content height based on visual lines
1139    pub fn content_height_from_visual_lines(&self) -> f32 {
1140        self.visual_line_count() as f32 * self.line_height
1141    }
1142
1143    /// Calculate cursor Y position accounting for text wrapping
1144    ///
1145    /// Returns (cursor_y, content_height) where cursor_y is the visual Y position
1146    /// of the cursor line, and content_height is the total visual height.
1147    fn calculate_wrapped_positions(
1148        &self,
1149        font_size: f32,
1150        line_height: f32,
1151        available_width: f32,
1152        wrap_enabled: bool,
1153    ) -> (f32, f32) {
1154        if !wrap_enabled || available_width <= 0.0 {
1155            // No wrapping - simple calculation
1156            let cursor_y = self.cursor.line as f32 * line_height;
1157            let content_height = self.lines.len() as f32 * line_height;
1158            return (cursor_y, content_height);
1159        }
1160
1161        // With wrapping, count visual lines up to cursor line
1162        let mut visual_line_count = 0usize;
1163        let mut cursor_visual_y = 0.0f32;
1164
1165        for (idx, line) in self.lines.iter().enumerate() {
1166            let line_visual_count = Self::visual_lines_for_text(line, font_size, available_width);
1167
1168            if idx < self.cursor.line {
1169                visual_line_count += line_visual_count;
1170            } else if idx == self.cursor.line {
1171                cursor_visual_y = visual_line_count as f32 * line_height;
1172                // For the cursor's line, we need to account for wrapped position
1173                // within that line based on cursor column position
1174                if line_visual_count > 1 && !line.is_empty() {
1175                    let text_before: String = line.chars().take(self.cursor.column).collect();
1176                    let prefix_width =
1177                        crate::text_measure::measure_text(&text_before, font_size).width;
1178                    let lines_into = (prefix_width / available_width).floor() as usize;
1179                    cursor_visual_y += lines_into as f32 * line_height;
1180                }
1181                visual_line_count += line_visual_count;
1182            } else {
1183                visual_line_count += line_visual_count;
1184            }
1185        }
1186
1187        let content_height = visual_line_count as f32 * line_height;
1188        (cursor_visual_y, content_height)
1189    }
1190
1191    /// Ensure the cursor is visible by adjusting scroll offset if needed
1192    ///
1193    /// This should be called after any cursor movement to auto-scroll
1194    /// when the cursor moves outside the visible area.
1195    /// Uses cached wrap settings from the state.
1196    pub fn ensure_cursor_visible(&mut self, line_height: f32, viewport_height: f32) {
1197        // Use visual lines for accurate cursor position if available
1198        let (cursor_y, content_height) = if !self.visual_lines.is_empty() {
1199            let (_, cursor_y) = self.cursor_position_from_visual_lines();
1200            let content_height = self.content_height_from_visual_lines();
1201            (cursor_y, content_height)
1202        } else {
1203            self.calculate_wrapped_positions(
1204                self.font_size,
1205                line_height,
1206                self.available_width,
1207                self.wrap_enabled,
1208            )
1209        };
1210        let cursor_bottom = cursor_y + line_height;
1211
1212        // Get current scroll offset from physics (offset_y is negative when scrolled down)
1213        let mut physics = self.scroll_physics.lock().unwrap();
1214        let current_offset = -physics.offset_y; // Convert to positive scroll offset
1215
1216        // If cursor is above visible area, scroll up
1217        let mut new_offset = current_offset;
1218        if cursor_y < current_offset {
1219            new_offset = cursor_y;
1220        }
1221
1222        // If cursor is below visible area, scroll down
1223        if cursor_bottom > current_offset + viewport_height {
1224            new_offset = cursor_bottom - viewport_height;
1225        }
1226
1227        // Clamp scroll offset to valid range
1228        let max_scroll = (content_height - viewport_height).max(0.0);
1229        new_offset = new_offset.clamp(0.0, max_scroll);
1230
1231        // Update physics offset (negative for scroll physics convention)
1232        physics.offset_y = -new_offset;
1233    }
1234
1235    /// Get current scroll offset (positive value, 0 = top)
1236    pub fn scroll_offset(&self) -> f32 {
1237        -self.scroll_physics.lock().unwrap().offset_y
1238    }
1239
1240    /// Calculate cursor position from click coordinates
1241    ///
1242    /// Takes x/y coordinates relative to the text content area (after padding).
1243    /// Returns the TextPosition (line, column) for the clicked location.
1244    pub fn cursor_position_from_xy(&self, x: f32, y: f32) -> TextPosition {
1245        if self.lines.is_empty() {
1246            return TextPosition::default();
1247        }
1248
1249        let line_height = self.line_height;
1250        let font_size = self.font_size;
1251        let scroll_offset = self.scroll_offset();
1252
1253        // Account for scroll offset - y is in viewport space
1254        let text_y = y + scroll_offset;
1255
1256        // Find the visual line index that was clicked
1257        let visual_line_idx = (text_y / line_height).floor().max(0.0) as usize;
1258
1259        // Use computed visual lines if available for accurate positioning
1260        if !self.visual_lines.is_empty() {
1261            // Clamp to valid range
1262            let visual_line_idx = visual_line_idx.min(self.visual_lines.len().saturating_sub(1));
1263            let vl = &self.visual_lines[visual_line_idx];
1264
1265            // Find character position within this visual line
1266            let column = self.char_position_in_visual_line(visual_line_idx, x, font_size);
1267
1268            return TextPosition::new(vl.logical_line, column);
1269        }
1270
1271        // Fallback: no visual lines computed
1272        let logical_line = visual_line_idx.min(self.lines.len().saturating_sub(1));
1273        let column = self.char_position_from_x(logical_line, x, font_size);
1274        TextPosition::new(logical_line, column)
1275    }
1276
1277    /// Find character position from x coordinate within a visual line
1278    fn char_position_in_visual_line(
1279        &self,
1280        visual_line_idx: usize,
1281        x: f32,
1282        font_size: f32,
1283    ) -> usize {
1284        if visual_line_idx >= self.visual_lines.len() {
1285            return 0;
1286        }
1287
1288        let vl = &self.visual_lines[visual_line_idx];
1289        if vl.text.is_empty() {
1290            return vl.start_char;
1291        }
1292
1293        let char_count = vl.text.chars().count();
1294        let mut best_pos = 0;
1295        let mut min_dist = f32::MAX;
1296
1297        // Check position before each character and after the last within this visual line
1298        for i in 0..=char_count {
1299            let prefix: String = vl.text.chars().take(i).collect();
1300            let prefix_width = crate::text_measure::measure_text(&prefix, font_size).width;
1301
1302            let dist = (prefix_width - x).abs();
1303            if dist < min_dist {
1304                min_dist = dist;
1305                best_pos = i;
1306            }
1307        }
1308
1309        // Convert local position to absolute position within logical line
1310        vl.start_char + best_pos
1311    }
1312
1313    /// Find character position from x coordinate within a line
1314    fn char_position_from_x(&self, line_index: usize, x: f32, font_size: f32) -> usize {
1315        if line_index >= self.lines.len() {
1316            return 0;
1317        }
1318
1319        let line = &self.lines[line_index];
1320        if line.is_empty() {
1321            return 0;
1322        }
1323
1324        let char_count = line.chars().count();
1325        let mut best_pos = 0;
1326        let mut min_dist = f32::MAX;
1327
1328        // Check position before each character and after the last
1329        for i in 0..=char_count {
1330            let prefix: String = line.chars().take(i).collect();
1331            let prefix_width = crate::text_measure::measure_text(&prefix, font_size).width;
1332
1333            let dist = (prefix_width - x).abs();
1334            if dist < min_dist {
1335                min_dist = dist;
1336                best_pos = i;
1337            }
1338        }
1339
1340        best_pos
1341    }
1342}
1343
1344/// Convert character index to byte index
1345fn char_to_byte_pos(line: &str, char_pos: usize) -> usize {
1346    line.char_indices()
1347        .nth(char_pos)
1348        .map(|(i, _)| i)
1349        .unwrap_or(line.len())
1350}
1351
1352/// Shared text area state handle
1353pub type SharedTextAreaState = Arc<Mutex<TextAreaState>>;
1354
1355/// Create a shared text area state
1356pub fn text_area_state() -> SharedTextAreaState {
1357    Arc::new(Mutex::new(TextAreaState::new()))
1358}
1359
1360/// Create a shared text area state with placeholder
1361pub fn text_area_state_with_placeholder(placeholder: impl Into<String>) -> SharedTextAreaState {
1362    Arc::new(Mutex::new(TextAreaState::with_placeholder(placeholder)))
1363}
1364
1365/// Ready-to-use text area element
1366///
1367/// Uses FSM-driven state management via `Stateful<TextFieldState>` for visual states
1368/// while maintaining separate text content state for editing.
1369///
1370/// Usage: `text_area(&state).rows(4).w(400.0).rounded(12.0)`
1371pub struct TextArea {
1372    /// Inner Stateful element for FSM-driven visual states
1373    inner: Stateful<TextFieldState>,
1374    /// Text area state (content, cursor, etc.)
1375    state: SharedTextAreaState,
1376    /// Text area configuration
1377    config: Arc<Mutex<TextAreaConfig>>,
1378}
1379
1380impl TextArea {
1381    /// Create a new text area with shared state
1382    pub fn new(state: &SharedTextAreaState) -> Self {
1383        let config = Arc::new(Mutex::new(TextAreaConfig::default()));
1384        let cfg = config.lock().unwrap();
1385        let default_width = cfg.effective_width();
1386        let default_height = cfg.effective_height();
1387        drop(cfg);
1388
1389        // Get initial visual state and existing stateful_state from data
1390        let (initial_visual, existing_stateful_state) = {
1391            let d = state.lock().unwrap();
1392            (d.visual, d.stateful_state.clone())
1393        };
1394
1395        // Reuse existing stateful_state if available, otherwise create new one
1396        // This ensures state persists across rebuilds (e.g., window resize)
1397        let shared_state: SharedState<TextFieldState> =
1398            existing_stateful_state.unwrap_or_else(|| {
1399                let new_state = Arc::new(Mutex::new(StatefulInner::new(initial_visual)));
1400                // Store reference in TextAreaState for triggering refreshes
1401                if let Ok(mut d) = state.lock() {
1402                    d.stateful_state = Some(Arc::clone(&new_state));
1403                }
1404                new_state
1405            });
1406
1407        // Clear stale node_id from previous tree builds
1408        // During full rebuild (e.g., window resize), the old node_id points to
1409        // nodes in the old tree. Clearing it ensures the new node_id gets assigned
1410        // when this element is added to the new tree.
1411        {
1412            let mut shared = shared_state.lock().unwrap();
1413            shared.node_id = None;
1414        }
1415
1416        // Create inner Stateful with text area event handlers
1417        let mut inner = Self::create_inner_with_handlers(
1418            Arc::clone(&shared_state),
1419            Arc::clone(state),
1420            Arc::clone(&config),
1421        );
1422
1423        // Set default dimensions from config
1424        // HTML-like flex behavior:
1425        // 1. min-width: 0 - allows shrinking below content size in flex containers
1426        // 2. flex-shrink: 1 (default) - allows shrinking when container is constrained
1427        // 3. The height is always set explicitly (based on rows or config.height)
1428        // Note: Don't use overflow_clip() here - the inner Scroll handles clipping.
1429        // Using overflow_clip on the outer container with rounded corners causes
1430        // the clip to interfere with border rendering at the corners.
1431        inner = inner.w(default_width).h(default_height).min_w(0.0);
1432
1433        // Register callback immediately so it's available for incremental diff
1434        // The diff system calls children_builders() before build(), so the callback
1435        // must be registered here, not in build()
1436        {
1437            let config_for_callback = Arc::clone(&config);
1438            let data_for_callback = Arc::clone(state);
1439            let shared_state_for_callback = Arc::clone(state);
1440            let mut shared = shared_state.lock().unwrap();
1441
1442            shared.state_callback = Some(Arc::new(
1443                move |visual: &TextFieldState, container: &mut Div| {
1444                    let mut cfg = config_for_callback.lock().unwrap().clone();
1445                    let mut data_guard = data_for_callback.lock().unwrap();
1446
1447                    // Apply CSS stylesheet overrides if element has an ID
1448                    let css_outline = if let Some(ref element_id) = data_guard.css_element_id {
1449                        if let Some(stylesheet) = active_stylesheet() {
1450                            apply_css_overrides_textarea(&mut cfg, &stylesheet, element_id, visual);
1451                            extract_outline_from_textarea_stylesheet(
1452                                &stylesheet,
1453                                element_id,
1454                                visual,
1455                            )
1456                        } else {
1457                            None
1458                        }
1459                    } else {
1460                        None
1461                    };
1462
1463                    // Sync visual state to data so it matches the FSM
1464                    data_guard.visual = *visual;
1465
1466                    // Update cached scroll dimensions from config
1467                    let line_height = cfg.font_size * cfg.line_height;
1468                    let viewport_height =
1469                        cfg.effective_height() - cfg.padding_y * 2.0 - cfg.border_width * 2.0;
1470                    let available_width =
1471                        cfg.effective_width() - cfg.padding_x * 2.0 - cfg.border_width * 2.0;
1472                    data_guard.line_height = line_height;
1473                    data_guard.viewport_height = viewport_height;
1474                    data_guard.font_size = cfg.font_size;
1475                    data_guard.available_width = available_width;
1476                    data_guard.wrap_enabled = cfg.wrap;
1477
1478                    // Recompute visual lines for proper cursor tracking with wrapped text
1479                    data_guard.compute_visual_lines();
1480
1481                    // Determine colors based on visual state
1482                    let (bg, border) = match visual {
1483                        TextFieldState::Focused | TextFieldState::FocusedHovered => {
1484                            (cfg.focused_bg_color, cfg.focused_border_color)
1485                        }
1486                        TextFieldState::Hovered => (cfg.hover_bg_color, cfg.hover_border_color),
1487                        TextFieldState::Disabled => (
1488                            Color::rgba(0.12, 0.12, 0.15, 0.5),
1489                            Color::rgba(0.25, 0.25, 0.3, 0.5),
1490                        ),
1491                        _ => (cfg.bg_color, cfg.border_color),
1492                    };
1493
1494                    // Apply visual styling directly to the container (preserves fixed dimensions)
1495                    container.set_bg(bg);
1496                    container.set_border(cfg.border_width, border);
1497                    container.set_rounded(cfg.corner_radius);
1498
1499                    // Apply CSS outline if specified
1500                    if let Some((width, color, offset)) = css_outline {
1501                        container.outline_width = width;
1502                        container.outline_color = Some(color);
1503                        container.outline_offset = offset;
1504                    }
1505
1506                    // Build content wrapper with explicit padding spacers (like TextInput)
1507                    let content = TextArea::build_content(
1508                        *visual,
1509                        &data_guard,
1510                        &cfg,
1511                        Arc::clone(&shared_state_for_callback),
1512                    );
1513                    container.set_child(content);
1514                },
1515            ));
1516
1517            shared.needs_visual_update = true;
1518        }
1519
1520        // Ensure state handlers (hover/press) are registered immediately
1521        // so they're available for incremental diff
1522        inner.ensure_state_handlers_registered();
1523
1524        let textarea = Self {
1525            inner,
1526            state: Arc::clone(state),
1527            config,
1528        };
1529
1530        // Initialize scroll dimensions from default config
1531        textarea.update_scroll_dimensions();
1532
1533        textarea
1534    }
1535
1536    /// Create the inner Stateful element with all event handlers registered
1537    fn create_inner_with_handlers(
1538        shared_state: SharedState<TextFieldState>,
1539        data: SharedTextAreaState,
1540        config: Arc<Mutex<TextAreaConfig>>,
1541    ) -> Stateful<TextFieldState> {
1542        use blinc_core::events::event_types;
1543
1544        let data_for_click = Arc::clone(&data);
1545        let data_for_text = Arc::clone(&data);
1546        let data_for_key = Arc::clone(&data);
1547        let config_for_click = Arc::clone(&config);
1548        let shared_for_click = Arc::clone(&shared_state);
1549        let shared_for_text = Arc::clone(&shared_state);
1550        let shared_for_key = Arc::clone(&shared_state);
1551
1552        Stateful::with_shared_state(shared_state)
1553            // Handle mouse down to focus and position cursor
1554            .on_mouse_down(move |ctx| {
1555                // First, forcibly blur any previously focused text input/area
1556                set_focused_text_area(&data_for_click);
1557
1558                // Get click position from context for cursor positioning
1559                let click_x = ctx.local_x;
1560                let click_y = ctx.local_y;
1561
1562                // Get config values for visual line computation
1563                // Note: We DON'T use padding offsets here because local_x/local_y from the event
1564                // are relative to the innermost hit element (the text content), not the outer container
1565                let cfg = config_for_click.lock().unwrap();
1566                let font_size = cfg.font_size;
1567                let line_height = cfg.font_size * cfg.line_height;
1568                let available_width =
1569                    cfg.effective_width() - cfg.padding_x * 2.0 - cfg.border_width * 2.0;
1570                let wrap_enabled = cfg.wrap;
1571                drop(cfg);
1572
1573                let needs_refresh = {
1574                    let mut d = match data_for_click.lock() {
1575                        Ok(d) => d,
1576                        Err(_) => return,
1577                    };
1578
1579                    if d.disabled {
1580                        return;
1581                    }
1582
1583                    // Set focus via FSM transition
1584                    {
1585                        let mut shared = shared_for_click.lock().unwrap();
1586                        if !shared.state.is_focused() {
1587                            // Transition to focused state
1588                            if let Some(new_state) = shared
1589                                .state
1590                                .on_event(event_types::POINTER_DOWN)
1591                                .or_else(|| shared.state.on_event(event_types::FOCUS))
1592                            {
1593                                shared.state = new_state;
1594                                shared.needs_visual_update = true;
1595                            }
1596                        }
1597                    }
1598
1599                    // Update data state
1600                    let was_focused = d.visual.is_focused();
1601                    if !was_focused {
1602                        d.visual = TextFieldState::Focused;
1603                        increment_focus_count();
1604                        request_continuous_redraw_pub();
1605                    }
1606
1607                    // Update cached config values for visual line computation
1608                    d.font_size = font_size;
1609                    d.line_height = line_height;
1610                    d.available_width = available_width;
1611                    d.wrap_enabled = wrap_enabled;
1612
1613                    // Ensure visual lines are computed before positioning cursor
1614                    // This is needed because click handler may run before the callback
1615                    // that computes visual lines during rebuild
1616                    if d.visual_lines.is_empty() || d.wrap_enabled {
1617                        d.compute_visual_lines();
1618                    }
1619
1620                    // Position cursor at click location
1621                    // Use clicked_visual_line if set by a line element's click handler,
1622                    // otherwise fall back to y-coordinate calculation
1623                    let text_x = click_x.max(0.0);
1624
1625                    let new_pos = if let Some(visual_line_idx) = d.clicked_visual_line.take() {
1626                        // Line element told us which visual line was clicked
1627                        // Use that for accurate positioning
1628                        d.cursor_position_from_visual_line(visual_line_idx, text_x)
1629                    } else {
1630                        // Fallback: try to compute from y coordinate
1631                        let text_y = click_y.max(0.0);
1632                        d.cursor_position_from_xy(text_x, text_y)
1633                    };
1634                    d.cursor = new_pos;
1635                    d.selection_start = None; // Clear any selection
1636                    d.reset_cursor_blink();
1637
1638                    true // needs refresh
1639                }; // Lock released here
1640
1641                // Trigger incremental refresh AFTER releasing the data lock
1642                if needs_refresh {
1643                    refresh_stateful(&shared_for_click);
1644                }
1645            })
1646            // Handle text input
1647            .on_event(event_types::TEXT_INPUT, move |ctx| {
1648                let (needs_refresh, change_signal) = {
1649                    let mut d = match data_for_text.lock() {
1650                        Ok(d) => d,
1651                        Err(_) => return,
1652                    };
1653
1654                    if d.disabled || !d.visual.is_focused() {
1655                        return;
1656                    }
1657
1658                    if let Some(c) = ctx.key_char {
1659                        d.insert(&c.to_string());
1660                        d.reset_cursor_blink();
1661                        // Recompute visual lines after text change
1662                        d.compute_visual_lines();
1663                        // Ensure cursor is visible after text insertion (use cached values)
1664                        let line_height = d.line_height;
1665                        let viewport_height = d.viewport_height;
1666                        d.ensure_cursor_visible(line_height, viewport_height);
1667                        tracing::debug!("TextArea received char: {:?}, value: {}", c, d.value());
1668                        (true, d.change_signal_id)
1669                    } else {
1670                        (false, None)
1671                    }
1672                }; // Lock released here
1673
1674                // Trigger incremental refresh AFTER releasing the data lock
1675                if needs_refresh {
1676                    refresh_stateful(&shared_for_text);
1677                }
1678
1679                // Notify external listeners of text change
1680                if let Some(signal_id) = change_signal {
1681                    crate::stateful::check_stateful_deps(&[signal_id]);
1682                }
1683            })
1684            // Handle key down for navigation and deletion
1685            .on_key_down(move |ctx| {
1686                let needs_refresh = {
1687                    let mut d = match data_for_key.lock() {
1688                        Ok(d) => d,
1689                        Err(_) => return,
1690                    };
1691
1692                    if d.disabled || !d.visual.is_focused() {
1693                        return;
1694                    }
1695
1696                    let mut cursor_changed = true;
1697                    let mut should_blur = false;
1698                    let mut text_changed = false;
1699                    match ctx.key_code {
1700                        8 => {
1701                            // Backspace
1702                            d.delete_backward();
1703                            text_changed = true;
1704                            tracing::debug!("TextArea backspace, value: {}", d.value());
1705                        }
1706                        127 => {
1707                            // Delete
1708                            d.delete_forward();
1709                            text_changed = true;
1710                        }
1711                        13 => {
1712                            // Enter - insert newline
1713                            d.insert_newline();
1714                            text_changed = true;
1715                            tracing::debug!("TextArea newline, lines: {}", d.line_count());
1716                        }
1717                        37 => {
1718                            // Left arrow
1719                            d.move_left(ctx.shift);
1720                        }
1721                        39 => {
1722                            // Right arrow
1723                            d.move_right(ctx.shift);
1724                        }
1725                        38 => {
1726                            // Up arrow
1727                            d.move_up(ctx.shift);
1728                        }
1729                        40 => {
1730                            // Down arrow
1731                            d.move_down(ctx.shift);
1732                        }
1733                        36 => {
1734                            // Home
1735                            d.move_to_line_start(ctx.shift);
1736                        }
1737                        35 => {
1738                            // End
1739                            d.move_to_line_end(ctx.shift);
1740                        }
1741                        27 => {
1742                            // Escape - blur the textarea
1743                            should_blur = true;
1744                            cursor_changed = true;
1745                        }
1746                        _ => {
1747                            cursor_changed = false;
1748                        }
1749                    }
1750
1751                    // Recompute visual lines after text changes
1752                    if text_changed {
1753                        d.compute_visual_lines();
1754                    }
1755
1756                    if cursor_changed && !should_blur {
1757                        d.reset_cursor_blink();
1758                        // Ensure cursor is visible (auto-scroll if needed, use cached values)
1759                        let line_height = d.line_height;
1760                        let viewport_height = d.viewport_height;
1761                        d.ensure_cursor_visible(line_height, viewport_height);
1762                    }
1763
1764                    // Get change signal ID if text changed
1765                    let change_signal = if text_changed {
1766                        d.change_signal_id
1767                    } else {
1768                        None
1769                    };
1770
1771                    (cursor_changed, should_blur, change_signal)
1772                }; // Lock released here
1773
1774                // Handle blur (Escape key)
1775                if needs_refresh.1 {
1776                    crate::widgets::text_input::blur_all_text_inputs();
1777                } else if needs_refresh.0 {
1778                    // Trigger incremental refresh AFTER releasing the data lock
1779                    refresh_stateful(&shared_for_key);
1780                }
1781
1782                // Notify external listeners of text change
1783                if let Some(signal_id) = needs_refresh.2 {
1784                    crate::stateful::check_stateful_deps(&[signal_id]);
1785                }
1786            })
1787            // Set text cursor (I-beam) for text area
1788            .cursor_text()
1789        // Note: Scroll events are handled by the scroll() widget inside build_content
1790    }
1791
1792    /// Build the content div based on current visual state and data
1793    /// Returns a Div with explicit padding spacers (like TextInput) for proper
1794    /// visual separation from rounded corners.
1795    fn build_content(
1796        visual: TextFieldState,
1797        data: &TextAreaState,
1798        config: &TextAreaConfig,
1799        shared_state: SharedTextAreaState,
1800    ) -> Div {
1801        // Note: Visual styling (bg, border, rounded) is now applied directly to the
1802        // container in the callback via set_* methods, not here.
1803
1804        let text_color = if data.is_empty() {
1805            config.placeholder_color
1806        } else if data.disabled {
1807            Color::rgba(0.4, 0.4, 0.4, 1.0)
1808        } else {
1809            config.text_color
1810        };
1811
1812        // Check if cursor should be shown (focused state)
1813        let is_focused = visual.is_focused();
1814        let cursor_color = config.cursor_color;
1815
1816        // Cursor dimensions
1817        let cursor_height = config.font_size * 1.2;
1818        let line_height = config.font_size * config.line_height;
1819
1820        // Calculate available width for text (for wrap calculations)
1821        let text_area_width =
1822            config.effective_width() - config.padding_x * 2.0 - config.border_width * 2.0;
1823
1824        // Use visual lines for cursor positioning (computed in callback before build_content)
1825        // This provides accurate cursor tracking for wrapped text
1826        let (cursor_x, cursor_visual_y) = if !data.visual_lines.is_empty() {
1827            data.cursor_position_from_visual_lines()
1828        } else {
1829            // Fallback: simple calculation when visual lines not yet computed
1830            let cursor_line = data.cursor.line;
1831            let cursor_col = data.cursor.column;
1832            let cursor_x = if cursor_col > 0 && cursor_line < data.lines.len() {
1833                let line_text = &data.lines[cursor_line];
1834                let text_before: String = line_text.chars().take(cursor_col).collect();
1835                crate::text_measure::measure_text(&text_before, config.font_size).width
1836            } else {
1837                0.0
1838            };
1839            let cursor_y = cursor_line as f32 * line_height;
1840            (cursor_x, cursor_y)
1841        };
1842
1843        // Clone the cursor state for the canvas callback
1844        let cursor_state_for_canvas = Arc::clone(&data.cursor_state);
1845
1846        // Build cursor canvas element (if focused)
1847        // The cursor is positioned inside the scroll content so it scrolls with text
1848        let cursor_canvas_opt = if is_focused {
1849            // Cursor top is based on visual line position plus vertical centering within line
1850            // Shift cursor UP - fonts have descender space at bottom which pushes visible text upward
1851            let descender_offset = config.font_size * 0.1;
1852            let cursor_top =
1853                cursor_visual_y + (line_height - cursor_height) / 2.0 - descender_offset;
1854            let cursor_left = cursor_x;
1855
1856            {
1857                if let Ok(mut cs) = cursor_state_for_canvas.lock() {
1858                    cs.visible = true;
1859                    cs.color = cursor_color;
1860                    cs.x = cursor_x;
1861                    cs.animation = CursorAnimation::SmoothFade;
1862                }
1863            }
1864
1865            let cursor_state_clone = Arc::clone(&cursor_state_for_canvas);
1866            let cursor_canvas = canvas(
1867                move |ctx: &mut dyn blinc_core::DrawContext,
1868                      bounds: crate::canvas::CanvasBounds| {
1869                    let cs = cursor_state_clone.lock().unwrap();
1870
1871                    if !cs.visible {
1872                        return;
1873                    }
1874
1875                    let opacity = cs.current_opacity();
1876                    if opacity < 0.01 {
1877                        return;
1878                    }
1879
1880                    let color = blinc_core::Color::rgba(
1881                        cs.color.r,
1882                        cs.color.g,
1883                        cs.color.b,
1884                        cs.color.a * opacity,
1885                    );
1886
1887                    ctx.fill_rect(
1888                        blinc_core::Rect::new(0.0, 0.0, cs.width, bounds.height),
1889                        blinc_core::CornerRadius::default(),
1890                        blinc_core::Brush::Solid(color),
1891                    );
1892                },
1893            )
1894            .absolute()
1895            .left(cursor_left)
1896            .top(cursor_top)
1897            .w(2.0)
1898            .h(cursor_height);
1899
1900            Some(cursor_canvas)
1901        } else {
1902            if let Ok(mut cs) = cursor_state_for_canvas.lock() {
1903                cs.visible = false;
1904            }
1905            None
1906        };
1907
1908        // Build text content - left-aligned column of text lines
1909        // Use relative positioning to allow cursor absolute positioning within
1910        // Note: Don't use w_full() here - each line has explicit width, and w_full would
1911        // cause content to extend into the rounded corner areas of the outer container
1912        // Use overflow_visible to prevent clipping cursor when it shifts up
1913        let mut text_content = div()
1914            .flex_col()
1915            .justify_start()
1916            .items_start()
1917            .relative()
1918            .overflow_visible();
1919
1920        if data.is_empty() {
1921            // Use state's placeholder if available, otherwise fall back to config
1922            let placeholder = if !data.placeholder.is_empty() {
1923                &data.placeholder
1924            } else {
1925                &config.placeholder
1926            };
1927
1928            // Placeholder always uses no_wrap for consistent appearance
1929            text_content = text_content.child(
1930                div()
1931                    .h(line_height)
1932                    .flex_row()
1933                    .items_center()
1934                    .w(text_area_width)
1935                    .child(
1936                        text(placeholder)
1937                            .size(config.font_size)
1938                            .color(text_color)
1939                            .text_left()
1940                            .no_wrap(),
1941                    ),
1942            );
1943        } else if config.wrap && !data.visual_lines.is_empty() {
1944            // Wrapping mode with computed visual lines
1945            // Render each visual line segment for precise cursor alignment
1946            // Each line has a click handler that stores its visual line index
1947            for (visual_line_idx, vl) in data.visual_lines.iter().enumerate() {
1948                let line_text = if vl.text.is_empty() {
1949                    " "
1950                } else {
1951                    vl.text.as_str()
1952                };
1953                let state_for_line = Arc::clone(&shared_state);
1954
1955                // Each visual line has fixed height and a click handler
1956                text_content = text_content.child(
1957                    div()
1958                        .h(line_height)
1959                        .w(text_area_width)
1960                        .flex_row()
1961                        .items_center()
1962                        .on_mouse_down(move |_ctx| {
1963                            // Store which visual line was clicked so the main handler knows
1964                            if let Ok(mut state) = state_for_line.lock() {
1965                                state.clicked_visual_line = Some(visual_line_idx);
1966                            }
1967                        })
1968                        .child(
1969                            text(line_text)
1970                                .size(config.font_size)
1971                                .color(text_color)
1972                                .text_left()
1973                                .no_wrap(), // Visual lines are pre-wrapped, don't wrap again
1974                        ),
1975                );
1976            }
1977        } else if config.wrap {
1978            // Wrapping mode fallback: use natural text wrapping
1979            // This path is used when visual lines not yet computed
1980            // In this mode, line_idx corresponds to logical line (visual line not computed)
1981            for (line_idx, line) in data.lines.iter().enumerate() {
1982                let line_text = if line.is_empty() { " " } else { line.as_str() };
1983                let state_for_line = Arc::clone(&shared_state);
1984
1985                // Don't use fixed height - let it grow based on wrapped content
1986                // Use min_h to ensure empty/short lines still have proper height
1987                text_content = text_content.child(
1988                    div()
1989                        .min_h(line_height)
1990                        .w(text_area_width)
1991                        .on_mouse_down(move |_ctx| {
1992                            // Store line index (logical line in fallback mode)
1993                            if let Ok(mut state) = state_for_line.lock() {
1994                                state.clicked_visual_line = Some(line_idx);
1995                            }
1996                        })
1997                        .child(
1998                            text(line_text)
1999                                .size(config.font_size)
2000                                .color(text_color)
2001                                .text_left(),
2002                            // No .no_wrap() - text wraps at container width
2003                        ),
2004                );
2005            }
2006        } else {
2007            // No-wrap mode: each line stays on single line, horizontally scrollable
2008            // In this mode, line_idx corresponds to both logical and visual line
2009            for (line_idx, line) in data.lines.iter().enumerate() {
2010                let line_text = if line.is_empty() { " " } else { line.as_str() };
2011                let state_for_line = Arc::clone(&shared_state);
2012
2013                text_content = text_content.child(
2014                    div()
2015                        .h(line_height)
2016                        .flex_row()
2017                        .items_center()
2018                        .on_mouse_down(move |_ctx| {
2019                            // Store line index
2020                            if let Ok(mut state) = state_for_line.lock() {
2021                                state.clicked_visual_line = Some(line_idx);
2022                            }
2023                        })
2024                        .child(
2025                            text(line_text)
2026                                .size(config.font_size)
2027                                .color(text_color)
2028                                .text_left()
2029                                .no_wrap(),
2030                        ),
2031                );
2032            }
2033        }
2034
2035        // Add cursor inside text_content so it scrolls with the text
2036        if let Some(cursor) = cursor_canvas_opt {
2037            text_content = text_content.child(cursor);
2038        }
2039
2040        // Build wrapper with explicit padding spacers (like TextInput)
2041        // This ensures proper visual separation from rounded corners
2042        let padding_x = config.padding_x;
2043        let padding_y = config.padding_y;
2044
2045        // Wrap text content in scroll container with shared physics
2046        // This provides proper scroll handling and clipping
2047        // TextArea scroll doesn't use bounce animation - just hard stops at edges
2048        // Note: Don't add rounded() to scroll - the outer container handles visual rounding
2049        let scrollable_content = Scroll::with_physics(Arc::clone(&data.scroll_physics))
2050            .direction(ScrollDirection::Vertical)
2051            .no_bounce()
2052            .flex_grow() // Take remaining space
2053            .child(text_content);
2054
2055        // Main content wrapper - uses explicit sizing to ensure proper intrinsic dimensions
2056        // Using flex_grow/w_full causes issues with w_fit() ancestors because
2057        // Percent(1.0) doesn't resolve correctly when ancestors don't have fixed sizes.
2058        // Structure matches TextInput: outer styled container -> padding spacers -> clip container
2059        let content_width = config.effective_width();
2060        let content_height = config.effective_height();
2061        let inner_height = content_height - padding_y * 2.0;
2062
2063        div()
2064            .flex_col()
2065            .w(content_width)
2066            .h(content_height)
2067            // Top padding spacer
2068            .child(div().h(padding_y).w(content_width))
2069            // Middle row with left/right padding and scroll content
2070            .child(
2071                div()
2072                    .flex_row()
2073                    .h(inner_height)
2074                    .w(content_width)
2075                    // Left padding spacer
2076                    .child(div().w(padding_x).h(inner_height))
2077                    // Scroll content in the middle (no rounded corners on scroll itself)
2078                    .child(scrollable_content)
2079                    // Right padding spacer
2080                    .child(div().w(padding_x).h(inner_height)),
2081            )
2082            // Bottom padding spacer
2083            .child(div().h(padding_y).w(content_width))
2084    }
2085
2086    /// Set placeholder text
2087    pub fn placeholder(mut self, text: impl Into<String>) -> Self {
2088        let placeholder = text.into();
2089        self.config.lock().unwrap().placeholder = placeholder.clone();
2090        if let Ok(mut s) = self.state.lock() {
2091            s.placeholder = placeholder;
2092        }
2093        self
2094    }
2095
2096    /// Update cached scroll dimensions from config
2097    /// This must be called whenever config values that affect scroll calculation change
2098    fn update_scroll_dimensions(&self) {
2099        let cfg = self.config.lock().unwrap();
2100        let line_height = cfg.font_size * cfg.line_height;
2101        let viewport_height = cfg.effective_height() - cfg.padding_y * 2.0 - cfg.border_width * 2.0;
2102        let viewport_width = cfg.effective_width() - cfg.padding_x * 2.0 - cfg.border_width * 2.0;
2103        drop(cfg);
2104
2105        if let Ok(mut s) = self.state.lock() {
2106            s.line_height = line_height;
2107            s.viewport_height = viewport_height;
2108            // Update scroll physics viewport dimensions
2109            if let Ok(mut physics) = s.scroll_physics.lock() {
2110                physics.viewport_height = viewport_height;
2111                physics.viewport_width = viewport_width;
2112            }
2113        }
2114    }
2115
2116    /// Set number of visible rows (like HTML textarea rows attribute)
2117    pub fn rows(mut self, rows: usize) -> Self {
2118        let height = {
2119            let mut cfg = self.config.lock().unwrap();
2120            cfg.rows = Some(rows);
2121            cfg.effective_height()
2122        };
2123        self.inner = std::mem::take(&mut self.inner).h(height);
2124        self.update_scroll_dimensions();
2125        self
2126    }
2127
2128    /// Set number of visible columns (like HTML textarea cols attribute)
2129    pub fn cols(mut self, cols: usize) -> Self {
2130        let width = {
2131            let mut cfg = self.config.lock().unwrap();
2132            cfg.cols = Some(cols);
2133            cfg.effective_width()
2134        };
2135        self.inner = std::mem::take(&mut self.inner).w(width);
2136        self
2137    }
2138
2139    /// Set both rows and cols
2140    pub fn text_size(mut self, rows: usize, cols: usize) -> Self {
2141        let (width, height) = {
2142            let mut cfg = self.config.lock().unwrap();
2143            cfg.rows = Some(rows);
2144            cfg.cols = Some(cols);
2145            (cfg.effective_width(), cfg.effective_height())
2146        };
2147        self.inner = std::mem::take(&mut self.inner).w(width).h(height);
2148        self.update_scroll_dimensions();
2149        self
2150    }
2151
2152    /// Set font size
2153    pub fn font_size(mut self, size: f32) -> Self {
2154        self.config.lock().unwrap().font_size = size;
2155        self.update_scroll_dimensions();
2156        self
2157    }
2158
2159    /// Set disabled state
2160    pub fn disabled(mut self, disabled: bool) -> Self {
2161        self.config.lock().unwrap().disabled = disabled;
2162        if let Ok(mut s) = self.state.lock() {
2163            s.disabled = disabled;
2164            if disabled {
2165                s.visual = TextFieldState::Disabled;
2166            }
2167        }
2168        self
2169    }
2170
2171    /// Set maximum length
2172    pub fn max_length(mut self, max: usize) -> Self {
2173        self.config.lock().unwrap().max_length = max;
2174        self
2175    }
2176
2177    /// Enable or disable text wrapping
2178    ///
2179    /// When wrapping is enabled (default), long lines wrap to the next visual line.
2180    /// When disabled, text scrolls horizontally instead.
2181    pub fn wrap(mut self, wrap: bool) -> Self {
2182        self.config.lock().unwrap().wrap = wrap;
2183        self
2184    }
2185
2186    /// Disable text wrapping (alias for `.wrap(false)`)
2187    pub fn no_wrap(self) -> Self {
2188        self.wrap(false)
2189    }
2190
2191    // =========================================================================
2192    // Builder methods that return Self (shadow Div methods for fluent API)
2193    // =========================================================================
2194
2195    pub fn w(mut self, px: f32) -> Self {
2196        {
2197            let mut cfg = self.config.lock().unwrap();
2198            cfg.width = px;
2199            cfg.cols = None;
2200        }
2201        self.inner = std::mem::take(&mut self.inner).w(px);
2202        self
2203    }
2204
2205    pub fn h(mut self, px: f32) -> Self {
2206        {
2207            let mut cfg = self.config.lock().unwrap();
2208            cfg.height = px;
2209            cfg.rows = None;
2210        }
2211        self.inner = std::mem::take(&mut self.inner).h(px);
2212        self.update_scroll_dimensions();
2213        self
2214    }
2215
2216    pub fn size(mut self, w: f32, h: f32) -> Self {
2217        {
2218            let mut cfg = self.config.lock().unwrap();
2219            cfg.width = w;
2220            cfg.height = h;
2221            cfg.cols = None;
2222            cfg.rows = None;
2223        }
2224        self.inner = std::mem::take(&mut self.inner).size(w, h);
2225        self.update_scroll_dimensions();
2226        self
2227    }
2228
2229    pub fn square(mut self, size: f32) -> Self {
2230        self.inner = std::mem::take(&mut self.inner).square(size);
2231        self
2232    }
2233
2234    pub fn w_full(mut self) -> Self {
2235        self.inner = std::mem::take(&mut self.inner).w_full();
2236        self
2237    }
2238
2239    pub fn h_full(mut self) -> Self {
2240        self.inner = std::mem::take(&mut self.inner).h_full();
2241        self
2242    }
2243
2244    pub fn w_fit(mut self) -> Self {
2245        self.inner = std::mem::take(&mut self.inner).w_fit();
2246        self
2247    }
2248
2249    pub fn h_fit(mut self) -> Self {
2250        self.inner = std::mem::take(&mut self.inner).h_fit();
2251        self
2252    }
2253
2254    pub fn min_w(mut self, px: f32) -> Self {
2255        self.inner = std::mem::take(&mut self.inner).min_w(px);
2256        self
2257    }
2258
2259    pub fn p(mut self, px: f32) -> Self {
2260        self.inner = std::mem::take(&mut self.inner).p(px);
2261        self
2262    }
2263
2264    pub fn px(mut self, px: f32) -> Self {
2265        self.inner = std::mem::take(&mut self.inner).px(px);
2266        self
2267    }
2268
2269    pub fn py(mut self, px: f32) -> Self {
2270        self.inner = std::mem::take(&mut self.inner).py(px);
2271        self
2272    }
2273
2274    pub fn m(mut self, px: f32) -> Self {
2275        self.inner = std::mem::take(&mut self.inner).m(px);
2276        self
2277    }
2278
2279    pub fn mx(mut self, px: f32) -> Self {
2280        self.inner = std::mem::take(&mut self.inner).mx(px);
2281        self
2282    }
2283
2284    pub fn my(mut self, px: f32) -> Self {
2285        self.inner = std::mem::take(&mut self.inner).my(px);
2286        self
2287    }
2288
2289    pub fn gap(mut self, px: f32) -> Self {
2290        self.inner = std::mem::take(&mut self.inner).gap(px);
2291        self
2292    }
2293
2294    pub fn flex_row(mut self) -> Self {
2295        self.inner = std::mem::take(&mut self.inner).flex_row();
2296        self
2297    }
2298
2299    pub fn flex_col(mut self) -> Self {
2300        self.inner = std::mem::take(&mut self.inner).flex_col();
2301        self
2302    }
2303
2304    pub fn flex_grow(mut self) -> Self {
2305        self.inner = std::mem::take(&mut self.inner).flex_grow();
2306        self
2307    }
2308
2309    pub fn items_center(mut self) -> Self {
2310        self.inner = std::mem::take(&mut self.inner).items_center();
2311        self
2312    }
2313
2314    pub fn items_start(mut self) -> Self {
2315        self.inner = std::mem::take(&mut self.inner).items_start();
2316        self
2317    }
2318
2319    pub fn items_end(mut self) -> Self {
2320        self.inner = std::mem::take(&mut self.inner).items_end();
2321        self
2322    }
2323
2324    pub fn justify_center(mut self) -> Self {
2325        self.inner = std::mem::take(&mut self.inner).justify_center();
2326        self
2327    }
2328
2329    pub fn justify_start(mut self) -> Self {
2330        self.inner = std::mem::take(&mut self.inner).justify_start();
2331        self
2332    }
2333
2334    pub fn justify_end(mut self) -> Self {
2335        self.inner = std::mem::take(&mut self.inner).justify_end();
2336        self
2337    }
2338
2339    pub fn justify_between(mut self) -> Self {
2340        self.inner = std::mem::take(&mut self.inner).justify_between();
2341        self
2342    }
2343
2344    pub fn bg(mut self, color: impl Into<blinc_core::Brush>) -> Self {
2345        self.inner = std::mem::take(&mut self.inner).bg(color);
2346        self
2347    }
2348
2349    pub fn rounded(mut self, radius: f32) -> Self {
2350        self.config.lock().unwrap().corner_radius = radius;
2351        self.inner = std::mem::take(&mut self.inner).rounded(radius);
2352        self
2353    }
2354
2355    /// Set the CSS element ID for stylesheet matching
2356    ///
2357    /// When set, the TextArea will query the active stylesheet for
2358    /// styles matching `#id`, `#id:hover`, `#id:focus`, `#id:disabled`,
2359    /// and `#id::placeholder`.
2360    pub fn id(mut self, id: &str) -> Self {
2361        if let Ok(mut d) = self.state.lock() {
2362            d.css_element_id = Some(id.to_string());
2363        }
2364        self.inner = std::mem::take(&mut self.inner).id(id);
2365        self
2366    }
2367
2368    /// Add a CSS class name for selector matching
2369    pub fn class(mut self, name: &str) -> Self {
2370        self.inner = std::mem::take(&mut self.inner).class(name);
2371        self
2372    }
2373
2374    pub fn border(mut self, width: f32, color: blinc_core::Color) -> Self {
2375        self.inner = std::mem::take(&mut self.inner).border(width, color);
2376        self
2377    }
2378
2379    pub fn border_color(mut self, color: blinc_core::Color) -> Self {
2380        self.inner = std::mem::take(&mut self.inner).border_color(color);
2381        self
2382    }
2383
2384    pub fn border_width(mut self, width: f32) -> Self {
2385        self.inner = std::mem::take(&mut self.inner).border_width(width);
2386        self
2387    }
2388
2389    pub fn shadow(mut self, shadow: blinc_core::Shadow) -> Self {
2390        self.inner = std::mem::take(&mut self.inner).shadow(shadow);
2391        self
2392    }
2393
2394    pub fn shadow_sm(mut self) -> Self {
2395        self.inner = std::mem::take(&mut self.inner).shadow_sm();
2396        self
2397    }
2398
2399    pub fn shadow_md(mut self) -> Self {
2400        self.inner = std::mem::take(&mut self.inner).shadow_md();
2401        self
2402    }
2403
2404    pub fn shadow_lg(mut self) -> Self {
2405        self.inner = std::mem::take(&mut self.inner).shadow_lg();
2406        self
2407    }
2408
2409    pub fn transform(mut self, transform: blinc_core::Transform) -> Self {
2410        self.inner = std::mem::take(&mut self.inner).transform(transform);
2411        self
2412    }
2413
2414    pub fn opacity(mut self, opacity: f32) -> Self {
2415        self.inner = std::mem::take(&mut self.inner).opacity(opacity);
2416        self
2417    }
2418
2419    pub fn overflow_clip(mut self) -> Self {
2420        self.inner = std::mem::take(&mut self.inner).overflow_clip();
2421        self
2422    }
2423
2424    pub fn child(mut self, child: impl ElementBuilder + 'static) -> Self {
2425        self.inner = std::mem::take(&mut self.inner).child(child);
2426        self
2427    }
2428
2429    pub fn children<I>(mut self, children: I) -> Self
2430    where
2431        I: IntoIterator,
2432        I::Item: ElementBuilder + 'static,
2433    {
2434        self.inner = std::mem::take(&mut self.inner).children(children);
2435        self
2436    }
2437
2438    // Event handlers
2439    pub fn on_click<F>(mut self, handler: F) -> Self
2440    where
2441        F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
2442    {
2443        self.inner = std::mem::take(&mut self.inner).on_click(handler);
2444        self
2445    }
2446
2447    pub fn on_hover_enter<F>(mut self, handler: F) -> Self
2448    where
2449        F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
2450    {
2451        self.inner = std::mem::take(&mut self.inner).on_hover_enter(handler);
2452        self
2453    }
2454
2455    pub fn on_hover_leave<F>(mut self, handler: F) -> Self
2456    where
2457        F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
2458    {
2459        self.inner = std::mem::take(&mut self.inner).on_hover_leave(handler);
2460        self
2461    }
2462
2463    pub fn on_mouse_down<F>(mut self, handler: F) -> Self
2464    where
2465        F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
2466    {
2467        self.inner = std::mem::take(&mut self.inner).on_mouse_down(handler);
2468        self
2469    }
2470
2471    pub fn on_mouse_up<F>(mut self, handler: F) -> Self
2472    where
2473        F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
2474    {
2475        self.inner = std::mem::take(&mut self.inner).on_mouse_up(handler);
2476        self
2477    }
2478
2479    pub fn on_focus<F>(mut self, handler: F) -> Self
2480    where
2481        F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
2482    {
2483        self.inner = std::mem::take(&mut self.inner).on_focus(handler);
2484        self
2485    }
2486
2487    pub fn on_blur<F>(mut self, handler: F) -> Self
2488    where
2489        F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
2490    {
2491        self.inner = std::mem::take(&mut self.inner).on_blur(handler);
2492        self
2493    }
2494
2495    pub fn on_key_down<F>(mut self, handler: F) -> Self
2496    where
2497        F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
2498    {
2499        self.inner = std::mem::take(&mut self.inner).on_key_down(handler);
2500        self
2501    }
2502
2503    pub fn on_key_up<F>(mut self, handler: F) -> Self
2504    where
2505        F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
2506    {
2507        self.inner = std::mem::take(&mut self.inner).on_key_up(handler);
2508        self
2509    }
2510
2511    pub fn on_scroll<F>(mut self, handler: F) -> Self
2512    where
2513        F: Fn(&crate::event_handler::EventContext) + Send + Sync + 'static,
2514    {
2515        self.inner = std::mem::take(&mut self.inner).on_scroll(handler);
2516        self
2517    }
2518
2519    /// Set a signal ID to be notified when text content changes
2520    ///
2521    /// When the text area content is modified (typing, deletion, paste, etc.),
2522    /// this signal will be triggered via `check_stateful_deps`, causing any
2523    /// stateful elements that depend on this signal to rebuild.
2524    ///
2525    /// # Example
2526    ///
2527    /// ```ignore
2528    /// let markdown_state = ctx.use_state_keyed("editor", || {
2529    ///     Arc::new(Mutex::new(TextAreaState::new()))
2530    /// });
2531    ///
2532    /// // Editor panel
2533    /// text_area(&markdown_state.get())
2534    ///     .on_change_signal(markdown_state.signal_id())
2535    ///
2536    /// // Preview panel - will rebuild when text changes
2537    /// stateful(preview_state)
2538    ///     .deps(&[markdown_state.signal_id()])
2539    ///     .on_state(|_, container| { ... })
2540    /// ```
2541    pub fn on_change_signal(self, signal_id: SignalId) -> Self {
2542        // Store the signal ID in the TextAreaState
2543        if let Ok(mut state) = self.state.lock() {
2544            state.set_change_signal(signal_id);
2545        }
2546        self
2547    }
2548}
2549
2550/// Create a ready-to-use multi-line text area
2551///
2552/// The text area inherits ALL Div methods, so you have full layout control.
2553///
2554/// # Example
2555///
2556/// ```ignore
2557/// let state = text_area_state_with_placeholder("Enter message...");
2558/// text_area(&state)
2559///     .rows(4)
2560///     .w(400.0)
2561///     .rounded(12.0)
2562///     .shadow_sm()
2563/// ```
2564/// Create a text area widget
2565/// By default, width inherits from parent (w_full). Use .w() to set explicit width.
2566pub fn text_area(state: &SharedTextAreaState) -> TextArea {
2567    TextArea::new(state).w_full()
2568}
2569
2570impl ElementBuilder for TextArea {
2571    fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
2572        // Set base render props and layout style for incremental updates
2573        // Note: callback and handlers are registered in new() so they're available for incremental diff
2574        // base_style must be updated here because on_state() captures it before .w()/.h() are applied
2575        {
2576            let shared_state = self.inner.shared_state();
2577            let mut shared = shared_state.lock().unwrap();
2578            shared.base_render_props = Some(self.inner.inner_render_props());
2579            shared.base_style = self.inner.inner_layout_style();
2580        }
2581
2582        // Build the inner Stateful
2583        self.inner.build(tree)
2584    }
2585
2586    fn render_props(&self) -> RenderProps {
2587        self.inner.render_props()
2588    }
2589
2590    fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
2591        self.inner.children_builders()
2592    }
2593
2594    fn element_type_id(&self) -> crate::div::ElementTypeId {
2595        crate::div::ElementTypeId::Div
2596    }
2597
2598    fn semantic_type_name(&self) -> Option<&'static str> {
2599        Some("textarea")
2600    }
2601
2602    fn event_handlers(&self) -> Option<&crate::event_handler::EventHandlers> {
2603        ElementBuilder::event_handlers(&self.inner)
2604    }
2605
2606    fn layout_style(&self) -> Option<&taffy::Style> {
2607        self.inner.layout_style()
2608    }
2609
2610    fn layout_bounds_storage(&self) -> Option<crate::renderer::LayoutBoundsStorage> {
2611        // Return the layout bounds storage from the state so it gets updated after layout
2612        if let Ok(data) = self.state.lock() {
2613            Some(Arc::clone(&data.layout_bounds_storage))
2614        } else {
2615            None
2616        }
2617    }
2618
2619    fn layout_bounds_callback(&self) -> Option<crate::renderer::LayoutBoundsCallback> {
2620        // When layout bounds change, update config dimensions and trigger refresh
2621        let config = Arc::clone(&self.config);
2622        let state = Arc::clone(&self.state);
2623        let stateful_state = self.inner.shared_state();
2624        Some(Arc::new(move |bounds| {
2625            let mut needs_refresh = false;
2626
2627            if let Ok(mut cfg) = config.lock() {
2628                let new_width = bounds.width;
2629                let new_height = bounds.height;
2630
2631                // Check if width changed significantly
2632                let width_changed = (cfg.width - new_width).abs() > 1.0;
2633                // Check if height changed significantly
2634                let height_changed = (cfg.height - new_height).abs() > 1.0;
2635
2636                if width_changed || height_changed {
2637                    if width_changed {
2638                        cfg.width = new_width;
2639                    }
2640                    if height_changed {
2641                        cfg.height = new_height;
2642                    }
2643
2644                    // Update state with new dimensions
2645                    if let Ok(mut data) = state.lock() {
2646                        if width_changed {
2647                            data.available_width =
2648                                new_width - cfg.padding_x * 2.0 - cfg.border_width * 2.0;
2649                            // Recompute visual lines with new width
2650                            data.compute_visual_lines();
2651                        }
2652
2653                        if height_changed {
2654                            // Update viewport height for scroll calculations
2655                            let viewport_height =
2656                                new_height - cfg.padding_y * 2.0 - cfg.border_width * 2.0;
2657                            data.viewport_height = viewport_height;
2658
2659                            // Update scroll physics viewport
2660                            if let Ok(mut p) = data.scroll_physics.lock() {
2661                                p.viewport_height = viewport_height;
2662                            }
2663                        }
2664                    }
2665
2666                    needs_refresh = true;
2667                }
2668            }
2669
2670            if needs_refresh {
2671                // Trigger a visual update
2672                crate::stateful::refresh_stateful(&stateful_state);
2673            }
2674        }))
2675    }
2676}
2677
2678#[cfg(test)]
2679mod tests {
2680    use super::*;
2681
2682    #[test]
2683    fn test_text_area_state_insert() {
2684        let mut state = TextAreaState::new();
2685        state.insert("hello");
2686        assert_eq!(state.value(), "hello");
2687
2688        state.insert_newline();
2689        state.insert("world");
2690        assert_eq!(state.value(), "hello\nworld");
2691        assert_eq!(state.line_count(), 2);
2692    }
2693
2694    #[test]
2695    fn test_text_area_state_delete() {
2696        let mut state = TextAreaState::with_value("hello\nworld");
2697        state.cursor = TextPosition::new(1, 5);
2698
2699        state.delete_backward();
2700        assert_eq!(state.value(), "hello\nworl");
2701
2702        state.cursor = TextPosition::new(1, 0);
2703        state.delete_backward();
2704        assert_eq!(state.value(), "helloworl");
2705        assert_eq!(state.line_count(), 1);
2706    }
2707
2708    #[test]
2709    fn test_text_area_state_navigation() {
2710        let mut state = TextAreaState::with_value("line1\nline2\nline3");
2711        state.cursor = TextPosition::new(1, 3);
2712
2713        state.move_up(false);
2714        assert_eq!(state.cursor, TextPosition::new(0, 3));
2715
2716        state.move_down(false);
2717        assert_eq!(state.cursor, TextPosition::new(1, 3));
2718
2719        state.move_to_line_start(false);
2720        assert_eq!(state.cursor, TextPosition::new(1, 0));
2721
2722        state.move_to_line_end(false);
2723        assert_eq!(state.cursor, TextPosition::new(1, 5));
2724    }
2725
2726    #[test]
2727    fn test_text_area_state_selection() {
2728        let mut state = TextAreaState::with_value("hello\nworld");
2729
2730        state.select_all();
2731        assert_eq!(state.selected_text(), Some("hello\nworld".to_string()));
2732
2733        state.insert("new");
2734        assert_eq!(state.value(), "new");
2735        assert_eq!(state.line_count(), 1);
2736    }
2737}