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 = s.font_size * renderer_text::LINE_HEIGHT_FACTOR;
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(id);
65 let remeasure = {
66 let value = value.clone();
67 effect(move || {
68 value.with(|_| {});
70 mark_dirty(node).ok();
71 })
72 };
73 Ok(Self {
74 value,
75 caret: signal(caret),
76 style,
77 id,
78 leaf: LayoutLeaf { node, rect },
79 placeholder: String::new(),
80 _remeasure: remeasure,
81 })
82 }
83
84 pub fn placeholder(mut self, p: impl Into<String>) -> Self {
86 self.placeholder = p.into();
87 self
88 }
89
90 pub fn request_focus(&self) {
93 focus::request(self.id);
94 }
95
96 pub fn focused(&self) -> bool {
98 focus::is_focused(self.id)
99 }
100
101 pub fn focus_handle(&self) -> focus::FocusHandle {
104 focus::handle(self.id)
105 }
106
107 fn line_height(&self) -> f32 {
108 (self.style)().font_size * renderer_text::LINE_HEIGHT_FACTOR
109 }
110
111 fn caret_at(&self, text: &str) -> usize {
113 let mut c = self.caret.get().min(text.len());
114 while c > 0 && !text.is_char_boundary(c) {
115 c -= 1;
116 }
117 c
118 }
119
120 fn edit(&mut self, key: &Key, mods: &ModifiersState, style: &TextStyle) -> EventResult {
124 let mut text = self.value.get();
125 let mut caret = self.caret_at(&text);
126 let mut changed = false;
127 match key {
128 Key::Char(_) if mods.is_ctrl || mods.is_meta => return EventResult::Ignored,
130 Key::Char(c) if !c.is_control() => {
131 text.insert(caret, *c);
132 caret += c.len_utf8();
133 changed = true;
134 }
135 Key::Named(NamedKey::Space) => {
136 text.insert(caret, ' ');
137 caret += 1;
138 changed = true;
139 }
140 Key::Named(NamedKey::Enter) => {
141 text.insert(caret, '\n');
142 caret += 1;
143 changed = true;
144 }
145 Key::Named(NamedKey::Tab) => {
146 text.insert_str(caret, TAB_INSERT);
147 caret += TAB_INSERT.len();
148 changed = true;
149 }
150 Key::Named(NamedKey::Backspace) => {
151 if caret == 0 {
152 return EventResult::Ignored;
153 }
154 let prev = prev_boundary(&text, caret);
155 text.replace_range(prev..caret, "");
156 caret = prev;
157 changed = true;
158 }
159 Key::Named(NamedKey::Delete) => {
160 if caret >= text.len() {
161 return EventResult::Ignored;
162 }
163 let next = next_boundary(&text, caret);
164 text.replace_range(caret..next, "");
165 changed = true;
166 }
167 Key::Named(NamedKey::ArrowLeft) => caret = prev_boundary(&text, caret),
168 Key::Named(NamedKey::ArrowRight) => caret = next_boundary(&text, caret),
169 Key::Named(NamedKey::ArrowUp) => {
170 let line = line_index(&text, caret);
171 if line > 0 {
172 let x = caret_x(&text, caret, style);
173 caret = offset_at_line_x(&text, style, line - 1, x);
174 } else {
175 caret = 0;
176 }
177 }
178 Key::Named(NamedKey::ArrowDown) => {
179 let line = line_index(&text, caret);
180 let last = text.matches('\n').count();
181 if line < last {
182 let x = caret_x(&text, caret, style);
183 caret = offset_at_line_x(&text, style, line + 1, x);
184 } else {
185 caret = text.len();
186 }
187 }
188 Key::Named(NamedKey::Home) => caret = line_bounds(&text, caret).0,
189 Key::Named(NamedKey::End) => caret = line_bounds(&text, caret).1,
190 Key::Named(NamedKey::Escape) => {
191 focus::release(self.id);
192 return EventResult::Handled;
193 }
194 _ => return EventResult::Ignored,
195 }
196 if changed {
197 self.value.set(text);
200 }
201 self.caret.set(caret);
202 EventResult::Handled
203 }
204}
205
206impl Component for TextArea {
207 fn view(&self) -> RenderNode {
208 let r = self.leaf.rect.get();
209 let text = self.value.get();
210 let style = (self.style)();
211 let line_h = self.line_height();
212 let full = Rect {
215 x: 0.0,
216 y: 0.0,
217 width: NO_WRAP_WIDTH,
218 height: r.height.max(line_h),
219 };
220 let text_node = if text.is_empty() && !self.placeholder.is_empty() {
221 let muted = match style.paint {
222 Paint::Solid(c) => Paint::Solid(c.with_alpha(c.a * 0.5)),
223 _ => Paint::Solid(Color::rgba(0.5, 0.5, 0.55, 0.5)),
224 };
225 let mut ph_style = style;
226 ph_style.paint = muted;
227 RenderNode::text(self.placeholder.clone(), full, ph_style)
228 } else {
229 RenderNode::text(Arc::<str>::from(text.as_str()), full, style)
230 };
231
232 if focus::is_focused(self.id) {
233 let caret = self.caret_at(&text);
234 let line = line_index(&text, caret);
235 let x = caret_x(&text, caret, &style);
236 let caret_rect = Rect {
237 x,
238 y: line as f32 * line_h,
239 width: CARET_WIDTH,
240 height: line_h,
241 };
242 let caret_node =
243 RenderNode::rect(caret_rect, RectStyle::default().with_fill(style.paint));
244 self.leaf
245 .at_layout_position(RenderNode::group([text_node, caret_node]))
246 } else {
247 self.leaf.at_layout_position(text_node)
248 }
249 }
250
251 fn on_event(&mut self, event: &Event) -> EventResult {
252 let rect = self.leaf.rect.get();
253 match event {
254 Event::PointerPressed {
255 x,
256 y,
257 button: PointerButton::Primary,
258 ..
259 } => {
260 if rect.contains(*x as f32, *y as f32) {
261 focus::request(self.id);
262 let style = (self.style)();
263 let line_h = style.font_size * renderer_text::LINE_HEIGHT_FACTOR;
264 let text = self.value.get();
267 let local_y = (*y as f32 - rect.y).max(0.0);
268 let local_x = (*x as f32 - rect.x).max(0.0);
269 let last = text.matches('\n').count();
270 let line = ((local_y / line_h).floor() as usize).min(last);
271 self.caret
272 .set(offset_at_line_x(&text, &style, line, local_x));
273 EventResult::Handled
274 } else {
275 EventResult::Ignored
276 }
277 }
278 Event::KeyPressed { key, modifiers } if focus::is_focused(self.id) => {
279 let style = (self.style)();
280 self.edit(key, modifiers, &style)
281 }
282 _ => EventResult::Ignored,
283 }
284 }
285
286 fn debug_name(&self) -> &'static str {
287 "TextArea"
288 }
289}
290
291impl Drop for TextArea {
292 fn drop(&mut self) {
293 focus::unregister(self.id);
294 }
295}
296
297impl_leaf_widget!(TextArea);
298
299fn line_bounds(text: &str, caret: usize) -> (usize, usize) {
301 let caret = caret.min(text.len());
302 let start = text[..caret].rfind('\n').map(|i| i + 1).unwrap_or(0);
303 let end = text[caret..]
304 .find('\n')
305 .map(|i| caret + i)
306 .unwrap_or(text.len());
307 (start, end)
308}
309
310fn line_index(text: &str, caret: usize) -> usize {
312 text[..caret.min(text.len())].matches('\n').count()
313}
314
315fn nth_line_bounds(text: &str, n: usize) -> (usize, usize) {
317 let mut start = 0;
318 for _ in 0..n {
319 match text[start..].find('\n') {
320 Some(i) => start += i + 1,
321 None => return (text.len(), text.len()),
322 }
323 }
324 let end = text[start..]
325 .find('\n')
326 .map(|i| start + i)
327 .unwrap_or(text.len());
328 (start, end)
329}
330
331fn caret_x(text: &str, caret: usize, style: &TextStyle) -> f32 {
333 let (start, _) = line_bounds(text, caret);
334 renderer_text::measure_text(&text[start..caret.min(text.len())], NO_WRAP_WIDTH, style).0
335}
336
337fn offset_at_line_x(text: &str, style: &TextStyle, n: usize, x: f32) -> usize {
340 let (start, end) = nth_line_bounds(text, n);
341 let line = &text[start..end];
342 let mut best = start;
343 let mut best_dx = f32::MAX;
344 let mut idx = 0;
345 loop {
346 let w = renderer_text::measure_text(&line[..idx], NO_WRAP_WIDTH, style).0;
347 let dx = (w - x).abs();
348 if dx < best_dx {
349 best_dx = dx;
350 best = start + idx;
351 }
352 if idx >= line.len() {
353 break;
354 }
355 idx = next_boundary(line, idx);
356 }
357 best
358}
359
360fn prev_boundary(s: &str, i: usize) -> usize {
362 let mut j = i.min(s.len());
363 if j == 0 {
364 return 0;
365 }
366 j -= 1;
367 while j > 0 && !s.is_char_boundary(j) {
368 j -= 1;
369 }
370 j
371}
372
373fn next_boundary(s: &str, i: usize) -> usize {
375 let mut j = (i + 1).min(s.len());
376 while j < s.len() && !s.is_char_boundary(j) {
377 j += 1;
378 }
379 j
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use crate::context::{compute_layout, new_container, reset_layout_runtime};
386 use crate::layout_item::LayoutItem;
387 use layout_core::AvailableSpace;
388 use renderer_core::Color;
389
390 fn key(k: Key) -> Event {
391 Event::KeyPressed {
392 key: k,
393 modifiers: ModifiersState::default(),
394 }
395 }
396
397 fn focused(initial: &str) -> (TextArea, RwSignal<String>) {
398 reset_layout_runtime();
399 let value = signal(initial.to_string());
400 let area = TextArea::new(value.clone(), LayoutStyle::new().width(400.0), || {
401 TextStyle::new(14.0, Color::BLACK)
402 })
403 .unwrap();
404 let root = new_container(
405 LayoutStyle::new().flex_column().width(400.0).height(400.0),
406 &[area.layout_node()],
407 )
408 .unwrap();
409 compute_layout(
410 root,
411 AvailableSpace::Definite(400.0),
412 AvailableSpace::Definite(400.0),
413 )
414 .unwrap();
415 focus::request(area.id);
416 (area, value)
417 }
418
419 #[test]
420 fn enter_inserts_newline_and_typing_continues_on_new_line() {
421 let (mut area, value) = focused("ab");
422 area.on_event(&key(Key::Named(NamedKey::Enter)));
423 area.on_event(&key(Key::Char('c')));
424 assert_eq!(value.get(), "ab\nc");
425 assert_eq!(line_index(&value.get(), area.caret.get()), 1);
426 }
427
428 #[test]
429 fn backspace_at_line_start_joins_lines() {
430 let (mut area, value) = focused("ab\ncd");
431 area.on_event(&key(Key::Named(NamedKey::Home)));
433 area.on_event(&key(Key::Named(NamedKey::Backspace)));
434 assert_eq!(value.get(), "abcd");
435 }
436
437 #[test]
438 fn arrow_up_down_moves_between_lines() {
439 let (mut area, value) = focused("aaaa\nbb");
440 area.on_event(&key(Key::Named(NamedKey::ArrowUp)));
442 assert_eq!(line_index(&value.get(), area.caret.get()), 0);
443 area.on_event(&key(Key::Named(NamedKey::ArrowDown)));
444 assert_eq!(line_index(&value.get(), area.caret.get()), 1);
445 }
446
447 #[test]
448 fn click_focuses_and_positions_caret_without_reborrow() {
449 use platform_core::{PointerButton, PointerSource};
450 let (mut area, _value) = focused("hello\nworld");
451 focus::clear();
452 let r = area.leaf.rect.get();
455 let handled = area.on_event(&Event::PointerPressed {
456 x: (r.x + 5.0) as f64,
457 y: (r.y + 2.0) as f64,
458 button: PointerButton::Primary,
459 source: PointerSource::Mouse,
460 });
461 assert_eq!(handled, EventResult::Handled);
462 assert!(
463 focus::is_focused(area.id),
464 "a press inside focuses the area"
465 );
466 }
467
468 #[test]
469 fn ctrl_chord_is_ignored_as_shortcut() {
470 let (mut area, value) = focused("hi");
471 let save = Event::KeyPressed {
472 key: Key::Char('s'),
473 modifiers: ModifiersState {
474 is_ctrl: true,
475 ..Default::default()
476 },
477 };
478 assert_eq!(area.on_event(&save), EventResult::Ignored);
479 assert_eq!(value.get(), "hi");
480 }
481}