1#![deny(missing_docs)]
2use crate::action::{exec_after, set_ime_allowed, set_ime_cursor_area};
3use crate::event::{EventListener, EventPropagation};
4use crate::reactive::{Effect, RwSignal};
5use crate::style::{FontFamily, FontProps, PaddingProp, SelectionStyle, StyleClass, TextAlignProp};
6use crate::style::{FontStyle, FontWeight, TextColor};
7use crate::unit::{PxPct, PxPctAuto};
8use crate::view::ViewId;
9use crate::views::editor::text::Preedit;
10use crate::{Clipboard, prop_extractor, style_class};
11use floem_reactive::{SignalGet, SignalUpdate, SignalWith};
12use taffy::prelude::{Layout, NodeId};
13
14use floem_renderer::Renderer;
15use ui_events::keyboard::{Key, KeyState, KeyboardEvent, Modifiers, NamedKey};
16use ui_events::pointer::{PointerButton, PointerButtonEvent, PointerEvent};
17use unicode_segmentation::UnicodeSegmentation;
18
19use crate::{peniko::color::palette, style::Style, view::View};
20
21use std::{any::Any, ops::Range};
22
23use crate::platform::{Duration, Instant};
24use crate::text::{Attrs, AttrsList, FamilyOwned, TextLayout};
25
26use peniko::Brush;
27use peniko::kurbo::{Point, Rect, Size};
28
29use crate::{
30 context::{EventCx, UpdateCx},
31 event::Event,
32};
33
34use super::Decorators;
35
36style_class!(
37 pub TextInputClass
39);
40style_class!(
41 pub PlaceholderTextClass
43);
44
45prop_extractor! {
46 Extractor {
47 color: TextColor,
48 text_align: TextAlignProp,
49 }
50}
51
52prop_extractor! {
53 PlaceholderStyle {
54 pub color: TextColor,
55 pub font_weight: FontWeight,
57 pub font_style: FontStyle,
58 pub font_family: FontFamily,
59 pub text_align: TextAlignProp,
60 }
61}
62
63struct BufferState {
65 buffer: RwSignal<String>,
66 last_buffer: String,
67}
68
69impl BufferState {
70 fn update(&mut self, update: impl FnOnce(&mut String)) {
71 self.buffer.update(|s| {
72 let last = s.clone();
73 update(s);
74 self.last_buffer = last;
75 });
76 }
77
78 fn get_untracked(&self) -> String {
79 self.buffer.get_untracked()
80 }
81
82 fn with_untracked<T>(&self, f: impl FnOnce(&String) -> T) -> T {
83 self.buffer.with_untracked(f)
84 }
85}
86
87pub struct TextInput {
89 id: ViewId,
90 buffer: BufferState,
91 placeholder_text: Option<String>,
93 on_enter: Option<Box<dyn Fn()>>,
94 placeholder_style: PlaceholderStyle,
95 selection_style: SelectionStyle,
96 preedit: Option<Preedit>,
97 cursor_glyph_idx: usize,
99 cursor_x: f64,
101 text_buf: TextLayout,
102 text_node: Option<NodeId>,
103 clip_start_x: f64,
107 selection: Option<Range<usize>>,
108 width: f32,
109 height: f32,
110 glyph_max_size: Size,
112 style: Extractor,
113 font: FontProps,
114 cursor_width: f64, is_focused: bool,
116 last_pointer_down: Point,
117 last_cursor_action_on: Instant,
118 window_origin: Option<Point>,
119 last_ime_cursor_area: Option<(Point, Size)>,
120}
121
122#[derive(Clone, Copy, Debug)]
124pub enum Movement {
125 Glyph,
127 Word,
129 Line,
131}
132
133#[derive(Clone, Copy, Debug)]
135pub enum TextDirection {
136 Left,
138 Right,
140}
141
142pub fn text_input(buffer: RwSignal<String>) -> TextInput {
196 let id = ViewId::new();
197 let is_focused = RwSignal::new(false);
198
199 {
200 Effect::new(move |_| {
201 buffer.with(|_| {});
203 id.update_state(is_focused.get());
204 });
205 }
206
207 TextInput {
208 id,
209 cursor_glyph_idx: 0,
210 placeholder_text: None,
211 placeholder_style: Default::default(),
212 selection_style: Default::default(),
213 preedit: None,
214 buffer: BufferState {
215 buffer,
216 last_buffer: buffer.get_untracked(),
217 },
218 text_buf: TextLayout::new(),
219 text_node: None,
220 style: Default::default(),
221 font: FontProps::default(),
222 cursor_x: 0.0,
223 selection: None,
224 glyph_max_size: Size::ZERO,
225 clip_start_x: 0.0,
226 cursor_width: 1.0,
227 width: 0.0,
228 height: 0.0,
229 is_focused: false,
230 last_pointer_down: Point::ZERO,
231 last_cursor_action_on: Instant::now(),
232 on_enter: None,
233 window_origin: None,
234 last_ime_cursor_area: None,
235 }
236 .on_event_stop(EventListener::FocusGained, move |_| {
237 is_focused.set(true);
238 set_ime_allowed(true);
239 })
240 .on_event_stop(EventListener::FocusLost, move |_| {
241 is_focused.set(false);
242 set_ime_allowed(false);
243 })
244 .class(TextInputClass)
245}
246
247pub(crate) enum TextCommand {
248 SelectAll,
249 Copy,
250 Paste,
251 Cut,
252 None,
253}
254use ui_events::keyboard::Code;
255
256impl From<&KeyboardEvent> for TextCommand {
257 fn from(event: &KeyboardEvent) -> Self {
258 #[cfg(target_os = "macos")]
259 match (event.modifiers, event.code) {
260 (Modifiers::META, Code::KeyA) => Self::SelectAll,
261 (Modifiers::META, Code::KeyC) => Self::Copy,
262 (Modifiers::META, Code::KeyX) => Self::Cut,
263 (Modifiers::META, Code::KeyV) => Self::Paste,
264 _ => Self::None,
265 }
266 #[cfg(not(target_os = "macos"))]
267 match (event.modifiers, event.code) {
268 (Modifiers::CONTROL, Code::KeyA) => Self::SelectAll,
269 (Modifiers::CONTROL, Code::KeyC) => Self::Copy,
270 (Modifiers::CONTROL, Code::KeyX) => Self::Cut,
271 (Modifiers::CONTROL, Code::KeyV) => Self::Paste,
272 _ => Self::None,
273 }
274 }
275}
276
277fn get_word_based_motion(event: &KeyboardEvent) -> Option<Movement> {
279 #[cfg(not(target_os = "macos"))]
280 return event
281 .modifiers
282 .contains(Modifiers::CONTROL)
283 .then_some(Movement::Word);
284
285 #[cfg(target_os = "macos")]
286 return event
287 .modifiers
288 .contains(Modifiers::ALT)
289 .then_some(Movement::Word)
290 .or(event
291 .modifiers
292 .contains(Modifiers::META)
293 .then_some(Movement::Line));
294}
295
296const DEFAULT_FONT_SIZE: f32 = 14.0;
297const CURSOR_BLINK_INTERVAL_MS: u64 = 500;
298
299impl TextInput {
300 pub fn placeholder(mut self, text: impl Into<String>) -> Self {
312 self.placeholder_text = Some(text.into());
313 self
314 }
315
316 pub fn on_enter(mut self, action: impl Fn() + 'static) -> Self {
332 self.on_enter = Some(Box::new(action));
333 self
334 }
335}
336
337impl TextInput {
338 fn move_cursor(&mut self, move_kind: Movement, direction: TextDirection) -> bool {
339 match (move_kind, direction) {
340 (Movement::Glyph, TextDirection::Left) => {
341 let untracked_buffer = self.buffer.get_untracked();
342 let mut grapheme_iter = untracked_buffer[..self.cursor_glyph_idx].graphemes(true);
343 match grapheme_iter.next_back() {
344 None => false,
345 Some(prev_character) => {
346 self.cursor_glyph_idx -= prev_character.len();
347 true
348 }
349 }
350 }
351 (Movement::Glyph, TextDirection::Right) => {
352 let untracked_buffer = self.buffer.get_untracked();
353 let mut grapheme_iter = untracked_buffer[self.cursor_glyph_idx..].graphemes(true);
354 match grapheme_iter.next() {
355 None => false,
356 Some(next_character) => {
357 self.cursor_glyph_idx += next_character.len();
358 true
359 }
360 }
361 }
362 (Movement::Line, TextDirection::Right) => {
363 if self.cursor_glyph_idx < self.buffer.with_untracked(|buff| buff.len()) {
364 self.cursor_glyph_idx = self.buffer.with_untracked(|buff| buff.len());
365 return true;
366 }
367 false
368 }
369 (Movement::Line, TextDirection::Left) => {
370 if self.cursor_glyph_idx > 0 {
371 self.cursor_glyph_idx = 0;
372 return true;
373 }
374 false
375 }
376 (Movement::Word, TextDirection::Right) => self.buffer.with_untracked(|buff| {
377 for (idx, word) in buff.unicode_word_indices() {
378 let word_end_idx = idx + word.len();
379 if word_end_idx > self.cursor_glyph_idx {
380 self.cursor_glyph_idx = word_end_idx;
381 return true;
382 }
383 }
384 false
385 }),
386 (Movement::Word, TextDirection::Left) if self.cursor_glyph_idx > 0 => {
387 self.buffer.with_untracked(|buff| {
388 let mut prev_word_idx = 0;
389 for (idx, _) in buff.unicode_word_indices() {
390 if idx < self.cursor_glyph_idx {
391 prev_word_idx = idx;
392 } else {
393 break;
394 }
395 }
396 self.cursor_glyph_idx = prev_word_idx;
397 true
398 })
399 }
400 (_movement, _dir) => false,
401 }
402 }
403
404 fn cursor_visual_idx(&self) -> usize {
405 let Some(preedit) = &self.preedit else {
406 return self.cursor_glyph_idx;
407 };
408
409 let Some(cursor) = preedit.cursor else {
410 return self.cursor_glyph_idx + preedit.text.len();
411 };
412
413 self.cursor_glyph_idx + cursor.0
414 }
415
416 fn calculate_clip_offset(&mut self, node_layout: &Layout) {
417 let node_width = node_layout.size.width as f64;
418 let cursor_glyph_pos = self.text_buf.hit_position(self.cursor_visual_idx());
419 let cursor_x = cursor_glyph_pos.point.x;
420
421 let mut clip_start_x = self.clip_start_x;
422
423 if cursor_x < clip_start_x {
424 clip_start_x = cursor_x;
425 } else if cursor_x > clip_start_x + node_width {
426 clip_start_x = cursor_x - node_width;
427 }
428
429 self.cursor_x = cursor_x;
430 self.clip_start_x = clip_start_x;
431
432 self.update_text_layout();
433 }
434
435 fn get_cursor_rect(&self, node_layout: &Layout) -> Rect {
436 let node_location = node_layout.location;
437
438 let text_height = self.height as f64;
439
440 let cursor_start = Point::new(
441 self.cursor_x - self.clip_start_x + node_location.x as f64,
442 node_location.y as f64,
443 );
444
445 Rect::from_points(
446 cursor_start,
447 Point::new(
448 cursor_start.x + self.cursor_width,
449 node_location.y as f64 + text_height,
450 ),
451 )
452 }
453
454 fn scroll(&mut self, offset: f64) {
455 self.clip_start_x += offset;
456 self.clip_start_x = self
457 .clip_start_x
458 .min(self.text_buf.size().width - self.width as f64)
459 .max(0.0);
460 }
461
462 fn handle_double_click(&mut self, pos_x: f64) {
463 let clicked_glyph_idx = self.get_box_position(pos_x);
464
465 self.buffer.with_untracked(|buff| {
466 let selection = get_dbl_click_selection(clicked_glyph_idx, buff);
467 if selection.start != selection.end {
468 self.cursor_glyph_idx = selection.end;
469 self.selection = Some(selection);
470 }
471 })
472 }
473
474 fn handle_triple_click(&mut self) {
475 self.select_all();
476 }
477
478 fn get_box_position(&self, pos_x: f64) -> usize {
479 let layout = self.id.get_layout().unwrap_or_default();
480 let view_state = self.id.state();
481 let view_state = view_state.borrow();
482 let style = view_state.combined_style.builtin();
483
484 let padding_left = match style.padding_left() {
485 PxPct::Px(padding) => padding as f32,
486 PxPct::Pct(pct) => pct as f32 * layout.size.width,
487 };
488 self.text_buf
489 .hit_point(Point::new(
490 pos_x + self.clip_start_x - padding_left as f64,
491 0.0,
492 ))
493 .index
494 }
495
496 fn get_selection_rect(&self, node_layout: &Layout, left_padding: f64) -> Rect {
497 let selection = if let Some(curr_selection) = &self.selection {
498 curr_selection.clone()
499 } else if let Some(cursor) = self.preedit.as_ref().and_then(|p| p.cursor) {
500 self.cursor_glyph_idx + cursor.0..self.cursor_glyph_idx + cursor.1
501 } else {
502 return Rect::ZERO;
503 };
504
505 let text_height = self.height;
506
507 let selection_start_x =
508 self.text_buf.hit_position(selection.start).point.x - self.clip_start_x;
509 let selection_start_x = selection_start_x.max(node_layout.location.x as f64 - left_padding);
510
511 let selection_end_x =
512 self.text_buf.hit_position(selection.end).point.x + left_padding - self.clip_start_x;
513 let selection_end_x =
514 selection_end_x.min(selection_start_x + self.width as f64 + left_padding);
515
516 let node_location = node_layout.location;
517
518 let selection_start = Point::new(
519 selection_start_x + node_location.x as f64,
520 node_location.y as f64,
521 );
522
523 Rect::from_points(
524 selection_start,
525 Point::new(selection_end_x, selection_start.y + text_height as f64),
526 )
527 }
528
529 fn get_font_glyph_max_size(&self) -> Size {
531 let mut tmp = TextLayout::new();
532 let attrs_list = self.get_text_attrs();
533 let align = self.style.text_align();
534 tmp.set_text("W", attrs_list, align);
535 tmp.size() + Size::new(0., tmp.hit_position(0).glyph_descent)
536 }
537
538 fn update_selection(&mut self, selection_start: usize, selection_end: usize) {
539 self.selection = match selection_start.cmp(&selection_end) {
540 std::cmp::Ordering::Less => Some(Range {
541 start: selection_start,
542 end: selection_end,
543 }),
544 std::cmp::Ordering::Greater => Some(Range {
545 start: selection_end,
546 end: selection_start,
547 }),
548 std::cmp::Ordering::Equal => None,
549 };
550 }
551
552 fn update_ime_cursor_area(&mut self) {
553 if !self.is_focused {
554 return;
555 }
556
557 let (Some(layout), Some(origin)) = (self.id.get_layout(), self.window_origin) else {
558 return;
559 };
560
561 let left_padding = layout.border.left + layout.padding.left;
562 let top_padding = layout.border.top + layout.padding.top;
563
564 let pos = Point::new(
565 origin.x + self.cursor_x - self.clip_start_x + left_padding as f64,
566 origin.y + top_padding as f64,
567 );
568
569 let width = self
570 .preedit
571 .as_ref()
572 .map(|preedit| {
573 let start_idx = preedit.offset;
574 let end_idx = start_idx + preedit.text.len();
575
576 let start_x = self.text_buf.hit_position(start_idx).point.x;
577 let end_x = self.text_buf.hit_position(end_idx).point.x;
578
579 (end_x - start_x).abs()
580 })
581 .unwrap_or_default();
582
583 let size = Size::new(width, layout.content_box_height() as f64);
584
585 if self.last_ime_cursor_area != Some((pos, size)) {
586 set_ime_cursor_area(pos, size);
587 self.last_ime_cursor_area = Some((pos, size));
588 }
589 }
590
591 fn commit_preedit(&mut self) -> bool {
592 if let Some(preedit) = self.preedit.take() {
593 self.buffer
594 .update(|buf| buf.insert_str(self.cursor_glyph_idx, &preedit.text));
595
596 if self.is_focused {
597 set_ime_allowed(false);
599 set_ime_allowed(true);
600 }
602
603 self.update_text_layout();
604 true
605 } else {
606 false
607 }
608 }
609
610 fn update_text_layout(&mut self) {
611 let glyph_max_size = self.get_font_glyph_max_size();
612 self.height = glyph_max_size.height as f32;
613 self.glyph_max_size = glyph_max_size;
614
615 let buffer_is_empty = self.buffer.with_untracked(|buff| {
616 buff.is_empty() && self.preedit.as_ref().is_none_or(|p| p.text.is_empty())
617 });
618
619 if let (Some(placeholder_text), true) = (&self.placeholder_text, buffer_is_empty) {
620 let attrs_list = self.get_placeholder_text_attrs();
621 self.text_buf.set_text(
622 placeholder_text,
623 attrs_list,
624 self.placeholder_style.text_align(),
625 );
626 } else {
627 let attrs_list = self.get_text_attrs();
628 let align = self.style.text_align();
629 self.buffer.with_untracked(|buff| {
630 let preedited;
631 let display_text = if let Some(preedit) = &self.preedit {
632 let preedit_offset = self.cursor_glyph_idx.min(buff.len());
633
634 preedited = [
635 &buff[..preedit_offset],
636 &preedit.text,
637 &buff[preedit_offset..],
638 ]
639 .concat();
640
641 &preedited
642 } else {
643 buff
644 };
645
646 self.text_buf
647 .set_text(display_text, attrs_list.clone(), align);
648 });
649 }
650 }
651
652 fn font_size(&self) -> f32 {
653 self.font.size().unwrap_or(DEFAULT_FONT_SIZE)
654 }
655
656 pub fn get_placeholder_text_attrs(&self) -> AttrsList {
658 let mut attrs = Attrs::new().color(
659 self.placeholder_style
660 .color()
661 .unwrap_or(palette::css::BLACK),
662 );
663
664 attrs = attrs.font_size(self.font_size());
669
670 if let Some(font_style) = self.placeholder_style.font_style() {
671 attrs = attrs.style(font_style);
672 } else if let Some(font_style) = self.font.style() {
673 attrs = attrs.style(font_style);
674 }
675
676 if let Some(font_weight) = self.placeholder_style.font_weight() {
677 attrs = attrs.weight(font_weight);
678 } else if let Some(font_weight) = self.font.weight() {
679 attrs = attrs.weight(font_weight);
680 }
681
682 let input_font_family = self.font.family().as_ref().map(|font_family| {
683 let family: Vec<FamilyOwned> = FamilyOwned::parse_list(font_family).collect();
684 family
685 });
686
687 let placeholder_font_family =
688 self.placeholder_style
689 .font_family()
690 .as_ref()
691 .map(|font_family| {
692 let family: Vec<FamilyOwned> = FamilyOwned::parse_list(font_family).collect();
693 family
694 });
695
696 if let Some(font_family) = placeholder_font_family.as_ref() {
698 attrs = attrs.family(font_family);
699 } else if let Some(font_family) = input_font_family.as_ref() {
700 attrs = attrs.family(font_family);
701 }
702
703 AttrsList::new(attrs)
704 }
705
706 pub fn get_text_attrs(&self) -> AttrsList {
708 let mut attrs = Attrs::new().color(self.style.color().unwrap_or(palette::css::BLACK));
709
710 attrs = attrs.font_size(self.font_size());
711
712 if let Some(font_style) = self.font.style() {
713 attrs = attrs.style(font_style);
714 }
715 let font_family = self.font.family().as_ref().map(|font_family| {
716 let family: Vec<FamilyOwned> = FamilyOwned::parse_list(font_family).collect();
717 family
718 });
719 if let Some(font_family) = font_family.as_ref() {
720 attrs = attrs.family(font_family);
721 }
722 if let Some(font_weight) = self.font.weight() {
723 attrs = attrs.weight(font_weight);
724 }
725 AttrsList::new(attrs)
726 }
727
728 fn select_all(&mut self) {
730 let len = self.buffer.with_untracked(|val| val.len());
731
732 if len == 0 {
733 return;
734 }
735
736 let text_node = self.text_node.unwrap();
737 let node_layout = self
738 .id
739 .taffy()
740 .borrow()
741 .layout(text_node)
742 .cloned()
743 .unwrap_or_default();
744 self.cursor_glyph_idx = len;
745
746 let buf_width = self.text_buf.size().width;
747 let node_width = node_layout.size.width as f64;
748
749 if buf_width > node_width {
750 self.calculate_clip_offset(&node_layout);
751 }
752
753 self.selection = Some(0..len);
754 }
755
756 fn handle_modifier_cmd(&mut self, event: &KeyboardEvent) -> bool {
757 if event.modifiers.is_empty() || event.modifiers == Modifiers::SHIFT {
758 return false;
759 }
760
761 let command = event.into();
762
763 match command {
764 TextCommand::SelectAll => {
765 self.select_all();
766 true
767 }
768 TextCommand::Copy => {
769 if let Some(selection) = &self.selection {
770 let selection_txt = self
771 .buffer
772 .get_untracked()
773 .chars()
774 .skip(selection.start)
775 .take(selection.end - selection.start)
776 .collect();
777 let _ = Clipboard::set_contents(selection_txt);
778 }
779 true
780 }
781 TextCommand::Cut => {
782 if let Some(selection) = &self.selection {
783 let selection_txt = self
784 .buffer
785 .get_untracked()
786 .chars()
787 .skip(selection.start)
788 .take(selection.end - selection.start)
789 .collect();
790 let _ = Clipboard::set_contents(selection_txt);
791
792 self.buffer
793 .update(|buf| replace_range(buf, selection.clone(), None));
794
795 self.cursor_glyph_idx = selection.start;
796 self.selection = None;
797 }
798
799 true
800 }
801 TextCommand::Paste => {
802 let mut clipboard_content = match Clipboard::get_contents() {
803 Ok(content) => content,
804 Err(_) => return false,
805 };
806
807 clipboard_content.retain(|c| c != '\r' && c != '\n');
808
809 if clipboard_content.is_empty() {
810 return false;
811 }
812
813 if let Some(selection) = self.selection.take() {
814 self.buffer.update(|buf| {
815 replace_range(buf, selection.clone(), Some(&clipboard_content))
816 });
817
818 self.cursor_glyph_idx = selection.start + clipboard_content.len();
819 } else {
820 self.buffer
821 .update(|buf| buf.insert_str(self.cursor_glyph_idx, &clipboard_content));
822 self.cursor_glyph_idx += clipboard_content.len();
823 }
824
825 true
826 }
827 TextCommand::None => {
828 self.selection = None;
829 false
830 }
831 }
832 }
833
834 fn handle_key_down(&mut self, cx: &mut EventCx, event: &KeyboardEvent) -> bool {
835 let handled = match event.key {
836 Key::Character(ref c) if c == " " => {
837 if let Some(selection) = &self.selection {
838 self.buffer
839 .update(|buf| replace_range(buf, selection.clone(), None));
840 self.cursor_glyph_idx = selection.start;
841 self.selection = None;
842 } else {
843 self.buffer
844 .update(|buf| buf.insert(self.cursor_glyph_idx, ' '));
845 }
846 self.move_cursor(Movement::Glyph, TextDirection::Right)
847 }
848 Key::Named(NamedKey::Backspace) => {
849 let selection = self.selection.clone();
850 if let Some(selection) = selection {
851 self.cursor_glyph_idx = selection.start;
852 self.buffer
853 .update(|buf| replace_range(buf, selection, None));
854 self.selection = None;
855 true
856 } else {
857 let prev_cursor_idx = self.cursor_glyph_idx;
858
859 self.move_cursor(
860 get_word_based_motion(event).unwrap_or(Movement::Glyph),
861 TextDirection::Left,
862 );
863
864 if self.cursor_glyph_idx == prev_cursor_idx {
865 return false;
866 }
867
868 self.buffer.update(|buf| {
869 replace_range(buf, self.cursor_glyph_idx..prev_cursor_idx, None);
870 });
871 true
872 }
873 }
874 Key::Named(NamedKey::Delete) => {
875 let selection = self.selection.clone();
876 if let Some(selection) = selection {
877 self.cursor_glyph_idx = selection.start;
878 self.buffer
879 .update(|buf| replace_range(buf, selection, None));
880 self.selection = None;
881 return true;
882 }
883
884 let prev_cursor_idx = self.cursor_glyph_idx;
885
886 self.move_cursor(
887 get_word_based_motion(event).unwrap_or(Movement::Glyph),
888 TextDirection::Right,
889 );
890
891 if self.cursor_glyph_idx == prev_cursor_idx {
892 return false;
893 }
894
895 self.buffer.update(|buf| {
896 replace_range(buf, prev_cursor_idx..self.cursor_glyph_idx, None);
897 });
898
899 self.cursor_glyph_idx = prev_cursor_idx;
900 true
901 }
902 Key::Named(NamedKey::Escape) => {
903 cx.window_state.clear_focus();
904 true
905 }
906 Key::Named(NamedKey::Enter) => {
907 if let Some(action) = &self.on_enter {
908 action();
909 }
910 true
911 }
912 Key::Named(NamedKey::End) => {
913 if event.modifiers.contains(Modifiers::SHIFT) {
914 match &self.selection {
915 Some(selection_value) => self.update_selection(
916 selection_value.start,
917 self.buffer.get_untracked().len(),
918 ),
919 None => self.update_selection(
920 self.cursor_glyph_idx,
921 self.buffer.get_untracked().len(),
922 ),
923 }
924 } else {
925 self.selection = None;
926 }
927 self.move_cursor(Movement::Line, TextDirection::Right)
928 }
929 Key::Named(NamedKey::Home) => {
930 if event.modifiers.contains(Modifiers::SHIFT) {
931 match &self.selection {
932 Some(selection_value) => self.update_selection(0, selection_value.end),
933 None => self.update_selection(0, self.cursor_glyph_idx),
934 }
935 } else {
936 self.selection = None;
937 }
938 self.move_cursor(Movement::Line, TextDirection::Left)
939 }
940 Key::Named(NamedKey::ArrowLeft) => {
941 let old_glyph_idx = self.cursor_glyph_idx;
942
943 let move_kind = get_word_based_motion(event).unwrap_or(Movement::Glyph);
944 let cursor_moved = self.move_cursor(move_kind, TextDirection::Left);
945
946 if event.modifiers.contains(Modifiers::SHIFT) {
947 self.move_selection(old_glyph_idx, self.cursor_glyph_idx);
948 } else if let Some(selection) = self.selection.take() {
949 if matches!(move_kind, Movement::Glyph) {
951 self.cursor_glyph_idx = selection.start;
952 }
953 }
954
955 cursor_moved
956 }
957 Key::Named(NamedKey::ArrowRight) => {
958 let old_glyph_idx = self.cursor_glyph_idx;
959
960 let move_kind = get_word_based_motion(event).unwrap_or(Movement::Glyph);
961 let cursor_moved = self.move_cursor(move_kind, TextDirection::Right);
962
963 if event.modifiers.contains(Modifiers::SHIFT) {
964 self.move_selection(old_glyph_idx, self.cursor_glyph_idx);
965 } else if let Some(selection) = self.selection.take() {
966 if matches!(move_kind, Movement::Glyph) {
968 self.cursor_glyph_idx = selection.end;
969 }
970 }
971
972 cursor_moved
973 }
974 _ => false,
975 };
976 if handled {
977 return true;
978 }
979
980 match event.key {
981 Key::Character(ref ch) => {
982 let handled_modifier_cmd = self.handle_modifier_cmd(event);
983 if handled_modifier_cmd {
984 return true;
985 }
986 let non_shift_mask = Modifiers::all().difference(Modifiers::SHIFT);
987 if event.modifiers.intersects(non_shift_mask) {
988 return false;
989 }
990 self.insert_text(ch)
991 }
992 _ => false,
993 }
994 }
995
996 fn insert_text(&mut self, ch: &str) -> bool {
997 let selection = self.selection.clone();
998 if let Some(selection) = selection {
999 self.buffer
1000 .update(|buf| replace_range(buf, selection.clone(), None));
1001 self.cursor_glyph_idx = selection.start;
1002 self.selection = None;
1003 }
1004
1005 self.buffer
1006 .update(|buf| buf.insert_str(self.cursor_glyph_idx, ch));
1007 self.move_cursor(Movement::Glyph, TextDirection::Right)
1008 }
1009
1010 fn move_selection(&mut self, old_glyph_idx: usize, curr_glyph_idx: usize) {
1011 let new_selection = if let Some(selection) = &self.selection {
1012 let anchor = if selection.start == old_glyph_idx {
1015 selection.end
1016 } else {
1017 selection.start
1018 };
1019
1020 if anchor < curr_glyph_idx {
1021 anchor..curr_glyph_idx
1022 } else {
1023 curr_glyph_idx..anchor
1024 }
1025 } else if old_glyph_idx < curr_glyph_idx {
1026 old_glyph_idx..curr_glyph_idx
1027 } else {
1028 curr_glyph_idx..old_glyph_idx
1029 };
1030
1031 self.selection = if new_selection.is_empty() {
1033 None
1034 } else {
1035 Some(new_selection)
1036 };
1037 }
1038
1039 fn paint_selection_rect(&self, &node_layout: &Layout, cx: &mut crate::context::PaintCx<'_>) {
1040 let view_state = self.id.state();
1041 let view_state = view_state.borrow();
1042 let style = &view_state.combined_style;
1043
1044 let cursor_color = self.selection_style.selection_color();
1045
1046 let padding_left = match style.get(PaddingProp).left.unwrap_or(PxPct::Px(0.)) {
1047 PxPct::Px(padding) => padding,
1048 PxPct::Pct(pct) => pct / 100.0 * node_layout.size.width as f64,
1049 };
1050
1051 let border_radius = self.selection_style.corner_radius();
1052 let selection_rect = self
1053 .get_selection_rect(&node_layout, padding_left)
1054 .to_rounded_rect(border_radius);
1055 cx.save();
1056 cx.clip(&self.id.get_content_rect());
1057 cx.fill(&selection_rect, &cursor_color, 0.0);
1058 cx.restore();
1059 }
1060}
1061
1062fn replace_range(buff: &mut String, del_range: Range<usize>, replacement: Option<&str>) {
1063 assert!(del_range.start <= del_range.end);
1064 if !buff.is_char_boundary(del_range.end) {
1065 eprintln!("[Floem] Tried to delete range with invalid end: {del_range:?}");
1066 return;
1067 }
1068
1069 if !buff.is_char_boundary(del_range.start) {
1070 eprintln!("[Floem] Tried to delete range with invalid start: {del_range:?}");
1071 return;
1072 }
1073
1074 let after_del_range = buff.split_off(del_range.end);
1076
1077 buff.truncate(del_range.start);
1079
1080 if let Some(repl) = replacement {
1081 buff.push_str(repl);
1082 }
1083
1084 buff.push_str(&after_del_range);
1085}
1086
1087fn get_dbl_click_selection(glyph_idx: usize, buffer: &str) -> Range<usize> {
1088 let mut selectable_ranges: Vec<Range<usize>> = Vec::new();
1089 let glyph_idx = usize::min(glyph_idx, buffer.len().saturating_sub(1));
1090
1091 for (idx, word) in buffer.unicode_word_indices() {
1092 let word_range = idx..idx + word.len();
1093
1094 if let Some(prev) = selectable_ranges.last() {
1095 if prev.end != idx {
1096 selectable_ranges.push(prev.end..idx);
1098 }
1099 } else if idx > 0 {
1100 selectable_ranges.push(0..idx);
1102 }
1103
1104 selectable_ranges.push(word_range);
1105 }
1106
1107 if let Some(last) = selectable_ranges.last() {
1109 if last.end != buffer.len() {
1110 selectable_ranges.push(last.end..buffer.len());
1111 }
1112 }
1113
1114 for range in selectable_ranges {
1115 if range.contains(&glyph_idx) {
1116 return range;
1117 }
1118 }
1119
1120 0..buffer.len()
1122}
1123
1124impl View for TextInput {
1125 fn id(&self) -> ViewId {
1126 self.id
1127 }
1128
1129 fn debug_name(&self) -> std::borrow::Cow<'static, str> {
1130 format!("TextInput: {:?}", self.buffer.get_untracked()).into()
1131 }
1132
1133 fn update(&mut self, cx: &mut UpdateCx, state: Box<dyn Any>) {
1134 if let Ok(state) = state.downcast::<bool>() {
1135 let is_focused = *state;
1136
1137 if self.is_focused != is_focused {
1138 self.is_focused = is_focused;
1139 self.last_ime_cursor_area = None;
1140
1141 self.commit_preedit();
1142 self.update_ime_cursor_area();
1143
1144 if is_focused && !cx.window_state.is_active(&self.id) {
1145 self.selection = None;
1146 self.cursor_glyph_idx = self.buffer.with_untracked(|buf| buf.len());
1147 }
1148 }
1149
1150 let text_updated = self.buffer.buffer.with_untracked(|buf| {
1152 let updated = *buf != self.buffer.last_buffer;
1153
1154 if updated {
1155 self.buffer.last_buffer.clone_from(buf);
1156 }
1157
1158 updated
1159 });
1160
1161 if text_updated {
1162 self.update_text_layout();
1163 self.id.request_layout();
1164 }
1165 } else {
1166 eprintln!("downcast failed");
1167 }
1168 }
1169
1170 fn event_before_children(&mut self, cx: &mut EventCx, event: &Event) -> EventPropagation {
1171 let buff_len = self.buffer.with_untracked(|buff| buff.len());
1172 if self.cursor_glyph_idx > buff_len {
1175 self.cursor_glyph_idx = buff_len;
1176 }
1177
1178 let is_handled = match &event {
1179 Event::Pointer(PointerEvent::Down(PointerButtonEvent {
1181 button: Some(PointerButton::Primary),
1182 state,
1183 ..
1184 })) => {
1185 cx.update_active(self.id);
1186 let point = state.logical_point();
1187 self.last_pointer_down = point;
1188
1189 self.commit_preedit();
1190
1191 if self.buffer.with_untracked(|buff| !buff.is_empty()) {
1192 if state.count == 2 {
1193 self.handle_double_click(point.x);
1194 } else if state.count == 3 {
1195 self.handle_triple_click();
1196 } else {
1197 self.cursor_glyph_idx = self.get_box_position(point.x);
1198 self.selection = None;
1199 }
1200 }
1201 true
1202 }
1203 Event::Pointer(PointerEvent::Move(pu)) => {
1204 if cx.is_active(self.id) && self.buffer.with_untracked(|buff| !buff.is_empty()) {
1205 if self.commit_preedit() {
1206 self.id.request_layout();
1207 }
1208 let pos = pu.current.logical_point();
1209
1210 if pos.x < 0. && pos.x < self.last_pointer_down.x {
1211 self.scroll(pos.x);
1212 } else if pos.x > self.width as f64 && pos.x > self.last_pointer_down.x {
1213 self.scroll(pos.x - self.width as f64);
1214 }
1215
1216 let selection_stop = self.get_box_position(pos.x);
1217 self.update_selection(self.cursor_glyph_idx, selection_stop);
1218
1219 self.id.request_paint();
1220 }
1221 false
1222 }
1223 Event::Key(
1224 ke @ KeyboardEvent {
1225 state: KeyState::Down,
1226 ..
1227 },
1228 ) => self.handle_key_down(cx, ke),
1229 Event::ImePreedit { text, cursor } => {
1230 if self.is_focused && !text.is_empty() {
1231 if let Some(selection) = self.selection.take() {
1232 self.cursor_glyph_idx = selection.start;
1233 self.buffer
1234 .update(|buf| replace_range(buf, selection.clone(), None));
1235 }
1236
1237 let mut preedit = self.preedit.take().unwrap_or_else(|| Preedit {
1238 text: Default::default(),
1239 cursor: None,
1240 offset: 0,
1241 });
1242 preedit.text.clone_from(text);
1243 preedit.cursor = *cursor;
1244 self.preedit = Some(preedit);
1245
1246 true
1247 } else {
1248 self.preedit.take().is_some()
1250 }
1251 }
1252 Event::ImeDeleteSurrounding {
1253 before_bytes,
1254 after_bytes,
1255 } => {
1256 if self.is_focused {
1257 self.buffer.update(|buf| {
1258 if let Some(selection) = self.selection.take() {
1259 self.cursor_glyph_idx = selection.start;
1260 buf.replace_range(selection, "");
1261 }
1262 let before_start = buf[..self.cursor_glyph_idx]
1269 .char_indices()
1270 .rev()
1271 .find(|(index, _)| self.cursor_glyph_idx - index >= *before_bytes)
1272 .map(|(i, _)| i)
1273 .unwrap_or(0);
1274 let after_end = buf[self.cursor_glyph_idx..]
1275 .char_indices()
1276 .map(|(index, _)| index)
1277 .find(|index| index >= after_bytes)
1278 .map(|i| i + self.cursor_glyph_idx)
1279 .unwrap_or(buf.len());
1280 buf.replace_range(before_start..after_end, "");
1281 self.cursor_glyph_idx = before_start;
1282 });
1283 true
1284 } else {
1285 false
1286 }
1287 }
1288 Event::ImeCommit(text) => {
1289 if self.is_focused {
1290 self.buffer
1291 .update(|buf| buf.insert_str(self.cursor_glyph_idx, text));
1292 self.cursor_glyph_idx += text.len();
1293 self.preedit = None;
1294
1295 true
1296 } else {
1297 false
1298 }
1299 }
1300 _ => false,
1301 };
1302
1303 if is_handled {
1304 self.update_text_layout();
1305 self.id.request_layout();
1306 self.last_cursor_action_on = Instant::now();
1307 }
1308
1309 if is_handled {
1310 EventPropagation::Stop
1311 } else {
1312 EventPropagation::Continue
1313 }
1314 }
1315
1316 fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
1317 let style = cx.style();
1318
1319 let placeholder_style = cx.resolve_nested_maps(
1320 style.clone(),
1321 &[PlaceholderTextClass::class_ref()],
1322 false,
1323 false,
1324 false,
1325 );
1326 self.placeholder_style.read_style(cx, &placeholder_style);
1327
1328 if self.font.read(cx) {
1329 self.update_text_layout();
1330 self.id.request_layout();
1331 }
1332 if self.style.read(cx) {
1333 cx.window_state.request_paint(self.id);
1334
1335 self.update_text_layout();
1337 }
1338
1339 self.selection_style.read_style(cx, &style);
1340 }
1341
1342 fn layout(&mut self, cx: &mut crate::context::LayoutCx) -> taffy::tree::NodeId {
1343 cx.layout_node(self.id(), true, |cx| {
1344 let was_focused = self.is_focused;
1345 self.is_focused = cx.window_state.is_focused(&self.id);
1346
1347 if was_focused && !self.is_focused {
1348 self.selection = None;
1349 }
1350
1351 if self.text_node.is_none() {
1352 self.text_node = Some(
1353 self.id
1354 .taffy()
1355 .borrow_mut()
1356 .new_leaf(taffy::style::Style::DEFAULT)
1357 .unwrap(),
1358 );
1359 }
1360
1361 let text_node = self.text_node.unwrap();
1362
1363 let style = Style::new()
1364 .width(PxPctAuto::Pct(100.))
1365 .height(self.height)
1366 .to_taffy_style();
1367 let _ = self.id.taffy().borrow_mut().set_style(text_node, style);
1368
1369 vec![text_node]
1370 })
1371 }
1372
1373 fn compute_layout(&mut self, cx: &mut crate::context::ComputeLayoutCx) -> Option<Rect> {
1374 self.width = self.id.get_content_rect().width() as f32;
1375 let buf_width = self.text_buf.size().width;
1376 let text_node = self.text_node.unwrap();
1377 let node_layout = self
1378 .id
1379 .taffy()
1380 .borrow()
1381 .layout(text_node)
1382 .cloned()
1383 .unwrap_or_default();
1384 let node_width = node_layout.size.width as f64;
1385
1386 if buf_width > node_width {
1387 self.calculate_clip_offset(&node_layout);
1388 } else {
1389 self.clip_start_x = 0.0;
1390 let hit_pos = self.text_buf.hit_position(self.cursor_visual_idx());
1391 self.cursor_x = hit_pos.point.x;
1392 }
1393
1394 self.window_origin = Some(cx.window_origin);
1395 self.update_ime_cursor_area();
1396
1397 None
1398 }
1399
1400 fn paint(&mut self, cx: &mut crate::context::PaintCx) {
1401 let text_node = self.text_node.unwrap();
1402 let node_layout = self
1403 .id
1404 .taffy()
1405 .borrow()
1406 .layout(text_node)
1407 .cloned()
1408 .unwrap_or_default();
1409
1410 let location = node_layout.location;
1411 let text_start_point = Point::new(location.x as f64, location.y as f64);
1412
1413 cx.save();
1414 cx.clip(&self.id.get_content_rect());
1415 cx.draw_text(
1416 &self.text_buf,
1417 Point::new(text_start_point.x - self.clip_start_x, text_start_point.y),
1418 );
1419
1420 if let Some(preedit) = &self.preedit {
1422 let start_idx = self.cursor_glyph_idx;
1423 let end_idx = start_idx + preedit.text.len();
1424
1425 let start_hit = self.text_buf.hit_position(start_idx);
1426 let start_x = location.x as f64 + start_hit.point.x - self.clip_start_x;
1427 let end_x =
1428 location.x as f64 + self.text_buf.hit_position(end_idx).point.x - self.clip_start_x;
1429
1430 let color = self.style.color().unwrap_or(palette::css::BLACK);
1431 let y = location.y as f64 + start_hit.glyph_ascent;
1432
1433 cx.fill(
1434 &Rect::new(start_x, y, end_x, y + 1.0),
1435 &Brush::Solid(color),
1436 0.0,
1437 );
1438 }
1439
1440 cx.restore();
1441
1442 if !cx.window_state.is_focused(&self.id()) {
1444 return;
1445 }
1446
1447 let has_selection = self.selection.is_some()
1449 || self
1450 .preedit
1451 .as_ref()
1452 .is_some_and(|p| p.cursor.is_some_and(|c| c.0 != c.1));
1453
1454 if has_selection {
1455 self.paint_selection_rect(&node_layout, cx);
1456 return;
1458 }
1459
1460 let is_cursor_visible = (self.last_cursor_action_on.elapsed().as_millis()
1462 / CURSOR_BLINK_INTERVAL_MS as u128)
1463 .is_multiple_of(2);
1464
1465 if is_cursor_visible {
1466 let cursor_color = self
1467 .id
1468 .state()
1469 .borrow()
1470 .combined_style
1471 .builtin()
1472 .cursor_color();
1473 let cursor_rect = self.get_cursor_rect(&node_layout);
1474 cx.fill(&cursor_rect, &cursor_color, 0.0);
1475 }
1476
1477 let id = self.id();
1479 exec_after(Duration::from_millis(CURSOR_BLINK_INTERVAL_MS), move |_| {
1480 id.request_paint();
1481 });
1482 }
1483}
1484
1485#[cfg(test)]
1486mod tests {
1487 use crate::views::text_input::get_dbl_click_selection;
1488
1489 use super::replace_range;
1490
1491 #[test]
1492 fn replace_range_start() {
1493 let mut s = "Sample text".to_owned();
1494 replace_range(&mut s, 0..7, Some("Replaced___"));
1495 assert_eq!("Replaced___text", s);
1496 }
1497
1498 #[test]
1499 fn delete_range_start() {
1500 let mut s = "Sample text".to_owned();
1501 replace_range(&mut s, 0..7, None);
1502 assert_eq!("text", s);
1503 }
1504
1505 #[test]
1506 fn replace_range_end() {
1507 let mut s = "Sample text".to_owned();
1508 let len = s.len();
1509 replace_range(&mut s, 6..len, Some("++Replaced"));
1510 assert_eq!("Sample++Replaced", s);
1511 }
1512
1513 #[test]
1514 fn delete_range_full() {
1515 let mut s = "Sample text".to_owned();
1516 let len = s.len();
1517 replace_range(&mut s, 0..len, None);
1518 assert_eq!("", s);
1519 }
1520
1521 #[test]
1522 fn replace_range_full() {
1523 let mut s = "Sample text".to_owned();
1524 let len = s.len();
1525 replace_range(&mut s, 0..len, Some("Hello world"));
1526 assert_eq!("Hello world", s);
1527 }
1528
1529 #[test]
1530 fn delete_range_end() {
1531 let mut s = "Sample text".to_owned();
1532 let len = s.len();
1533 replace_range(&mut s, 6..len, None);
1534 assert_eq!("Sample", s);
1535 }
1536
1537 #[test]
1538 fn dbl_click_whitespace_before_word() {
1539 let s = " select ".to_owned();
1540
1541 let range = get_dbl_click_selection(0, &s);
1542 assert_eq!(range, 0..2);
1543
1544 let range = get_dbl_click_selection(1, &s);
1545 assert_eq!(range, 0..2);
1546 }
1547
1548 #[test]
1549 fn dbl_click_word_surrounded_by_whitespace() {
1550 let s = " select ".to_owned();
1551
1552 let range = get_dbl_click_selection(2, &s);
1553 assert_eq!(range, 2..8);
1554
1555 let range = get_dbl_click_selection(6, &s);
1556 assert_eq!(range, 2..8);
1557 }
1558
1559 #[test]
1560 fn dbl_click_whitespace_bween_words() {
1561 let s = "select select".to_owned();
1562
1563 let range = get_dbl_click_selection(6, &s);
1564 assert_eq!(range, 6..9);
1565
1566 let range = get_dbl_click_selection(7, &s);
1567 assert_eq!(range, 6..9);
1568
1569 let range = get_dbl_click_selection(8, &s);
1570 assert_eq!(range, 6..9);
1571 }
1572
1573 #[test]
1574 fn dbl_click_whitespace_after_word() {
1575 let s = " select ".to_owned();
1576
1577 let range = get_dbl_click_selection(8, &s);
1578 assert_eq!(range, 8..10);
1579
1580 let range = get_dbl_click_selection(9, &s);
1581 assert_eq!(range, 8..10);
1582 }
1583
1584 #[test]
1585 fn dbl_click_letter_after_whitespace() {
1586 let s = " s".to_owned();
1587 let range = get_dbl_click_selection(5, &s);
1588
1589 assert_eq!(range, 5..6);
1590 }
1591
1592 #[test]
1593 fn dbl_click_single_letter() {
1594 let s = "s".to_owned();
1595 let range = get_dbl_click_selection(0, &s);
1596
1597 assert_eq!(range, 0..1);
1598 }
1599
1600 #[test]
1601 fn dbl_click_outside_boundaries_selects_all() {
1602 let s = " ".to_owned();
1603 let range = get_dbl_click_selection(100, &s);
1604
1605 assert_eq!(range, 0..5);
1606 }
1607
1608 #[test]
1609 fn dbl_click_letters_with_whitespace() {
1610 let s = " s s ".to_owned();
1611 let range = get_dbl_click_selection(1, &s);
1612 assert_eq!(range, 1..2);
1613
1614 let range = get_dbl_click_selection(4, &s);
1615 assert_eq!(range, 4..5);
1616 }
1617
1618 #[test]
1619 fn dbl_click_single_word() {
1620 let s = "123testttttttttttttttttttt123".to_owned();
1621 let range = get_dbl_click_selection(1, &s);
1622 let len = s.len();
1623 assert_eq!(range, 0..len);
1624
1625 let range = get_dbl_click_selection(5, &s);
1626 assert_eq!(range, 0..len);
1627
1628 let range = get_dbl_click_selection(len - 1, &s);
1629 assert_eq!(range, 0..len);
1630 }
1631
1632 #[test]
1633 fn dbl_click_two_words_and_whitespace() {
1634 let s = " word1 word2 ".to_owned();
1635
1636 let range = get_dbl_click_selection(2, &s);
1637 assert_eq!(range, 2..7);
1638
1639 let range = get_dbl_click_selection(6, &s);
1640 assert_eq!(range, 2..7);
1641 }
1642
1643 #[test]
1644 fn dbl_click_empty_string() {
1645 let s = "".to_owned();
1646
1647 let range = get_dbl_click_selection(0, &s);
1648 assert_eq!(range, 0..0);
1649
1650 let range = get_dbl_click_selection(1, &s);
1651 assert_eq!(range, 0..0);
1652 }
1653
1654 #[test]
1655 fn dbl_click_whitespace_only() {
1656 let s = " ".to_owned();
1657 let range = get_dbl_click_selection(2, &s);
1658
1659 assert_eq!(range, 0..s.len());
1660 }
1661}