1use std::rc::Rc;
2use std::sync::Arc;
3
4use geometry_core::Rect;
5use layout_core::{LayoutError, LayoutStyle};
6use platform_core::{Event, Key, ModifiersState, NamedKey, PointerButton};
7use reactive_core::{Effect, RwSignal, effect, signal};
8use renderer_core::{Color, Paint, RectStyle, ShapeStyle, TextStyle};
9use ui_tree::{Component, EventResult, RenderNode};
10
11use crate::context::{mark_dirty, new_measured_leaf};
12use crate::focus::{self, FocusId};
13use crate::impl_leaf_widget;
14use crate::layout_leaf::LayoutLeaf;
15
16const CARET_WIDTH: f32 = 1.5;
18const NO_WRAP_WIDTH: f32 = 1.0e6;
21const TAB_INSERT: &str = " ";
23
24pub struct TextArea {
33 value: RwSignal<String>,
34 caret: RwSignal<usize>,
36 anchor: RwSignal<Option<usize>>,
39 style: Rc<dyn Fn() -> TextStyle>,
40 id: FocusId,
41 leaf: LayoutLeaf,
42 placeholder: String,
43 _remeasure: Effect,
46}
47
48impl TextArea {
49 pub fn new(
50 value: RwSignal<String>,
51 layout_style: LayoutStyle,
52 style_fn: impl Fn() -> TextStyle + 'static,
53 ) -> Result<Self, LayoutError> {
54 let style: Rc<dyn Fn() -> TextStyle> = Rc::new(style_fn);
55 let measure_value = value.clone();
58 let measure_style = Rc::clone(&style);
59 let measure = Box::new(move |_max_width: f32| {
60 let s = (measure_style)();
61 let line_h = crate::text_metrics::line_height(s.font_size);
62 let lines = measure_value.with(|t| t.matches('\n').count() + 1);
63 (0.0, lines as f32 * line_h)
64 });
65 let (node, rect) = new_measured_leaf(layout_style.align_self_stretch(), measure)?;
66 let caret = value.with(|s| s.len());
67 let id = focus::next_id();
68 focus::register_with_role(
69 id,
70 focus::FocusKind::TextEntry,
71 node,
72 focus::Role::MultilineTextInput,
73 );
74 let remeasure = {
75 let value = value.clone();
76 effect(move || {
77 value.with(|_| {});
79 mark_dirty(node).ok();
80 })
81 };
82 Ok(Self {
83 value,
84 caret: signal(caret),
85 anchor: signal(None),
86 style,
87 id,
88 leaf: LayoutLeaf { node, rect },
89 placeholder: String::new(),
90 _remeasure: remeasure,
91 })
92 }
93
94 pub fn placeholder(mut self, p: impl Into<String>) -> Self {
96 self.placeholder = p.into();
97 self
98 }
99
100 pub fn request_focus(&self) {
103 focus::request(self.id);
104 }
105
106 pub fn focused(&self) -> bool {
108 focus::is_focused(self.id)
109 }
110
111 pub fn focus_handle(&self) -> focus::FocusHandle {
114 focus::handle(self.id)
115 }
116
117 fn line_height(&self) -> f32 {
118 crate::text_metrics::line_height((self.style)().font_size)
119 }
120
121 fn caret_at(&self, text: &str) -> usize {
123 let mut c = self.caret.get().min(text.len());
124 while c > 0 && !text.is_char_boundary(c) {
125 c -= 1;
126 }
127 c
128 }
129
130 fn selection(&self, text: &str) -> Option<(usize, usize)> {
132 let caret = self.caret_at(text);
133 let mut anchor = self.anchor.get()?.min(text.len());
134 while anchor > 0 && !text.is_char_boundary(anchor) {
135 anchor -= 1;
136 }
137 (anchor != caret).then(|| (anchor.min(caret), anchor.max(caret)))
138 }
139
140 fn selected_text(&self, text: &str) -> Option<String> {
141 self.selection(text)
142 .map(|(from, to)| text[from..to].to_string())
143 }
144
145 fn take_selection(&self, text: &mut String) -> Option<usize> {
148 let (from, to) = self.selection(text)?;
149 text.replace_range(from..to, "");
150 Some(from)
151 }
152
153 fn edit(&mut self, key: &Key, mods: &ModifiersState, style: &TextStyle) -> EventResult {
157 let mut text = self.value.get();
158 let mut caret = self.caret_at(&text);
159 let mut changed = false;
160 let anchor = if mods.is_shift {
163 Some(self.anchor.get().unwrap_or(caret))
164 } else {
165 None
166 };
167 match key {
168 Key::Char('a') | Key::Char('A') if mods.is_ctrl || mods.is_meta => {
169 self.anchor.set(Some(0));
170 self.caret.set(text.len());
171 return EventResult::Handled;
172 }
173 Key::Char('c') | Key::Char('C') if mods.is_ctrl || mods.is_meta => {
174 let Some(selected) = self.selected_text(&text) else {
175 return EventResult::Ignored;
176 };
177 services_core::set_clipboard_text(&selected);
178 return EventResult::Handled;
180 }
181 Key::Char('x') | Key::Char('X') if mods.is_ctrl || mods.is_meta => {
182 let Some(selected) = self.selected_text(&text) else {
183 return EventResult::Ignored;
184 };
185 services_core::set_clipboard_text(&selected);
186 caret = self.take_selection(&mut text).unwrap_or(caret);
187 changed = true;
188 }
189 Key::Char('v') | Key::Char('V') if mods.is_ctrl || mods.is_meta => {
192 let Some(pasted) = services_core::clipboard_text() else {
193 return EventResult::Ignored;
194 };
195 let had_selection = self.take_selection(&mut text);
196 if pasted.is_empty() && had_selection.is_none() {
197 return EventResult::Ignored;
198 }
199 caret = had_selection.unwrap_or(caret);
200 text.insert_str(caret, &pasted);
201 caret += pasted.len();
202 changed = true;
203 }
204 Key::Char(_) if mods.is_ctrl || mods.is_meta => return EventResult::Ignored,
206 Key::Char(c) if !c.is_control() => {
207 caret = self.take_selection(&mut text).unwrap_or(caret);
208 text.insert(caret, *c);
209 caret += c.len_utf8();
210 changed = true;
211 }
212 Key::Named(NamedKey::Space) => {
213 caret = self.take_selection(&mut text).unwrap_or(caret);
214 text.insert(caret, ' ');
215 caret += 1;
216 changed = true;
217 }
218 Key::Named(NamedKey::Enter) => {
219 caret = self.take_selection(&mut text).unwrap_or(caret);
220 text.insert(caret, '\n');
221 caret += 1;
222 changed = true;
223 }
224 Key::Named(NamedKey::Tab) => {
225 text.insert_str(caret, TAB_INSERT);
226 caret += TAB_INSERT.len();
227 changed = true;
228 }
229 Key::Named(NamedKey::Backspace) => {
230 if let Some(at) = self.take_selection(&mut text) {
231 caret = at;
232 } else {
233 if caret == 0 {
234 return EventResult::Ignored;
235 }
236 let prev = prev_boundary(&text, caret);
237 text.replace_range(prev..caret, "");
238 caret = prev;
239 }
240 changed = true;
241 }
242 Key::Named(NamedKey::Delete) => {
243 if let Some(at) = self.take_selection(&mut text) {
244 caret = at;
245 } else {
246 if caret >= text.len() {
247 return EventResult::Ignored;
248 }
249 let next = next_boundary(&text, caret);
250 text.replace_range(caret..next, "");
251 }
252 changed = true;
253 }
254 Key::Named(NamedKey::ArrowLeft) => caret = prev_boundary(&text, caret),
255 Key::Named(NamedKey::ArrowRight) => caret = next_boundary(&text, caret),
256 Key::Named(NamedKey::ArrowUp) => {
257 let line = line_index(&text, caret);
258 if line > 0 {
259 let x = caret_x(&text, caret, style);
260 caret = offset_at_line_x(&text, style, line - 1, x);
261 } else {
262 caret = 0;
263 }
264 }
265 Key::Named(NamedKey::ArrowDown) => {
266 let line = line_index(&text, caret);
267 let last = text.matches('\n').count();
268 if line < last {
269 let x = caret_x(&text, caret, style);
270 caret = offset_at_line_x(&text, style, line + 1, x);
271 } else {
272 caret = text.len();
273 }
274 }
275 Key::Named(NamedKey::Home) => caret = line_bounds(&text, caret).0,
276 Key::Named(NamedKey::End) => caret = line_bounds(&text, caret).1,
277 Key::Named(NamedKey::Escape) => {
278 focus::release(self.id);
279 return EventResult::Handled;
280 }
281 _ => return EventResult::Ignored,
282 }
283 if changed {
284 self.value.set(text);
287 }
288 self.caret.set(caret);
289 self.anchor.set(anchor.filter(|a| *a != caret));
291 EventResult::Handled
292 }
293}
294
295impl Component for TextArea {
296 fn view(&self) -> RenderNode {
297 let r = self.leaf.rect.get();
298 let text = self.value.get();
299 let style = (self.style)();
300 let line_h = self.line_height();
301 let full = Rect {
304 x: 0.0,
305 y: 0.0,
306 width: NO_WRAP_WIDTH,
307 height: r.height.max(line_h),
308 };
309 let text_node = if text.is_empty() && !self.placeholder.is_empty() {
310 let muted = match style.paint {
311 Paint::Solid(c) => Paint::Solid(c.with_alpha(c.a * 0.5)),
312 _ => Paint::Solid(Color::rgba(0.5, 0.5, 0.55, 0.5)),
313 };
314 let mut ph_style = style;
315 ph_style.paint = muted;
316 RenderNode::text(self.placeholder.clone(), full, ph_style)
317 } else {
318 RenderNode::text(Arc::<str>::from(text.as_str()), full, style)
319 };
320
321 if focus::is_focused(self.id) {
322 let caret = self.caret_at(&text);
323 let line = line_index(&text, caret);
324 let highlight = self.selection(&text).map(|(from, to)| {
328 let (first, last) = (line_index(&text, from), line_index(&text, to));
329 let mut bands = Vec::with_capacity(last - first + 1);
330 for line in first..=last {
331 let (start, end) = nth_line_bounds(&text, line);
332 let x0 = if line == first {
333 caret_x(&text, from, &style)
334 } else {
335 caret_x(&text, start, &style)
336 };
337 let x1 = if line == last {
338 caret_x(&text, to, &style)
339 } else {
340 caret_x(&text, end, &style).max(x0 + line_h * 0.35)
343 };
344 let fill = match style.paint {
345 Paint::Solid(c) => c.with_alpha(0.25),
346 _ => Color::rgba(0.4, 0.6, 0.9, 0.3),
347 };
348 bands.push(RenderNode::rect(
349 Rect {
350 x: x0,
351 y: line as f32 * line_h,
352 width: (x1 - x0).max(1.0),
353 height: line_h,
354 },
355 RectStyle::default().with_fill(Paint::Solid(fill)),
356 ));
357 }
358 RenderNode::group(bands)
359 });
360 let x = caret_x(&text, caret, &style);
361 let caret_rect = Rect {
362 x,
363 y: line as f32 * line_h,
364 width: CARET_WIDTH,
365 height: line_h,
366 };
367 let caret_node =
368 RenderNode::rect(caret_rect, RectStyle::default().with_fill(style.paint));
369 let layers = match highlight {
370 Some(highlight) => vec![highlight, text_node, caret_node],
371 None => vec![text_node, caret_node],
372 };
373 self.leaf.at_layout_position(RenderNode::group(layers))
374 } else {
375 self.leaf.at_layout_position(text_node)
376 }
377 }
378
379 fn on_event(&mut self, event: &Event) -> EventResult {
380 let rect = self.leaf.rect.get();
381 match event {
382 Event::PointerPressed {
383 x,
384 y,
385 button: PointerButton::Primary,
386 ..
387 } => {
388 if rect.contains(*x as f32, *y as f32) {
389 focus::request_from_pointer(self.id);
390 let style = (self.style)();
391 let line_h = crate::text_metrics::line_height(style.font_size);
392 let text = self.value.get();
395 let local_y = (*y as f32 - rect.y).max(0.0);
396 let local_x = (*x as f32 - rect.x).max(0.0);
397 let last = text.matches('\n').count();
398 let line = ((local_y / line_h).floor() as usize).min(last);
399 let at = offset_at_line_x(&text, &style, line, local_x);
400 if crate::keyboard::modifiers().is_shift {
403 let from = self.anchor.get().unwrap_or_else(|| self.caret.get());
404 self.anchor.set(Some(from));
405 } else {
406 self.anchor.set(None);
407 }
408 self.caret.set(at);
409 EventResult::Handled
410 } else {
411 EventResult::Ignored
412 }
413 }
414 Event::KeyPressed { key, modifiers } if focus::is_focused(self.id) => {
415 let style = (self.style)();
416 self.edit(key, modifiers, &style)
417 }
418 _ => EventResult::Ignored,
419 }
420 }
421
422 fn debug_name(&self) -> &'static str {
423 "TextArea"
424 }
425}
426
427impl Drop for TextArea {
428 fn drop(&mut self) {
429 focus::unregister(self.id);
430 }
431}
432
433impl_leaf_widget!(TextArea);
434
435fn line_bounds(text: &str, caret: usize) -> (usize, usize) {
437 let caret = caret.min(text.len());
438 let start = text[..caret].rfind('\n').map(|i| i + 1).unwrap_or(0);
439 let end = text[caret..]
440 .find('\n')
441 .map(|i| caret + i)
442 .unwrap_or(text.len());
443 (start, end)
444}
445
446fn line_index(text: &str, caret: usize) -> usize {
448 text[..caret.min(text.len())].matches('\n').count()
449}
450
451fn nth_line_bounds(text: &str, n: usize) -> (usize, usize) {
453 let mut start = 0;
454 for _ in 0..n {
455 match text[start..].find('\n') {
456 Some(i) => start += i + 1,
457 None => return (text.len(), text.len()),
458 }
459 }
460 let end = text[start..]
461 .find('\n')
462 .map(|i| start + i)
463 .unwrap_or(text.len());
464 (start, end)
465}
466
467fn caret_x(text: &str, caret: usize, style: &TextStyle) -> f32 {
469 let (start, _) = line_bounds(text, caret);
470 crate::text_metrics::measure_text(&text[start..caret.min(text.len())], NO_WRAP_WIDTH, style).0
471}
472
473fn offset_at_line_x(text: &str, style: &TextStyle, n: usize, x: f32) -> usize {
476 let (start, end) = nth_line_bounds(text, n);
477 let line = &text[start..end];
478 let mut best = start;
479 let mut best_dx = f32::MAX;
480 let mut idx = 0;
481 loop {
482 let w = crate::text_metrics::measure_text(&line[..idx], NO_WRAP_WIDTH, style).0;
483 let dx = (w - x).abs();
484 if dx < best_dx {
485 best_dx = dx;
486 best = start + idx;
487 }
488 if idx >= line.len() {
489 break;
490 }
491 idx = next_boundary(line, idx);
492 }
493 best
494}
495
496fn prev_boundary(s: &str, i: usize) -> usize {
498 let mut j = i.min(s.len());
499 if j == 0 {
500 return 0;
501 }
502 j -= 1;
503 while j > 0 && !s.is_char_boundary(j) {
504 j -= 1;
505 }
506 j
507}
508
509fn next_boundary(s: &str, i: usize) -> usize {
511 let mut j = (i + 1).min(s.len());
512 while j < s.len() && !s.is_char_boundary(j) {
513 j += 1;
514 }
515 j
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521 use crate::context::{compute_layout, new_container, reset_layout_runtime};
522 use crate::layout_item::LayoutItem;
523 use layout_core::AvailableSpace;
524 use renderer_core::Color;
525
526 fn key(k: Key) -> Event {
527 Event::KeyPressed {
528 key: k,
529 modifiers: ModifiersState::default(),
530 }
531 }
532
533 fn chord(k: Key) -> Event {
534 Event::KeyPressed {
535 key: k,
536 modifiers: ModifiersState {
537 is_ctrl: true,
538 ..ModifiersState::default()
539 },
540 }
541 }
542
543 fn shifted(k: Key) -> Event {
544 Event::KeyPressed {
545 key: k,
546 modifiers: ModifiersState {
547 is_shift: true,
548 ..ModifiersState::default()
549 },
550 }
551 }
552
553 #[test]
554 fn typing_over_a_selection_replaces_it() {
555 let (mut area, value) = focused("one\ntwo");
556 area.on_event(&chord(Key::Char('a')));
557 area.on_event(&key(Key::Char('x')));
558 assert_eq!(value.get(), "x");
559 }
560
561 #[test]
563 fn a_selection_across_lines_cuts_whole() {
564 let (mut area, value) = focused("one\ntwo\nthree");
565 area.on_event(&chord(Key::Char('a')));
566 area.on_event(&chord(Key::Char('x')));
567 assert_eq!(value.get(), "");
568 }
569
570 #[test]
571 fn shift_arrows_grow_a_selection_and_backspace_takes_it() {
572 let (mut area, value) = focused("hello");
573 area.on_event(&shifted(Key::Named(NamedKey::ArrowLeft)));
574 area.on_event(&shifted(Key::Named(NamedKey::ArrowLeft)));
575 assert_eq!(area.selection("hello"), Some((3, 5)));
576 area.on_event(&key(Key::Named(NamedKey::Backspace)));
577 assert_eq!(value.get(), "hel");
578 }
579
580 #[test]
582 fn enter_replaces_a_selection() {
583 let (mut area, value) = focused("abcd");
584 area.on_event(&chord(Key::Char('a')));
585 area.on_event(&key(Key::Named(NamedKey::Enter)));
586 assert_eq!(value.get(), "\n");
587 }
588
589 #[test]
590 fn copy_leaves_the_text_and_the_selection_alone() {
591 let (mut area, value) = focused("one\ntwo");
592 area.on_event(&chord(Key::Char('a')));
593 assert_eq!(area.on_event(&chord(Key::Char('c'))), EventResult::Handled);
594 assert_eq!(value.get(), "one\ntwo");
595 assert_eq!(area.selection("one\ntwo"), Some((0, 7)));
596 }
597 fn focused(initial: &str) -> (TextArea, RwSignal<String>) {
598 reset_layout_runtime();
599 let value = signal(initial.to_string());
600 let area = TextArea::new(value.clone(), LayoutStyle::new().width(400.0), || {
601 TextStyle::new(14.0, Color::BLACK)
602 })
603 .unwrap();
604 let root = new_container(
605 LayoutStyle::new().flex_column().width(400.0).height(400.0),
606 &[area.layout_node()],
607 )
608 .unwrap();
609 compute_layout(
610 root,
611 AvailableSpace::Definite(400.0),
612 AvailableSpace::Definite(400.0),
613 )
614 .unwrap();
615 focus::request(area.id);
616 (area, value)
617 }
618
619 #[test]
622 fn the_shortcut_guard_covers_every_key_this_editor_edits() {
623 let plain = ModifiersState::default();
624 let named = [
625 NamedKey::Space,
626 NamedKey::Backspace,
627 NamedKey::Delete,
628 NamedKey::ArrowLeft,
629 NamedKey::ArrowRight,
630 NamedKey::ArrowUp,
631 NamedKey::ArrowDown,
632 NamedKey::Home,
633 NamedKey::End,
634 NamedKey::Enter,
635 NamedKey::Escape,
636 NamedKey::Tab,
637 NamedKey::PageUp,
638 NamedKey::F5,
639 ];
640 let keys: Vec<Key> = std::iter::once(Key::Char('x'))
641 .chain(named.into_iter().map(Key::Named))
642 .collect();
643 let style = TextStyle::new(14.0, Color::BLACK);
644 for k in keys {
645 let (mut area, _value) = focused("one\ntwo");
646 area.caret.set(5);
647 let guarded = focus::text_entry_takes_key(&k, plain);
649 let edited = area.edit(&k, &plain, &style) == EventResult::Handled;
650 assert!(
651 !edited || guarded,
652 "{k:?} is edited by the editor but the shortcut guard lets it through"
653 );
654 focus::clear();
655 }
656 }
657
658 #[test]
659 fn enter_inserts_newline_and_typing_continues_on_new_line() {
660 let (mut area, value) = focused("ab");
661 area.on_event(&key(Key::Named(NamedKey::Enter)));
662 area.on_event(&key(Key::Char('c')));
663 assert_eq!(value.get(), "ab\nc");
664 assert_eq!(line_index(&value.get(), area.caret.get()), 1);
665 }
666
667 #[test]
668 fn backspace_at_line_start_joins_lines() {
669 let (mut area, value) = focused("ab\ncd");
670 area.on_event(&key(Key::Named(NamedKey::Home)));
672 area.on_event(&key(Key::Named(NamedKey::Backspace)));
673 assert_eq!(value.get(), "abcd");
674 }
675
676 #[test]
677 fn arrow_up_down_moves_between_lines() {
678 let (mut area, value) = focused("aaaa\nbb");
679 area.on_event(&key(Key::Named(NamedKey::ArrowUp)));
681 assert_eq!(line_index(&value.get(), area.caret.get()), 0);
682 area.on_event(&key(Key::Named(NamedKey::ArrowDown)));
683 assert_eq!(line_index(&value.get(), area.caret.get()), 1);
684 }
685
686 #[test]
687 fn click_focuses_and_positions_caret_without_reborrow() {
688 use platform_core::{PointerButton, PointerSource};
689 let (mut area, _value) = focused("hello\nworld");
690 focus::clear();
691 let r = area.leaf.rect.get();
694 let handled = area.on_event(&Event::PointerPressed {
695 x: (r.x + 5.0) as f64,
696 y: (r.y + 2.0) as f64,
697 button: PointerButton::Primary,
698 source: PointerSource::Mouse,
699 });
700 assert_eq!(handled, EventResult::Handled);
701 assert!(
702 focus::is_focused(area.id),
703 "a press inside focuses the area"
704 );
705 }
706
707 #[test]
708 fn ctrl_chord_is_ignored_as_shortcut() {
709 let (mut area, value) = focused("hi");
710 let save = Event::KeyPressed {
711 key: Key::Char('s'),
712 modifiers: ModifiersState {
713 is_ctrl: true,
714 ..Default::default()
715 },
716 };
717 assert_eq!(area.on_event(&save), EventResult::Ignored);
718 assert_eq!(value.get(), "hi");
719 }
720}