1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
37pub struct TextPosition {
38 pub line: usize,
40 pub column: usize,
42}
43
44impl TextPosition {
45 pub fn new(line: usize, column: usize) -> Self {
46 Self { line, column }
47 }
48}
49
50fn apply_css_overrides_textarea(
56 cfg: &mut TextAreaConfig,
57 stylesheet: &Stylesheet,
58 element_id: &str,
59 visual: &TextFieldState,
60) {
61 if let Some(base) = stylesheet.get(element_id) {
63 apply_style_to_textarea_config(cfg, base, visual);
64 }
65
66 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 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
143fn 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#[derive(Clone)]
209pub struct TextAreaConfig {
210 pub placeholder: String,
212 pub width: f32,
214 pub height: f32,
216 pub rows: Option<usize>,
218 pub cols: Option<usize>,
220 pub font_size: f32,
222 pub line_height: f32,
224 pub char_width_ratio: f32,
226 pub text_color: Color,
228 pub placeholder_color: Color,
230 pub bg_color: Color,
232 pub hover_bg_color: Color,
234 pub focused_bg_color: Color,
236 pub border_color: Color,
238 pub hover_border_color: Color,
240 pub focused_border_color: Color,
242 pub border_width: f32,
244 pub corner_radius: f32,
246 pub padding_x: f32,
248 pub padding_y: f32,
250 pub cursor_color: Color,
252 pub selection_color: Color,
254 pub disabled: bool,
256 pub max_length: usize,
258 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 wrap: true,
296 }
297 }
298}
299
300impl TextAreaConfig {
301 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 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#[derive(Clone, Debug)]
324pub struct VisualLine {
325 pub logical_line: usize,
327 pub start_char: usize,
329 pub end_char: usize,
331 pub text: String,
333 pub width: f32,
335}
336
337#[derive(Clone)]
339pub struct TextAreaState {
340 pub lines: Vec<String>,
342 pub cursor: TextPosition,
344 pub selection_start: Option<TextPosition>,
346 pub visual: TextFieldState,
348 pub placeholder: String,
350 pub disabled: bool,
352 pub focus_time_ms: u64,
355 pub cursor_blink_interval_ms: u64,
357 pub cursor_state: SharedCursorState,
359 pub(crate) scroll_physics: SharedScrollPhysics,
361 pub(crate) viewport_height: f32,
363 pub(crate) line_height: f32,
365 pub(crate) font_size: f32,
367 pub(crate) available_width: f32,
369 pub(crate) wrap_enabled: bool,
371 pub(crate) visual_lines: Vec<VisualLine>,
374 pub(crate) stateful_state: Option<SharedState<TextFieldState>>,
376 pub(crate) clicked_visual_line: Option<usize>,
380 pub(crate) change_version: Arc<AtomicU64>,
383 pub(crate) change_signal_id: Option<SignalId>,
385 pub layout_bounds_storage: crate::renderer::LayoutBoundsStorage,
387 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 .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, cursor_state: cursor_state(),
419 scroll_physics: Arc::new(Mutex::new(ScrollPhysics::default())),
420 viewport_height: 120.0, line_height: 14.0 * 1.4, font_size: 14.0, available_width: 276.0, wrap_enabled: true, visual_lines: Vec::new(), stateful_state: None,
427 clicked_visual_line: None, 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 pub fn new() -> Self {
439 Self::default()
440 }
441
442 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 pub fn with_placeholder(placeholder: impl Into<String>) -> Self {
463 Self {
464 placeholder: placeholder.into(),
465 ..Default::default()
466 }
467 }
468
469 pub fn value(&self) -> String {
471 self.lines.join("\n")
472 }
473
474 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 pub fn line_count(&self) -> usize {
490 self.lines.len()
491 }
492
493 pub fn get_line(&self, index: usize) -> Option<&str> {
495 self.lines.get(index).map(|s| s.as_str())
496 }
497
498 pub fn is_empty(&self) -> bool {
500 self.lines.len() == 1 && self.lines[0].is_empty()
501 }
502
503 pub fn is_focused(&self) -> bool {
505 self.visual.is_focused()
506 }
507
508 pub fn signal_id(&self) -> Option<SignalId> {
529 self.change_signal_id
530 }
531
532 pub fn set_change_signal(&mut self, signal_id: SignalId) {
536 self.change_signal_id = Some(signal_id);
537 }
538
539 pub fn change_version(&self) -> u64 {
543 self.change_version.load(Ordering::SeqCst)
544 }
545
546 pub fn is_cursor_visible(&self, current_time_ms: u64) -> bool {
549 if self.cursor_blink_interval_ms == 0 {
550 return true; }
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 pub fn reset_cursor_blink(&mut self) {
559 self.focus_time_ms = elapsed_ms();
560 if let Ok(mut cs) = self.cursor_state.lock() {
562 cs.reset_blink();
563 }
564 }
565
566 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 pub fn insert_newline(&mut self) {
591 self.insert_newline_internal();
592 }
593
594 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 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(¤t_line);
625 }
626 }
627
628 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 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 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 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 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 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 !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 let prev_visual_idx = current_visual_idx - 1;
753 let prev_vl = &self.visual_lines[prev_visual_idx];
754
755 let cursor_x = self.cursor_x_in_visual_line();
757
758 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 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 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 !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 let next_visual_idx = current_visual_idx + 1;
788 let next_vl = &self.visual_lines[next_visual_idx];
789
790 let cursor_x = self.cursor_x_in_visual_line();
792
793 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 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 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 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 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 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 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 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 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 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 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 ((metrics.width / available_width).ceil() as usize).max(1)
933 }
934
935 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 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 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 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 let mut end_char = start_char;
989 let mut last_word_break = start_char;
990 let mut current_width = 0.0;
991
992 while end_char < char_count {
994 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 break;
1002 }
1003
1004 current_width = test_width;
1005 end_char = test_end;
1006
1007 if end_char < char_count && chars[end_char - 1].is_whitespace() {
1009 last_word_break = end_char;
1010 }
1011 }
1012
1013 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 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 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 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 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 if cursor_col >= vl.start_char && cursor_col <= vl.end_char {
1072 return idx;
1073 }
1074 if cursor_col == vl.end_char {
1076 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 self.visual_lines.len().saturating_sub(1)
1090 }
1091
1092 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 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 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 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 pub fn visual_line_count(&self) -> usize {
1130 if self.visual_lines.is_empty() {
1131 self.lines.len()
1133 } else {
1134 self.visual_lines.len()
1135 }
1136 }
1137
1138 pub fn content_height_from_visual_lines(&self) -> f32 {
1140 self.visual_line_count() as f32 * self.line_height
1141 }
1142
1143 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 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 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 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 pub fn ensure_cursor_visible(&mut self, line_height: f32, viewport_height: f32) {
1197 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 let mut physics = self.scroll_physics.lock().unwrap();
1214 let current_offset = -physics.offset_y; let mut new_offset = current_offset;
1218 if cursor_y < current_offset {
1219 new_offset = cursor_y;
1220 }
1221
1222 if cursor_bottom > current_offset + viewport_height {
1224 new_offset = cursor_bottom - viewport_height;
1225 }
1226
1227 let max_scroll = (content_height - viewport_height).max(0.0);
1229 new_offset = new_offset.clamp(0.0, max_scroll);
1230
1231 physics.offset_y = -new_offset;
1233 }
1234
1235 pub fn scroll_offset(&self) -> f32 {
1237 -self.scroll_physics.lock().unwrap().offset_y
1238 }
1239
1240 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 let text_y = y + scroll_offset;
1255
1256 let visual_line_idx = (text_y / line_height).floor().max(0.0) as usize;
1258
1259 if !self.visual_lines.is_empty() {
1261 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 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 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 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 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 vl.start_char + best_pos
1311 }
1312
1313 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 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
1344fn 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
1352pub type SharedTextAreaState = Arc<Mutex<TextAreaState>>;
1354
1355pub fn text_area_state() -> SharedTextAreaState {
1357 Arc::new(Mutex::new(TextAreaState::new()))
1358}
1359
1360pub fn text_area_state_with_placeholder(placeholder: impl Into<String>) -> SharedTextAreaState {
1362 Arc::new(Mutex::new(TextAreaState::with_placeholder(placeholder)))
1363}
1364
1365pub struct TextArea {
1372 inner: Stateful<TextFieldState>,
1374 state: SharedTextAreaState,
1376 config: Arc<Mutex<TextAreaConfig>>,
1378}
1379
1380impl TextArea {
1381 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 let (initial_visual, existing_stateful_state) = {
1391 let d = state.lock().unwrap();
1392 (d.visual, d.stateful_state.clone())
1393 };
1394
1395 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 if let Ok(mut d) = state.lock() {
1402 d.stateful_state = Some(Arc::clone(&new_state));
1403 }
1404 new_state
1405 });
1406
1407 {
1412 let mut shared = shared_state.lock().unwrap();
1413 shared.node_id = None;
1414 }
1415
1416 let mut inner = Self::create_inner_with_handlers(
1418 Arc::clone(&shared_state),
1419 Arc::clone(state),
1420 Arc::clone(&config),
1421 );
1422
1423 inner = inner.w(default_width).h(default_height).min_w(0.0);
1432
1433 {
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 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 data_guard.visual = *visual;
1465
1466 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 data_guard.compute_visual_lines();
1480
1481 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 container.set_bg(bg);
1496 container.set_border(cfg.border_width, border);
1497 container.set_rounded(cfg.corner_radius);
1498
1499 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 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 inner.ensure_state_handlers_registered();
1523
1524 let textarea = Self {
1525 inner,
1526 state: Arc::clone(state),
1527 config,
1528 };
1529
1530 textarea.update_scroll_dimensions();
1532
1533 textarea
1534 }
1535
1536 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 .on_mouse_down(move |ctx| {
1555 set_focused_text_area(&data_for_click);
1557
1558 let click_x = ctx.local_x;
1560 let click_y = ctx.local_y;
1561
1562 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 {
1585 let mut shared = shared_for_click.lock().unwrap();
1586 if !shared.state.is_focused() {
1587 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 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 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 if d.visual_lines.is_empty() || d.wrap_enabled {
1617 d.compute_visual_lines();
1618 }
1619
1620 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 d.cursor_position_from_visual_line(visual_line_idx, text_x)
1629 } else {
1630 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; d.reset_cursor_blink();
1637
1638 true }; if needs_refresh {
1643 refresh_stateful(&shared_for_click);
1644 }
1645 })
1646 .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 d.compute_visual_lines();
1663 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 }; if needs_refresh {
1676 refresh_stateful(&shared_for_text);
1677 }
1678
1679 if let Some(signal_id) = change_signal {
1681 crate::stateful::check_stateful_deps(&[signal_id]);
1682 }
1683 })
1684 .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 d.delete_backward();
1703 text_changed = true;
1704 tracing::debug!("TextArea backspace, value: {}", d.value());
1705 }
1706 127 => {
1707 d.delete_forward();
1709 text_changed = true;
1710 }
1711 13 => {
1712 d.insert_newline();
1714 text_changed = true;
1715 tracing::debug!("TextArea newline, lines: {}", d.line_count());
1716 }
1717 37 => {
1718 d.move_left(ctx.shift);
1720 }
1721 39 => {
1722 d.move_right(ctx.shift);
1724 }
1725 38 => {
1726 d.move_up(ctx.shift);
1728 }
1729 40 => {
1730 d.move_down(ctx.shift);
1732 }
1733 36 => {
1734 d.move_to_line_start(ctx.shift);
1736 }
1737 35 => {
1738 d.move_to_line_end(ctx.shift);
1740 }
1741 27 => {
1742 should_blur = true;
1744 cursor_changed = true;
1745 }
1746 _ => {
1747 cursor_changed = false;
1748 }
1749 }
1750
1751 if text_changed {
1753 d.compute_visual_lines();
1754 }
1755
1756 if cursor_changed && !should_blur {
1757 d.reset_cursor_blink();
1758 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 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 }; if needs_refresh.1 {
1776 crate::widgets::text_input::blur_all_text_inputs();
1777 } else if needs_refresh.0 {
1778 refresh_stateful(&shared_for_key);
1780 }
1781
1782 if let Some(signal_id) = needs_refresh.2 {
1784 crate::stateful::check_stateful_deps(&[signal_id]);
1785 }
1786 })
1787 .cursor_text()
1789 }
1791
1792 fn build_content(
1796 visual: TextFieldState,
1797 data: &TextAreaState,
1798 config: &TextAreaConfig,
1799 shared_state: SharedTextAreaState,
1800 ) -> Div {
1801 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 let is_focused = visual.is_focused();
1814 let cursor_color = config.cursor_color;
1815
1816 let cursor_height = config.font_size * 1.2;
1818 let line_height = config.font_size * config.line_height;
1819
1820 let text_area_width =
1822 config.effective_width() - config.padding_x * 2.0 - config.border_width * 2.0;
1823
1824 let (cursor_x, cursor_visual_y) = if !data.visual_lines.is_empty() {
1827 data.cursor_position_from_visual_lines()
1828 } else {
1829 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 let cursor_state_for_canvas = Arc::clone(&data.cursor_state);
1845
1846 let cursor_canvas_opt = if is_focused {
1849 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 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 let placeholder = if !data.placeholder.is_empty() {
1923 &data.placeholder
1924 } else {
1925 &config.placeholder
1926 };
1927
1928 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 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 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 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(), ),
1975 );
1976 }
1977 } else if config.wrap {
1978 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 text_content = text_content.child(
1988 div()
1989 .min_h(line_height)
1990 .w(text_area_width)
1991 .on_mouse_down(move |_ctx| {
1992 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 ),
2004 );
2005 }
2006 } else {
2007 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 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 if let Some(cursor) = cursor_canvas_opt {
2037 text_content = text_content.child(cursor);
2038 }
2039
2040 let padding_x = config.padding_x;
2043 let padding_y = config.padding_y;
2044
2045 let scrollable_content = Scroll::with_physics(Arc::clone(&data.scroll_physics))
2050 .direction(ScrollDirection::Vertical)
2051 .no_bounce()
2052 .flex_grow() .child(text_content);
2054
2055 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 .child(div().h(padding_y).w(content_width))
2069 .child(
2071 div()
2072 .flex_row()
2073 .h(inner_height)
2074 .w(content_width)
2075 .child(div().w(padding_x).h(inner_height))
2077 .child(scrollable_content)
2079 .child(div().w(padding_x).h(inner_height)),
2081 )
2082 .child(div().h(padding_y).w(content_width))
2084 }
2085
2086 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 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 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 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 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 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 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 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 pub fn max_length(mut self, max: usize) -> Self {
2173 self.config.lock().unwrap().max_length = max;
2174 self
2175 }
2176
2177 pub fn wrap(mut self, wrap: bool) -> Self {
2182 self.config.lock().unwrap().wrap = wrap;
2183 self
2184 }
2185
2186 pub fn no_wrap(self) -> Self {
2188 self.wrap(false)
2189 }
2190
2191 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 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 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 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 pub fn on_change_signal(self, signal_id: SignalId) -> Self {
2542 if let Ok(mut state) = self.state.lock() {
2544 state.set_change_signal(signal_id);
2545 }
2546 self
2547 }
2548}
2549
2550pub 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 {
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 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 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 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 let width_changed = (cfg.width - new_width).abs() > 1.0;
2633 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 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 data.compute_visual_lines();
2651 }
2652
2653 if height_changed {
2654 let viewport_height =
2656 new_height - cfg.padding_y * 2.0 - cfg.border_width * 2.0;
2657 data.viewport_height = viewport_height;
2658
2659 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 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}