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 {
32 value: RwSignal<String>,
33 caret: RwSignal<usize>,
35 style: Rc<dyn Fn() -> TextStyle>,
36 id: FocusId,
37 leaf: LayoutLeaf,
38 placeholder: String,
39 _remeasure: Effect,
42}
43
44impl TextArea {
45 pub fn new(
46 value: RwSignal<String>,
47 layout_style: LayoutStyle,
48 style_fn: impl Fn() -> TextStyle + 'static,
49 ) -> Result<Self, LayoutError> {
50 let style: Rc<dyn Fn() -> TextStyle> = Rc::new(style_fn);
51 let measure_value = value.clone();
54 let measure_style = Rc::clone(&style);
55 let measure = Box::new(move |_max_width: f32| {
56 let s = (measure_style)();
57 let line_h = crate::text_metrics::line_height(s.font_size);
58 let lines = measure_value.with(|t| t.matches('\n').count() + 1);
59 (0.0, lines as f32 * line_h)
60 });
61 let (node, rect) = new_measured_leaf(layout_style.align_self_stretch(), measure)?;
62 let caret = value.with(|s| s.len());
63 let id = focus::next_id();
64 focus::register_with_role(
65 id,
66 focus::FocusKind::TextEntry,
67 node,
68 focus::Role::MultilineTextInput,
69 );
70 let remeasure = {
71 let value = value.clone();
72 effect(move || {
73 value.with(|_| {});
75 mark_dirty(node).ok();
76 })
77 };
78 Ok(Self {
79 value,
80 caret: signal(caret),
81 style,
82 id,
83 leaf: LayoutLeaf { node, rect },
84 placeholder: String::new(),
85 _remeasure: remeasure,
86 })
87 }
88
89 pub fn placeholder(mut self, p: impl Into<String>) -> Self {
91 self.placeholder = p.into();
92 self
93 }
94
95 pub fn request_focus(&self) {
98 focus::request(self.id);
99 }
100
101 pub fn focused(&self) -> bool {
103 focus::is_focused(self.id)
104 }
105
106 pub fn focus_handle(&self) -> focus::FocusHandle {
109 focus::handle(self.id)
110 }
111
112 fn line_height(&self) -> f32 {
113 crate::text_metrics::line_height((self.style)().font_size)
114 }
115
116 fn caret_at(&self, text: &str) -> usize {
118 let mut c = self.caret.get().min(text.len());
119 while c > 0 && !text.is_char_boundary(c) {
120 c -= 1;
121 }
122 c
123 }
124
125 fn edit(&mut self, key: &Key, mods: &ModifiersState, style: &TextStyle) -> EventResult {
129 let mut text = self.value.get();
130 let mut caret = self.caret_at(&text);
131 let mut changed = false;
132 match key {
133 Key::Char(_) if mods.is_ctrl || mods.is_meta => return EventResult::Ignored,
135 Key::Char(c) if !c.is_control() => {
136 text.insert(caret, *c);
137 caret += c.len_utf8();
138 changed = true;
139 }
140 Key::Named(NamedKey::Space) => {
141 text.insert(caret, ' ');
142 caret += 1;
143 changed = true;
144 }
145 Key::Named(NamedKey::Enter) => {
146 text.insert(caret, '\n');
147 caret += 1;
148 changed = true;
149 }
150 Key::Named(NamedKey::Tab) => {
151 text.insert_str(caret, TAB_INSERT);
152 caret += TAB_INSERT.len();
153 changed = true;
154 }
155 Key::Named(NamedKey::Backspace) => {
156 if caret == 0 {
157 return EventResult::Ignored;
158 }
159 let prev = prev_boundary(&text, caret);
160 text.replace_range(prev..caret, "");
161 caret = prev;
162 changed = true;
163 }
164 Key::Named(NamedKey::Delete) => {
165 if caret >= text.len() {
166 return EventResult::Ignored;
167 }
168 let next = next_boundary(&text, caret);
169 text.replace_range(caret..next, "");
170 changed = true;
171 }
172 Key::Named(NamedKey::ArrowLeft) => caret = prev_boundary(&text, caret),
173 Key::Named(NamedKey::ArrowRight) => caret = next_boundary(&text, caret),
174 Key::Named(NamedKey::ArrowUp) => {
175 let line = line_index(&text, caret);
176 if line > 0 {
177 let x = caret_x(&text, caret, style);
178 caret = offset_at_line_x(&text, style, line - 1, x);
179 } else {
180 caret = 0;
181 }
182 }
183 Key::Named(NamedKey::ArrowDown) => {
184 let line = line_index(&text, caret);
185 let last = text.matches('\n').count();
186 if line < last {
187 let x = caret_x(&text, caret, style);
188 caret = offset_at_line_x(&text, style, line + 1, x);
189 } else {
190 caret = text.len();
191 }
192 }
193 Key::Named(NamedKey::Home) => caret = line_bounds(&text, caret).0,
194 Key::Named(NamedKey::End) => caret = line_bounds(&text, caret).1,
195 Key::Named(NamedKey::Escape) => {
196 focus::release(self.id);
197 return EventResult::Handled;
198 }
199 _ => return EventResult::Ignored,
200 }
201 if changed {
202 self.value.set(text);
205 }
206 self.caret.set(caret);
207 EventResult::Handled
208 }
209}
210
211impl Component for TextArea {
212 fn view(&self) -> RenderNode {
213 let r = self.leaf.rect.get();
214 let text = self.value.get();
215 let style = (self.style)();
216 let line_h = self.line_height();
217 let full = Rect {
220 x: 0.0,
221 y: 0.0,
222 width: NO_WRAP_WIDTH,
223 height: r.height.max(line_h),
224 };
225 let text_node = if text.is_empty() && !self.placeholder.is_empty() {
226 let muted = match style.paint {
227 Paint::Solid(c) => Paint::Solid(c.with_alpha(c.a * 0.5)),
228 _ => Paint::Solid(Color::rgba(0.5, 0.5, 0.55, 0.5)),
229 };
230 let mut ph_style = style;
231 ph_style.paint = muted;
232 RenderNode::text(self.placeholder.clone(), full, ph_style)
233 } else {
234 RenderNode::text(Arc::<str>::from(text.as_str()), full, style)
235 };
236
237 if focus::is_focused(self.id) {
238 let caret = self.caret_at(&text);
239 let line = line_index(&text, caret);
240 let x = caret_x(&text, caret, &style);
241 let caret_rect = Rect {
242 x,
243 y: line as f32 * line_h,
244 width: CARET_WIDTH,
245 height: line_h,
246 };
247 let caret_node =
248 RenderNode::rect(caret_rect, RectStyle::default().with_fill(style.paint));
249 self.leaf
250 .at_layout_position(RenderNode::group([text_node, caret_node]))
251 } else {
252 self.leaf.at_layout_position(text_node)
253 }
254 }
255
256 fn on_event(&mut self, event: &Event) -> EventResult {
257 let rect = self.leaf.rect.get();
258 match event {
259 Event::PointerPressed {
260 x,
261 y,
262 button: PointerButton::Primary,
263 ..
264 } => {
265 if rect.contains(*x as f32, *y as f32) {
266 focus::request_from_pointer(self.id);
267 let style = (self.style)();
268 let line_h = crate::text_metrics::line_height(style.font_size);
269 let text = self.value.get();
272 let local_y = (*y as f32 - rect.y).max(0.0);
273 let local_x = (*x as f32 - rect.x).max(0.0);
274 let last = text.matches('\n').count();
275 let line = ((local_y / line_h).floor() as usize).min(last);
276 self.caret
277 .set(offset_at_line_x(&text, &style, line, local_x));
278 EventResult::Handled
279 } else {
280 EventResult::Ignored
281 }
282 }
283 Event::KeyPressed { key, modifiers } if focus::is_focused(self.id) => {
284 let style = (self.style)();
285 self.edit(key, modifiers, &style)
286 }
287 _ => EventResult::Ignored,
288 }
289 }
290
291 fn debug_name(&self) -> &'static str {
292 "TextArea"
293 }
294}
295
296impl Drop for TextArea {
297 fn drop(&mut self) {
298 focus::unregister(self.id);
299 }
300}
301
302impl_leaf_widget!(TextArea);
303
304fn line_bounds(text: &str, caret: usize) -> (usize, usize) {
306 let caret = caret.min(text.len());
307 let start = text[..caret].rfind('\n').map(|i| i + 1).unwrap_or(0);
308 let end = text[caret..]
309 .find('\n')
310 .map(|i| caret + i)
311 .unwrap_or(text.len());
312 (start, end)
313}
314
315fn line_index(text: &str, caret: usize) -> usize {
317 text[..caret.min(text.len())].matches('\n').count()
318}
319
320fn nth_line_bounds(text: &str, n: usize) -> (usize, usize) {
322 let mut start = 0;
323 for _ in 0..n {
324 match text[start..].find('\n') {
325 Some(i) => start += i + 1,
326 None => return (text.len(), text.len()),
327 }
328 }
329 let end = text[start..]
330 .find('\n')
331 .map(|i| start + i)
332 .unwrap_or(text.len());
333 (start, end)
334}
335
336fn caret_x(text: &str, caret: usize, style: &TextStyle) -> f32 {
338 let (start, _) = line_bounds(text, caret);
339 crate::text_metrics::measure_text(&text[start..caret.min(text.len())], NO_WRAP_WIDTH, style).0
340}
341
342fn offset_at_line_x(text: &str, style: &TextStyle, n: usize, x: f32) -> usize {
345 let (start, end) = nth_line_bounds(text, n);
346 let line = &text[start..end];
347 let mut best = start;
348 let mut best_dx = f32::MAX;
349 let mut idx = 0;
350 loop {
351 let w = crate::text_metrics::measure_text(&line[..idx], NO_WRAP_WIDTH, style).0;
352 let dx = (w - x).abs();
353 if dx < best_dx {
354 best_dx = dx;
355 best = start + idx;
356 }
357 if idx >= line.len() {
358 break;
359 }
360 idx = next_boundary(line, idx);
361 }
362 best
363}
364
365fn prev_boundary(s: &str, i: usize) -> usize {
367 let mut j = i.min(s.len());
368 if j == 0 {
369 return 0;
370 }
371 j -= 1;
372 while j > 0 && !s.is_char_boundary(j) {
373 j -= 1;
374 }
375 j
376}
377
378fn next_boundary(s: &str, i: usize) -> usize {
380 let mut j = (i + 1).min(s.len());
381 while j < s.len() && !s.is_char_boundary(j) {
382 j += 1;
383 }
384 j
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390 use crate::context::{compute_layout, new_container, reset_layout_runtime};
391 use crate::layout_item::LayoutItem;
392 use layout_core::AvailableSpace;
393 use renderer_core::Color;
394
395 fn key(k: Key) -> Event {
396 Event::KeyPressed {
397 key: k,
398 modifiers: ModifiersState::default(),
399 }
400 }
401
402 fn focused(initial: &str) -> (TextArea, RwSignal<String>) {
403 reset_layout_runtime();
404 let value = signal(initial.to_string());
405 let area = TextArea::new(value.clone(), LayoutStyle::new().width(400.0), || {
406 TextStyle::new(14.0, Color::BLACK)
407 })
408 .unwrap();
409 let root = new_container(
410 LayoutStyle::new().flex_column().width(400.0).height(400.0),
411 &[area.layout_node()],
412 )
413 .unwrap();
414 compute_layout(
415 root,
416 AvailableSpace::Definite(400.0),
417 AvailableSpace::Definite(400.0),
418 )
419 .unwrap();
420 focus::request(area.id);
421 (area, value)
422 }
423
424 #[test]
427 fn the_shortcut_guard_covers_every_key_this_editor_edits() {
428 let plain = ModifiersState::default();
429 let named = [
430 NamedKey::Space,
431 NamedKey::Backspace,
432 NamedKey::Delete,
433 NamedKey::ArrowLeft,
434 NamedKey::ArrowRight,
435 NamedKey::ArrowUp,
436 NamedKey::ArrowDown,
437 NamedKey::Home,
438 NamedKey::End,
439 NamedKey::Enter,
440 NamedKey::Escape,
441 NamedKey::Tab,
442 NamedKey::PageUp,
443 NamedKey::F5,
444 ];
445 let keys: Vec<Key> = std::iter::once(Key::Char('x'))
446 .chain(named.into_iter().map(Key::Named))
447 .collect();
448 let style = TextStyle::new(14.0, Color::BLACK);
449 for k in keys {
450 let (mut area, _value) = focused("one\ntwo");
451 area.caret.set(5);
452 let guarded = focus::text_entry_takes_key(&k, plain);
454 let edited = area.edit(&k, &plain, &style) == EventResult::Handled;
455 assert!(
456 !edited || guarded,
457 "{k:?} is edited by the editor but the shortcut guard lets it through"
458 );
459 focus::clear();
460 }
461 }
462
463 #[test]
464 fn enter_inserts_newline_and_typing_continues_on_new_line() {
465 let (mut area, value) = focused("ab");
466 area.on_event(&key(Key::Named(NamedKey::Enter)));
467 area.on_event(&key(Key::Char('c')));
468 assert_eq!(value.get(), "ab\nc");
469 assert_eq!(line_index(&value.get(), area.caret.get()), 1);
470 }
471
472 #[test]
473 fn backspace_at_line_start_joins_lines() {
474 let (mut area, value) = focused("ab\ncd");
475 area.on_event(&key(Key::Named(NamedKey::Home)));
477 area.on_event(&key(Key::Named(NamedKey::Backspace)));
478 assert_eq!(value.get(), "abcd");
479 }
480
481 #[test]
482 fn arrow_up_down_moves_between_lines() {
483 let (mut area, value) = focused("aaaa\nbb");
484 area.on_event(&key(Key::Named(NamedKey::ArrowUp)));
486 assert_eq!(line_index(&value.get(), area.caret.get()), 0);
487 area.on_event(&key(Key::Named(NamedKey::ArrowDown)));
488 assert_eq!(line_index(&value.get(), area.caret.get()), 1);
489 }
490
491 #[test]
492 fn click_focuses_and_positions_caret_without_reborrow() {
493 use platform_core::{PointerButton, PointerSource};
494 let (mut area, _value) = focused("hello\nworld");
495 focus::clear();
496 let r = area.leaf.rect.get();
499 let handled = area.on_event(&Event::PointerPressed {
500 x: (r.x + 5.0) as f64,
501 y: (r.y + 2.0) as f64,
502 button: PointerButton::Primary,
503 source: PointerSource::Mouse,
504 });
505 assert_eq!(handled, EventResult::Handled);
506 assert!(
507 focus::is_focused(area.id),
508 "a press inside focuses the area"
509 );
510 }
511
512 #[test]
513 fn ctrl_chord_is_ignored_as_shortcut() {
514 let (mut area, value) = focused("hi");
515 let save = Event::KeyPressed {
516 key: Key::Char('s'),
517 modifiers: ModifiersState {
518 is_ctrl: true,
519 ..Default::default()
520 },
521 };
522 assert_eq!(area.on_event(&save), EventResult::Ignored);
523 assert_eq!(value.get(), "hi");
524 }
525}