1use std::cell::{Cell, RefCell};
18use std::rc::Rc;
19
20use smudge_core::clipboard;
21use smudge_core::element::{Element, Value};
22use smudge_core::geometry::{Color, Corners, Point, Rect, Size};
23use smudge_core::input::{Handled, Key, KeyEvent};
24use smudge_core::paint::PaintOp;
25use smudge_core::reactive::{RwSignal, prelude::*};
26use smudge_core::semantics::{Role, Semantics};
27use smudge_core::style::Length;
28use smudge_core::theme::ColorToken;
29use smudge_core::tree::Dirty;
30use smudge_core::widget::{AvailableSpace, MeasureInput, PaintCx, Widget};
31use smudge_core::window::Cursor;
32
33use crate::chrome::{
34 Direction, InputChrome, ParagraphCache, Placeholder, SELECTION_ALPHA, apply_edit_count,
35 apply_move, apply_outside_write, caret_node, copy_selection, cut_selection, handle_commit,
36 handle_focus, handle_move, handle_preedit, handle_press, handle_release, handle_wheel,
37 is_shortcut, outside_write, paint_paragraphs, paint_selection, types_text,
38};
39use crate::style::{TextStyle, current_text_style};
40
41pub struct TextArea {
48 chrome: Rc<InputChrome>,
50 placeholder: RefCell<Placeholder>,
52 style: TextStyle,
55 runs: RefCell<Option<ParagraphCache>>,
58 measured: Rc<Cell<f32>>,
60 relayout: RwSignal<u64>,
63}
64
65impl TextArea {
66 fn new(chrome: Rc<InputChrome>, measured: Rc<Cell<f32>>, relayout: RwSignal<u64>) -> TextArea {
70 TextArea {
71 chrome,
72 placeholder: RefCell::new(Placeholder::new()),
73 style: TextStyle::default(),
74 runs: RefCell::new(None),
75 measured,
76 relayout,
77 }
78 }
79
80 fn relayout_if_resized(&self) {
85 if self.chrome.editor.layout_size().height != self.measured.get() {
86 self.relayout.update(|count| *count += 1);
87 }
88 }
89
90 fn text_color(&self, cx: &PaintCx<'_>) -> Color {
93 match &cx.style.paint.text_color {
94 Some(color) => color.resolve(cx.theme),
95 None => self.style.color.resolve(cx.theme),
96 }
97 }
98}
99
100impl Widget for TextArea {
101 fn semantics(&self) -> Option<Semantics> {
107 let label = (!self.placeholder.borrow().text().is_empty())
108 .then(|| self.placeholder.borrow().text().into());
109 Some(Semantics {
110 role: Role::MultilineTextInput,
111 label,
112 value: Some(self.chrome.editor.text()),
113 })
114 }
115
116 fn paint(&self, cx: &PaintCx<'_>, out: &mut Vec<PaintOp>) {
131 let color = self.text_color(cx);
132 let inset = Point {
133 x: cx.content.x0 - cx.rect.x0,
134 y: cx.content.y0 - cx.rect.y0,
135 };
136 self.chrome.painted.set(Some((cx.content, color, inset)));
137 self.chrome
141 .editor
142 .set_wrap_width(Some(cx.content.width().max(0.0)));
143 let (scroll_x, scroll_y) = self.chrome.place(cx.content, color);
144 self.chrome
145 .line_height
146 .set(self.style.size * self.style.line_height);
147 let text_origin = Point {
148 x: cx.content.x0 - scroll_x,
149 y: cx.content.y0 - scroll_y,
150 };
151
152 let selection_color = Color {
153 a: SELECTION_ALPHA,
154 ..cx.theme.color(ColorToken::ACCENT)
155 };
156 paint_selection(
157 &self.chrome,
158 text_origin,
159 (scroll_y, scroll_y + cx.content.height()),
160 selection_color,
161 out,
162 );
163
164 for area in self.chrome.editor.compose_geometry() {
167 out.push(PaintOp::FillRect {
168 rect: Rect::new(
169 text_origin.x + area.x0,
170 text_origin.y + area.y0,
171 text_origin.x + area.x1,
172 text_origin.y + area.y1,
173 ),
174 radius: Corners::ZERO,
175 color,
176 });
177 }
178
179 if self.chrome.editor.is_empty()
180 && !self.chrome.editor.is_composing()
181 && !self.placeholder.borrow().text().is_empty()
182 {
183 let muted = cx.theme.color(ColorToken::TEXT_MUTED);
184 self.placeholder
185 .borrow_mut()
186 .paint(&self.style, text_origin, muted, out);
187 } else {
188 let top = scroll_y;
189 let bottom = scroll_y + cx.content.height();
190 paint_paragraphs(
191 &mut self.runs.borrow_mut(),
192 &self.chrome.editor,
193 text_origin,
194 color,
195 (top, bottom),
196 out,
197 );
198 }
199 }
200
201 fn ime_area(&self) -> Option<Rect> {
208 let (content, _, _) = self.chrome.painted.get()?;
209 let area = self.chrome.editor.ime_area();
210 let scroll_x = self.chrome.scroll_x.get();
211 let scroll_y = self.chrome.scroll_y.get();
212 Some(Rect::new(
213 content.x0 + area.x0 - scroll_x,
214 content.y0 + area.y0 - scroll_y,
215 content.x0 + area.x1 - scroll_x,
216 content.y0 + area.y1 - scroll_y,
217 ))
218 }
219}
220
221pub fn text_area(value: RwSignal<String>) -> Element<TextArea> {
258 let chrome = Rc::new(InputChrome::new(value.clone(), true));
259 let measured = Rc::new(Cell::new(0.0));
260 let relayout = RwSignal::new(0u64);
261 let relayout_source = relayout.clone();
262
263 let key_chrome = Rc::clone(&chrome);
264 let press_chrome = Rc::clone(&chrome);
265 let move_chrome = Rc::clone(&chrome);
266 let up_chrome = Rc::clone(&chrome);
267 let wheel_chrome = Rc::clone(&chrome);
268 let focus_chrome = Rc::clone(&chrome);
269 let commit_chrome = Rc::clone(&chrome);
270 let preedit_chrome = Rc::clone(&chrome);
271 let edits_chrome = Rc::clone(&chrome);
272 let caret_chrome = Rc::clone(&chrome);
273 let value_chrome = Rc::clone(&chrome);
274
275 let body = Element::new(Body {
276 chrome: Rc::clone(&chrome),
277 measured: Rc::clone(&measured),
278 })
279 .width(Length::pct(100.0))
280 .bind(
281 RelayoutKey,
282 move || relayout_source.get(),
283 |_, _| Dirty::Layout,
284 );
285
286 Element::new(TextArea::new(Rc::clone(&chrome), measured, relayout))
287 .clip(true)
288 .cursor(Cursor::Text)
289 .column()
290 .min_height(|| line_floor(Value::Static(1)))
291 .bind(AmbientStyleKey, current_text_style, apply_ambient_style)
292 .bind(
293 ValueKey,
294 move || outside_write(&value_chrome, &value),
295 apply_value,
296 )
297 .bind(EditsKey, move || edits_chrome.edits.get(), apply_edits)
298 .on_key(move |event| handle_key(&key_chrome, event))
299 .on_pointer_down(move |position| handle_press(&press_chrome, position))
300 .on_pointer_move(move |position| handle_move(&move_chrome, position))
301 .on_pointer_up(move |position| handle_release(&up_chrome, position))
302 .on_wheel(move |delta| handle_wheel(&wheel_chrome, delta))
303 .on_focus(move |focused| handle_focus(&focus_chrome, focused))
304 .on_ime_commit(move |text| handle_commit(&commit_chrome, text))
305 .on_ime_preedit(move |text, cursor| handle_preedit(&preedit_chrome, text, cursor))
306 .child(body)
307 .child(caret_node(&caret_chrome))
308}
309
310struct Body {
318 chrome: Rc<InputChrome>,
320 measured: Rc<Cell<f32>>,
323}
324
325impl Widget for Body {
326 fn measure(&mut self, input: MeasureInput) -> Option<Size> {
339 let offered = input.known_width.or(match input.available_width {
340 AvailableSpace::Definite(width) => Some(width),
341 AvailableSpace::MinContent | AvailableSpace::MaxContent => None,
342 });
343 let editor = &self.chrome.editor;
344 if let Some(width) = offered {
345 editor.set_wrap_width(Some(width.max(0.0)));
346 }
347 let height = editor.layout_size().height;
348 self.measured.set(height);
349 Some(Size {
350 width: offered.unwrap_or(0.0).max(0.0),
351 height,
352 })
353 }
354
355 fn paint(&self, _cx: &PaintCx<'_>, _out: &mut Vec<PaintOp>) {}
356}
357
358pub trait TextAreaExt: Sized {
372 fn placeholder(self, text: impl Into<Value<String>>) -> Self;
377
378 fn min_lines(self, lines: impl Into<Value<u32>>) -> Self;
385}
386
387struct AmbientStyleKey;
389
390struct ValueKey;
392
393struct EditsKey;
395
396struct PlaceholderKey;
398
399struct RelayoutKey;
401
402impl TextAreaExt for Element<TextArea> {
403 fn placeholder(self, text: impl Into<Value<String>>) -> Self {
404 self.bind(PlaceholderKey, text, set_placeholder)
405 }
406
407 fn min_lines(self, lines: impl Into<Value<u32>>) -> Self {
408 let lines = lines.into();
409 self.min_height(move || line_floor(lines.clone()))
410 }
411}
412
413fn line_floor(lines: Value<u32>) -> Length {
418 let lines = match lines {
419 Value::Static(lines) => lines,
420 Value::Dynamic(get) => get(),
421 };
422 let style = current_text_style();
423 Length::px(lines as f32 * style.size * style.line_height)
424}
425
426fn apply_ambient_style(area: &mut TextArea, ambient: TextStyle) -> Dirty {
428 if area.style == ambient {
429 return Dirty::Clean;
430 }
431 area.style = ambient;
432 area.chrome.editor.set_style(&area.style);
433 area.placeholder.borrow_mut().invalidate_style();
434 *area.runs.borrow_mut() = None;
435 area.relayout_if_resized();
436 Dirty::Layout
437}
438
439fn apply_value(area: &mut TextArea, value: Option<String>) -> Dirty {
442 match value {
443 Some(value) => {
444 let dirty = apply_outside_write(&area.chrome, &value);
445 area.relayout_if_resized();
446 dirty
447 }
448 None => Dirty::Clean,
449 }
450}
451
452fn apply_edits(area: &mut TextArea, _edits: u64) -> Dirty {
456 let dirty = apply_edit_count(&area.chrome);
457 area.relayout_if_resized();
458 dirty
459}
460
461fn set_placeholder(area: &mut TextArea, text: String) -> Dirty {
463 let mut placeholder = area.placeholder.borrow_mut();
464 if placeholder.text() == text {
465 return Dirty::Clean;
466 }
467 placeholder.set_text(text);
468 Dirty::Paint
469}
470
471fn visible_lines(chrome: &InputChrome) -> isize {
477 let content = chrome
478 .painted
479 .get()
480 .map(|(content, _, _)| content.height())
481 .unwrap_or(0.0);
482 let line = chrome.line_height.get().max(1.0);
483 ((content / line) as isize).max(1)
484}
485
486fn handle_key(chrome: &InputChrome, event: &KeyEvent) -> Handled {
496 if !event.pressed {
497 return Handled::No;
498 }
499 let modifiers = event.modifiers;
500 let by_word = modifiers.control || modifiers.alt;
501 match &event.key {
502 Key::Character(text) if is_shortcut(modifiers) && text.eq_ignore_ascii_case("a") => {
503 chrome.editor.select_all();
504 chrome.touched();
505 Handled::Yes
506 }
507 Key::Character(text) if is_shortcut(modifiers) && text.eq_ignore_ascii_case("c") => {
508 copy_selection(chrome);
509 Handled::Yes
510 }
511 Key::Character(text) if is_shortcut(modifiers) && text.eq_ignore_ascii_case("x") => {
512 if cut_selection(chrome) {
513 chrome.touched();
514 }
515 Handled::Yes
516 }
517 Key::Character(text) if is_shortcut(modifiers) && text.eq_ignore_ascii_case("v") => {
518 paste_from_clipboard(chrome);
519 Handled::Yes
520 }
521 Key::Character(text) if is_shortcut(modifiers) && text.eq_ignore_ascii_case("z") => {
522 if modifiers.shift {
523 chrome.editor.redo();
524 } else {
525 chrome.editor.undo();
526 }
527 chrome.touched();
528 Handled::Yes
529 }
530 Key::Character(text)
531 if cfg!(not(target_os = "macos"))
532 && is_shortcut(modifiers)
533 && text.eq_ignore_ascii_case("y") =>
534 {
535 chrome.editor.redo();
536 chrome.touched();
537 Handled::Yes
538 }
539 Key::Enter if modifiers.control || modifiers.alt || modifiers.meta => Handled::No,
540 Key::Enter => {
541 chrome.editor.insert("\n");
542 chrome.touched();
543 Handled::Yes
544 }
545 Key::Backspace => {
546 if by_word {
547 chrome.editor.backspace_word();
548 } else {
549 chrome.editor.backspace();
550 }
551 chrome.touched();
552 Handled::Yes
553 }
554 Key::Delete => {
555 if by_word {
556 chrome.editor.delete_word();
557 } else {
558 chrome.editor.delete();
559 }
560 chrome.touched();
561 Handled::Yes
562 }
563 Key::ArrowLeft => {
564 apply_move(chrome, modifiers.shift, by_word, Direction::Left);
565 Handled::Yes
566 }
567 Key::ArrowRight => {
568 apply_move(chrome, modifiers.shift, by_word, Direction::Right);
569 Handled::Yes
570 }
571 Key::ArrowUp => {
572 if modifiers.shift {
573 chrome.editor.extend_up();
574 } else {
575 chrome.editor.move_up();
576 }
577 chrome.touched();
578 Handled::Yes
579 }
580 Key::ArrowDown => {
581 if modifiers.shift {
582 chrome.editor.extend_down();
583 } else {
584 chrome.editor.move_down();
585 }
586 chrome.touched();
587 Handled::Yes
588 }
589 Key::PageUp => {
590 let lines = visible_lines(chrome);
591 if modifiers.shift {
592 chrome.editor.extend_lines(-lines);
593 } else {
594 chrome.editor.move_lines(-lines);
595 }
596 chrome.touched();
597 Handled::Yes
598 }
599 Key::PageDown => {
600 let lines = visible_lines(chrome);
601 if modifiers.shift {
602 chrome.editor.extend_lines(lines);
603 } else {
604 chrome.editor.move_lines(lines);
605 }
606 chrome.touched();
607 Handled::Yes
608 }
609 Key::Home => {
610 let editor = &chrome.editor;
611 match (modifiers.shift, modifiers.control || modifiers.meta) {
612 (false, false) => editor.move_to_line_start(),
613 (true, false) => editor.extend_to_line_start(),
614 (false, true) => editor.move_to_start(),
615 (true, true) => editor.extend_to_start(),
616 }
617 chrome.touched();
618 Handled::Yes
619 }
620 Key::End => {
621 let editor = &chrome.editor;
622 match (modifiers.shift, modifiers.control || modifiers.meta) {
623 (false, false) => editor.move_to_line_end(),
624 (true, false) => editor.extend_to_line_end(),
625 (false, true) => editor.move_to_end(),
626 (true, true) => editor.extend_to_end(),
627 }
628 chrome.touched();
629 Handled::Yes
630 }
631 Key::Character(text) => {
632 if !types_text(modifiers) {
633 return Handled::No;
634 }
635 chrome.editor.insert(text);
636 chrome.touched();
637 Handled::Yes
638 }
639 Key::Space => {
640 if !types_text(modifiers) {
641 return Handled::No;
642 }
643 chrome.editor.insert(" ");
644 chrome.touched();
645 Handled::Yes
646 }
647 _ => Handled::No,
648 }
649}
650
651fn paste_from_clipboard(chrome: &InputChrome) {
656 if let Some(text) = clipboard::with(|slot| slot.get()).flatten() {
657 chrome.editor.paste(&text);
658 chrome.touched();
659 }
660}