1mod editor;
34mod value;
35
36pub mod cursor;
37
38pub use cursor::Cursor;
39pub use value::Value;
40
41use editor::Editor;
42
43use crate::core::alignment;
44use crate::core::clipboard::{self, Clipboard};
45use crate::core::input_method;
46use crate::core::keyboard;
47use crate::core::keyboard::key;
48use crate::core::layout;
49use crate::core::mouse::{self, click};
50use crate::core::renderer;
51use crate::core::text::paragraph::{self, Paragraph as _};
52use crate::core::text::{self, Text};
53use crate::core::time::{Duration, Instant};
54use crate::core::touch;
55use crate::core::widget;
56use crate::core::widget::operation::{self, Operation};
57use crate::core::widget::tree::{self, Tree};
58use crate::core::window;
59use crate::core::{
60 Alignment, Background, Border, Color, Element, Event, InputMethod, Layout,
61 Length, Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector,
62 Widget,
63};
64
65pub struct TextInput<
98 'a,
99 Message,
100 Theme = crate::Theme,
101 Renderer = crate::Renderer,
102> where
103 Theme: Catalog,
104 Renderer: text::Renderer,
105{
106 id: Option<widget::Id>,
107 placeholder: String,
108 value: Value,
109 is_secure: bool,
110 font: Option<Renderer::Font>,
111 width: Length,
112 padding: Padding,
113 size: Option<Pixels>,
114 line_height: text::LineHeight,
115 alignment: alignment::Horizontal,
116 on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
117 on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>,
118 on_submit: Option<Message>,
119 icon: Option<Icon<Renderer::Font>>,
120 class: Theme::Class<'a>,
121 last_status: Option<Status>,
122}
123
124pub const DEFAULT_PADDING: Padding = Padding::new(5.0);
126
127impl<'a, Message, Theme, Renderer> TextInput<'a, Message, Theme, Renderer>
128where
129 Message: Clone,
130 Theme: Catalog,
131 Renderer: text::Renderer,
132{
133 pub fn new(placeholder: &str, value: &str) -> Self {
136 TextInput {
137 id: None,
138 placeholder: String::from(placeholder),
139 value: Value::new(value),
140 is_secure: false,
141 font: None,
142 width: Length::Fill,
143 padding: DEFAULT_PADDING,
144 size: None,
145 line_height: text::LineHeight::default(),
146 alignment: alignment::Horizontal::Left,
147 on_input: None,
148 on_paste: None,
149 on_submit: None,
150 icon: None,
151 class: Theme::default(),
152 last_status: None,
153 }
154 }
155
156 pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
158 self.id = Some(id.into());
159 self
160 }
161
162 pub fn secure(mut self, is_secure: bool) -> Self {
164 self.is_secure = is_secure;
165 self
166 }
167
168 pub fn on_input(
173 mut self,
174 on_input: impl Fn(String) -> Message + 'a,
175 ) -> Self {
176 self.on_input = Some(Box::new(on_input));
177 self
178 }
179
180 pub fn on_input_maybe(
185 mut self,
186 on_input: Option<impl Fn(String) -> Message + 'a>,
187 ) -> Self {
188 self.on_input = on_input.map(|f| Box::new(f) as _);
189 self
190 }
191
192 pub fn on_submit(mut self, message: Message) -> Self {
195 self.on_submit = Some(message);
196 self
197 }
198
199 pub fn on_submit_maybe(mut self, on_submit: Option<Message>) -> Self {
202 self.on_submit = on_submit;
203 self
204 }
205
206 pub fn on_paste(
209 mut self,
210 on_paste: impl Fn(String) -> Message + 'a,
211 ) -> Self {
212 self.on_paste = Some(Box::new(on_paste));
213 self
214 }
215
216 pub fn on_paste_maybe(
219 mut self,
220 on_paste: Option<impl Fn(String) -> Message + 'a>,
221 ) -> Self {
222 self.on_paste = on_paste.map(|f| Box::new(f) as _);
223 self
224 }
225
226 pub fn font(mut self, font: Renderer::Font) -> Self {
230 self.font = Some(font);
231 self
232 }
233
234 pub fn icon(mut self, icon: Icon<Renderer::Font>) -> Self {
236 self.icon = Some(icon);
237 self
238 }
239
240 pub fn width(mut self, width: impl Into<Length>) -> Self {
242 self.width = width.into();
243 self
244 }
245
246 pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
248 self.padding = padding.into();
249 self
250 }
251
252 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
254 self.size = Some(size.into());
255 self
256 }
257
258 pub fn line_height(
260 mut self,
261 line_height: impl Into<text::LineHeight>,
262 ) -> Self {
263 self.line_height = line_height.into();
264 self
265 }
266
267 pub fn align_x(
269 mut self,
270 alignment: impl Into<alignment::Horizontal>,
271 ) -> Self {
272 self.alignment = alignment.into();
273 self
274 }
275
276 #[must_use]
278 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
279 where
280 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
281 {
282 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
283 self
284 }
285
286 #[must_use]
288 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
289 self.class = class.into();
290 self
291 }
292
293 pub fn layout(
297 &mut self,
298 tree: &mut Tree,
299 renderer: &Renderer,
300 limits: &layout::Limits,
301 value: Option<&Value>,
302 ) -> layout::Node {
303 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
304 let value = value.unwrap_or(&self.value);
305
306 let font = self.font.unwrap_or_else(|| renderer.default_font());
307 let text_size = self.size.unwrap_or_else(|| renderer.default_size());
308 let padding = self.padding.fit(Size::ZERO, limits.max());
309 let height = self.line_height.to_absolute(text_size);
310
311 let limits = limits.width(self.width).shrink(padding);
312 let text_bounds = limits.resolve(self.width, height, Size::ZERO);
313
314 let placeholder_text = Text {
315 font,
316 line_height: self.line_height,
317 content: self.placeholder.as_str(),
318 bounds: Size::new(f32::INFINITY, text_bounds.height),
319 size: text_size,
320 align_x: text::Alignment::Default,
321 align_y: alignment::Vertical::Center,
322 shaping: text::Shaping::Advanced,
323 wrapping: text::Wrapping::default(),
324 };
325
326 let _ = state.placeholder.update(placeholder_text);
327
328 let secure_value = self.is_secure.then(|| value.secure());
329 let value = secure_value.as_ref().unwrap_or(value);
330
331 let _ = state.value.update(Text {
332 content: &value.to_string(),
333 ..placeholder_text
334 });
335
336 if let Some(icon) = &self.icon {
337 let mut content = [0; 4];
338
339 let icon_text = Text {
340 line_height: self.line_height,
341 content: icon.code_point.encode_utf8(&mut content) as &_,
342 font: icon.font,
343 size: icon.size.unwrap_or_else(|| renderer.default_size()),
344 bounds: Size::new(f32::INFINITY, text_bounds.height),
345 align_x: text::Alignment::Center,
346 align_y: alignment::Vertical::Center,
347 shaping: text::Shaping::Advanced,
348 wrapping: text::Wrapping::default(),
349 };
350
351 let _ = state.icon.update(icon_text);
352
353 let icon_width = state.icon.min_width();
354
355 let (text_position, icon_position) = match icon.side {
356 Side::Left => (
357 Point::new(
358 padding.left + icon_width + icon.spacing,
359 padding.top,
360 ),
361 Point::new(padding.left, padding.top),
362 ),
363 Side::Right => (
364 Point::new(padding.left, padding.top),
365 Point::new(
366 padding.left + text_bounds.width - icon_width,
367 padding.top,
368 ),
369 ),
370 };
371
372 let text_node = layout::Node::new(
373 text_bounds - Size::new(icon_width + icon.spacing, 0.0),
374 )
375 .move_to(text_position);
376
377 let icon_node =
378 layout::Node::new(Size::new(icon_width, text_bounds.height))
379 .move_to(icon_position);
380
381 layout::Node::with_children(
382 text_bounds.expand(padding),
383 vec![text_node, icon_node],
384 )
385 } else {
386 let text = layout::Node::new(text_bounds)
387 .move_to(Point::new(padding.left, padding.top));
388
389 layout::Node::with_children(text_bounds.expand(padding), vec![text])
390 }
391 }
392
393 fn input_method<'b>(
394 &self,
395 state: &'b State<Renderer::Paragraph>,
396 layout: Layout<'_>,
397 value: &Value,
398 ) -> InputMethod<&'b str> {
399 let Some(Focus {
400 is_window_focused: true,
401 ..
402 }) = &state.is_focused
403 else {
404 return InputMethod::Disabled;
405 };
406
407 let secure_value = self.is_secure.then(|| value.secure());
408 let value = secure_value.as_ref().unwrap_or(value);
409
410 let text_bounds = layout.children().next().unwrap().bounds();
411
412 let caret_index = match state.cursor.state(value) {
413 cursor::State::Index(position) => position,
414 cursor::State::Selection { start, end } => start.min(end),
415 };
416
417 let text = state.value.raw();
418 let (cursor_x, scroll_offset) =
419 measure_cursor_and_scroll_offset(text, text_bounds, caret_index);
420
421 let alignment_offset = alignment_offset(
422 text_bounds.width,
423 text.min_width(),
424 self.alignment,
425 );
426
427 let x = (text_bounds.x + cursor_x).floor() - scroll_offset
428 + alignment_offset;
429
430 InputMethod::Enabled {
431 cursor: Rectangle::new(
432 Point::new(x, text_bounds.y),
433 Size::new(1.0, text_bounds.height),
434 ),
435 purpose: if self.is_secure {
436 input_method::Purpose::Secure
437 } else {
438 input_method::Purpose::Normal
439 },
440 preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref),
441 }
442 }
443
444 pub fn draw(
449 &self,
450 tree: &Tree,
451 renderer: &mut Renderer,
452 theme: &Theme,
453 layout: Layout<'_>,
454 _cursor: mouse::Cursor,
455 value: Option<&Value>,
456 viewport: &Rectangle,
457 ) {
458 let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
459 let value = value.unwrap_or(&self.value);
460 let is_disabled = self.on_input.is_none();
461
462 let secure_value = self.is_secure.then(|| value.secure());
463 let value = secure_value.as_ref().unwrap_or(value);
464
465 let bounds = layout.bounds();
466
467 let mut children_layout = layout.children();
468 let text_bounds = children_layout.next().unwrap().bounds();
469
470 let style = theme
471 .style(&self.class, self.last_status.unwrap_or(Status::Disabled));
472
473 renderer.fill_quad(
474 renderer::Quad {
475 bounds,
476 border: style.border,
477 ..renderer::Quad::default()
478 },
479 style.background,
480 );
481
482 if self.icon.is_some() {
483 let icon_layout = children_layout.next().unwrap();
484
485 let icon = state.icon.raw();
486
487 renderer.fill_paragraph(
488 icon,
489 icon_layout.bounds().anchor(
490 icon.min_bounds(),
491 Alignment::Center,
492 Alignment::Center,
493 ),
494 style.icon,
495 *viewport,
496 );
497 }
498
499 let text = value.to_string();
500
501 let (cursor, offset, is_selecting) = if let Some(focus) = state
502 .is_focused
503 .as_ref()
504 .filter(|focus| focus.is_window_focused)
505 {
506 match state.cursor.state(value) {
507 cursor::State::Index(position) => {
508 let (text_value_width, offset) =
509 measure_cursor_and_scroll_offset(
510 state.value.raw(),
511 text_bounds,
512 position,
513 );
514
515 let is_cursor_visible = !is_disabled
516 && ((focus.now - focus.updated_at).as_millis()
517 / CURSOR_BLINK_INTERVAL_MILLIS)
518 .is_multiple_of(2);
519
520 let cursor = if is_cursor_visible {
521 Some((
522 renderer::Quad {
523 bounds: Rectangle {
524 x: (text_bounds.x + text_value_width)
525 .floor(),
526 y: text_bounds.y,
527 width: 1.0,
528 height: text_bounds.height,
529 },
530 ..renderer::Quad::default()
531 },
532 style.value,
533 ))
534 } else {
535 None
536 };
537
538 (cursor, offset, false)
539 }
540 cursor::State::Selection { start, end } => {
541 let left = start.min(end);
542 let right = end.max(start);
543
544 let (left_position, left_offset) =
545 measure_cursor_and_scroll_offset(
546 state.value.raw(),
547 text_bounds,
548 left,
549 );
550
551 let (right_position, right_offset) =
552 measure_cursor_and_scroll_offset(
553 state.value.raw(),
554 text_bounds,
555 right,
556 );
557
558 let width = right_position - left_position;
559
560 (
561 Some((
562 renderer::Quad {
563 bounds: Rectangle {
564 x: text_bounds.x + left_position,
565 y: text_bounds.y,
566 width,
567 height: text_bounds.height,
568 },
569 ..renderer::Quad::default()
570 },
571 style.selection,
572 )),
573 if end == right {
574 right_offset
575 } else {
576 left_offset
577 },
578 true,
579 )
580 }
581 }
582 } else {
583 (None, 0.0, false)
584 };
585
586 let draw = |renderer: &mut Renderer, viewport| {
587 let paragraph = if text.is_empty()
588 && state
589 .preedit
590 .as_ref()
591 .map(|preedit| preedit.content.is_empty())
592 .unwrap_or(true)
593 {
594 state.placeholder.raw()
595 } else {
596 state.value.raw()
597 };
598
599 let alignment_offset = alignment_offset(
600 text_bounds.width,
601 paragraph.min_width(),
602 self.alignment,
603 );
604
605 if let Some((cursor, color)) = cursor {
606 renderer.with_translation(
607 Vector::new(alignment_offset - offset, 0.0),
608 |renderer| {
609 renderer.fill_quad(cursor, color);
610 },
611 );
612 } else {
613 renderer.with_translation(Vector::ZERO, |_| {});
614 }
615
616 renderer.fill_paragraph(
617 paragraph,
618 text_bounds.anchor(
619 paragraph.min_bounds(),
620 Alignment::Start,
621 Alignment::Center,
622 ) + Vector::new(alignment_offset - offset, 0.0),
623 if text.is_empty() {
624 style.placeholder
625 } else {
626 style.value
627 },
628 viewport,
629 );
630 };
631
632 if is_selecting {
633 renderer
634 .with_layer(text_bounds, |renderer| draw(renderer, *viewport));
635 } else {
636 draw(renderer, text_bounds);
637 }
638 }
639}
640
641impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
642 for TextInput<'_, Message, Theme, Renderer>
643where
644 Message: Clone,
645 Theme: Catalog,
646 Renderer: text::Renderer,
647{
648 fn tag(&self) -> tree::Tag {
649 tree::Tag::of::<State<Renderer::Paragraph>>()
650 }
651
652 fn state(&self) -> tree::State {
653 tree::State::new(State::<Renderer::Paragraph>::new())
654 }
655
656 fn diff(&self, tree: &mut Tree) {
657 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
658
659 if self.on_input.is_none() {
661 state.is_pasting = None;
662 }
663 }
664
665 fn size(&self) -> Size<Length> {
666 Size {
667 width: self.width,
668 height: Length::Shrink,
669 }
670 }
671
672 fn layout(
673 &mut self,
674 tree: &mut Tree,
675 renderer: &Renderer,
676 limits: &layout::Limits,
677 ) -> layout::Node {
678 self.layout(tree, renderer, limits, None)
679 }
680
681 fn operate(
682 &mut self,
683 tree: &mut Tree,
684 layout: Layout<'_>,
685 _renderer: &Renderer,
686 operation: &mut dyn Operation,
687 ) {
688 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
689
690 operation.text_input(self.id.as_ref(), layout.bounds(), state);
691 operation.focusable(self.id.as_ref(), layout.bounds(), state);
692 }
693
694 fn update(
695 &mut self,
696 tree: &mut Tree,
697 event: &Event,
698 layout: Layout<'_>,
699 cursor: mouse::Cursor,
700 renderer: &Renderer,
701 clipboard: &mut dyn Clipboard,
702 shell: &mut Shell<'_, Message>,
703 _viewport: &Rectangle,
704 ) {
705 let update_cache = |state, value| {
706 replace_paragraph(
707 renderer,
708 state,
709 layout,
710 value,
711 self.font,
712 self.size,
713 self.line_height,
714 );
715 };
716
717 match &event {
718 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
719 | Event::Touch(touch::Event::FingerPressed { .. }) => {
720 let state = state::<Renderer>(tree);
721 let cursor_before = state.cursor;
722
723 let click_position = cursor.position_over(layout.bounds());
724
725 state.is_focused = if click_position.is_some() {
726 let now = Instant::now();
727
728 Some(Focus {
729 updated_at: now,
730 now,
731 is_window_focused: true,
732 })
733 } else {
734 None
735 };
736
737 if let Some(cursor_position) = click_position {
738 let text_layout = layout.children().next().unwrap();
739
740 let target = {
741 let text_bounds = text_layout.bounds();
742
743 let alignment_offset = alignment_offset(
744 text_bounds.width,
745 state.value.raw().min_width(),
746 self.alignment,
747 );
748
749 cursor_position.x - text_bounds.x - alignment_offset
750 };
751
752 let click = mouse::Click::new(
753 cursor_position,
754 mouse::Button::Left,
755 state.last_click,
756 );
757
758 match click.kind() {
759 click::Kind::Single => {
760 let position = if target > 0.0 {
761 let value = if self.is_secure {
762 self.value.secure()
763 } else {
764 self.value.clone()
765 };
766
767 find_cursor_position(
768 text_layout.bounds(),
769 &value,
770 state,
771 target,
772 )
773 } else {
774 None
775 }
776 .unwrap_or(0);
777
778 if state.keyboard_modifiers.shift() {
779 state.cursor.select_range(
780 state.cursor.start(&self.value),
781 position,
782 );
783 } else {
784 state.cursor.move_to(position);
785 }
786
787 state.is_dragging = Some(Drag::Select);
788 }
789 click::Kind::Double => {
790 if self.is_secure {
791 state.cursor.select_all(&self.value);
792
793 state.is_dragging = None;
794 } else {
795 let position = find_cursor_position(
796 text_layout.bounds(),
797 &self.value,
798 state,
799 target,
800 )
801 .unwrap_or(0);
802
803 state.cursor.select_range(
804 self.value.previous_start_of_word(position),
805 self.value.next_end_of_word(position),
806 );
807
808 state.is_dragging = Some(Drag::SelectWords {
809 anchor: position,
810 });
811 }
812 }
813 click::Kind::Triple => {
814 state.cursor.select_all(&self.value);
815 state.is_dragging = None;
816 }
817 }
818
819 state.last_click = Some(click);
820
821 if cursor_before != state.cursor {
822 shell.request_redraw();
823 }
824
825 shell.capture_event();
826 }
827 }
828 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
829 | Event::Touch(touch::Event::FingerLifted { .. })
830 | Event::Touch(touch::Event::FingerLost { .. }) => {
831 state::<Renderer>(tree).is_dragging = None;
832 }
833 Event::Mouse(mouse::Event::CursorMoved { position })
834 | Event::Touch(touch::Event::FingerMoved { position, .. }) => {
835 let state = state::<Renderer>(tree);
836
837 if let Some(is_dragging) = &state.is_dragging {
838 let text_layout = layout.children().next().unwrap();
839
840 let target = {
841 let text_bounds = text_layout.bounds();
842
843 let alignment_offset = alignment_offset(
844 text_bounds.width,
845 state.value.raw().min_width(),
846 self.alignment,
847 );
848
849 position.x - text_bounds.x - alignment_offset
850 };
851
852 let value = if self.is_secure {
853 self.value.secure()
854 } else {
855 self.value.clone()
856 };
857
858 let position = find_cursor_position(
859 text_layout.bounds(),
860 &value,
861 state,
862 target,
863 )
864 .unwrap_or(0);
865
866 let selection_before = state.cursor.selection(&value);
867
868 match is_dragging {
869 Drag::Select => {
870 state.cursor.select_range(
871 state.cursor.start(&value),
872 position,
873 );
874 }
875 Drag::SelectWords { anchor } => {
876 if position < *anchor {
877 state.cursor.select_range(
878 self.value.previous_start_of_word(position),
879 self.value.next_end_of_word(*anchor),
880 );
881 } else {
882 state.cursor.select_range(
883 self.value.previous_start_of_word(*anchor),
884 self.value.next_end_of_word(position),
885 );
886 }
887 }
888 }
889
890 if let Some(focus) = &mut state.is_focused {
891 focus.updated_at = Instant::now();
892 }
893
894 if selection_before != state.cursor.selection(&value) {
895 shell.request_redraw();
896 }
897
898 shell.capture_event();
899 }
900 }
901 Event::Keyboard(keyboard::Event::KeyPressed {
902 key,
903 text,
904 modified_key,
905 physical_key,
906 ..
907 }) => {
908 let state = state::<Renderer>(tree);
909
910 if let Some(focus) = &mut state.is_focused {
911 let modifiers = state.keyboard_modifiers;
912
913 match key.to_latin(*physical_key) {
914 Some('c')
915 if state.keyboard_modifiers.command()
916 && !self.is_secure =>
917 {
918 if let Some((start, end)) =
919 state.cursor.selection(&self.value)
920 {
921 clipboard.write(
922 clipboard::Kind::Standard,
923 self.value.select(start, end).to_string(),
924 );
925 }
926
927 shell.capture_event();
928 return;
929 }
930 Some('x')
931 if state.keyboard_modifiers.command()
932 && !self.is_secure =>
933 {
934 let Some(on_input) = &self.on_input else {
935 return;
936 };
937
938 if let Some((start, end)) =
939 state.cursor.selection(&self.value)
940 {
941 clipboard.write(
942 clipboard::Kind::Standard,
943 self.value.select(start, end).to_string(),
944 );
945 }
946
947 let mut editor =
948 Editor::new(&mut self.value, &mut state.cursor);
949 editor.delete();
950
951 let message = (on_input)(editor.contents());
952 shell.publish(message);
953 shell.capture_event();
954
955 focus.updated_at = Instant::now();
956 update_cache(state, &self.value);
957 return;
958 }
959 Some('v')
960 if state.keyboard_modifiers.command()
961 && !state.keyboard_modifiers.alt() =>
962 {
963 let Some(on_input) = &self.on_input else {
964 return;
965 };
966
967 let content = match state.is_pasting.take() {
968 Some(content) => content,
969 None => {
970 let content: String = clipboard
971 .read(clipboard::Kind::Standard)
972 .unwrap_or_default()
973 .chars()
974 .filter(|c| !c.is_control())
975 .collect();
976
977 Value::new(&content)
978 }
979 };
980
981 let mut editor =
982 Editor::new(&mut self.value, &mut state.cursor);
983 editor.paste(content.clone());
984
985 let message = if let Some(paste) = &self.on_paste {
986 (paste)(editor.contents())
987 } else {
988 (on_input)(editor.contents())
989 };
990 shell.publish(message);
991 shell.capture_event();
992
993 state.is_pasting = Some(content);
994 focus.updated_at = Instant::now();
995 update_cache(state, &self.value);
996 return;
997 }
998 Some('a') if state.keyboard_modifiers.command() => {
999 let cursor_before = state.cursor;
1000
1001 state.cursor.select_all(&self.value);
1002
1003 if cursor_before != state.cursor {
1004 focus.updated_at = Instant::now();
1005
1006 shell.request_redraw();
1007 }
1008
1009 shell.capture_event();
1010 return;
1011 }
1012 _ => {}
1013 }
1014
1015 if let Some(text) = text {
1016 let Some(on_input) = &self.on_input else {
1017 return;
1018 };
1019
1020 state.is_pasting = None;
1021
1022 if let Some(c) =
1023 text.chars().next().filter(|c| !c.is_control())
1024 {
1025 let mut editor =
1026 Editor::new(&mut self.value, &mut state.cursor);
1027
1028 editor.insert(c);
1029
1030 let message = (on_input)(editor.contents());
1031 shell.publish(message);
1032 shell.capture_event();
1033
1034 focus.updated_at = Instant::now();
1035 update_cache(state, &self.value);
1036 return;
1037 }
1038 }
1039
1040 #[cfg(target_os = "macos")]
1041 let macos_shortcut =
1042 crate::text_editor::convert_macos_shortcut(
1043 key, modifiers,
1044 );
1045
1046 #[cfg(target_os = "macos")]
1047 let modified_key =
1048 macos_shortcut.as_ref().unwrap_or(modified_key);
1049
1050 match modified_key.as_ref() {
1051 keyboard::Key::Named(key::Named::Enter) => {
1052 if let Some(on_submit) = self.on_submit.clone() {
1053 shell.publish(on_submit);
1054 shell.capture_event();
1055 }
1056 }
1057 keyboard::Key::Named(key::Named::Backspace) => {
1058 let Some(on_input) = &self.on_input else {
1059 return;
1060 };
1061
1062 if state.cursor.selection(&self.value).is_none() {
1063 if (self.is_secure && modifiers.jump())
1064 || modifiers.macos_command()
1065 {
1066 state.cursor.select_range(
1067 state.cursor.start(&self.value),
1068 0,
1069 );
1070 } else if modifiers.jump() {
1071 state
1072 .cursor
1073 .select_left_by_words(&self.value);
1074 }
1075 }
1076
1077 let mut editor =
1078 Editor::new(&mut self.value, &mut state.cursor);
1079 editor.backspace();
1080
1081 let message = (on_input)(editor.contents());
1082 shell.publish(message);
1083 shell.capture_event();
1084
1085 focus.updated_at = Instant::now();
1086 update_cache(state, &self.value);
1087 }
1088 keyboard::Key::Named(key::Named::Delete) => {
1089 let Some(on_input) = &self.on_input else {
1090 return;
1091 };
1092
1093 if state.cursor.selection(&self.value).is_none() {
1094 if (self.is_secure && modifiers.jump())
1095 || modifiers.macos_command()
1096 {
1097 state.cursor.select_range(
1098 state.cursor.start(&self.value),
1099 self.value.len(),
1100 );
1101 } else if modifiers.jump() {
1102 state
1103 .cursor
1104 .select_right_by_words(&self.value);
1105 }
1106 }
1107
1108 let mut editor =
1109 Editor::new(&mut self.value, &mut state.cursor);
1110 editor.delete();
1111
1112 let message = (on_input)(editor.contents());
1113 shell.publish(message);
1114 shell.capture_event();
1115
1116 focus.updated_at = Instant::now();
1117 update_cache(state, &self.value);
1118 }
1119 keyboard::Key::Named(key::Named::Home) => {
1120 let cursor_before = state.cursor;
1121
1122 if modifiers.shift() {
1123 state.cursor.select_range(
1124 state.cursor.start(&self.value),
1125 0,
1126 );
1127 } else {
1128 state.cursor.move_to(0);
1129 }
1130
1131 if cursor_before != state.cursor {
1132 focus.updated_at = Instant::now();
1133
1134 shell.request_redraw();
1135 }
1136
1137 shell.capture_event();
1138 }
1139 keyboard::Key::Named(key::Named::End) => {
1140 let cursor_before = state.cursor;
1141
1142 if modifiers.shift() {
1143 state.cursor.select_range(
1144 state.cursor.start(&self.value),
1145 self.value.len(),
1146 );
1147 } else {
1148 state.cursor.move_to(self.value.len());
1149 }
1150
1151 if cursor_before != state.cursor {
1152 focus.updated_at = Instant::now();
1153
1154 shell.request_redraw();
1155 }
1156
1157 shell.capture_event();
1158 }
1159 keyboard::Key::Named(key::Named::ArrowLeft) => {
1160 let cursor_before = state.cursor;
1161
1162 if (self.is_secure && modifiers.jump())
1163 || modifiers.macos_command()
1164 {
1165 if modifiers.shift() {
1166 state.cursor.select_range(
1167 state.cursor.start(&self.value),
1168 0,
1169 );
1170 } else {
1171 state.cursor.move_to(0);
1172 }
1173 } else if modifiers.jump() {
1174 if modifiers.shift() {
1175 state
1176 .cursor
1177 .select_left_by_words(&self.value);
1178 } else {
1179 state
1180 .cursor
1181 .move_left_by_words(&self.value);
1182 }
1183 } else if modifiers.shift() {
1184 state.cursor.select_left(&self.value);
1185 } else {
1186 state.cursor.move_left(&self.value);
1187 }
1188
1189 if cursor_before != state.cursor {
1190 focus.updated_at = Instant::now();
1191
1192 shell.request_redraw();
1193 }
1194
1195 shell.capture_event();
1196 }
1197 keyboard::Key::Named(key::Named::ArrowRight) => {
1198 let cursor_before = state.cursor;
1199
1200 if (self.is_secure && modifiers.jump())
1201 || modifiers.macos_command()
1202 {
1203 if modifiers.shift() {
1204 state.cursor.select_range(
1205 state.cursor.start(&self.value),
1206 self.value.len(),
1207 );
1208 } else {
1209 state.cursor.move_to(self.value.len());
1210 }
1211 } else if modifiers.jump() {
1212 if modifiers.shift() {
1213 state
1214 .cursor
1215 .select_right_by_words(&self.value);
1216 } else {
1217 state
1218 .cursor
1219 .move_right_by_words(&self.value);
1220 }
1221 } else if modifiers.shift() {
1222 state.cursor.select_right(&self.value);
1223 } else {
1224 state.cursor.move_right(&self.value);
1225 }
1226
1227 if cursor_before != state.cursor {
1228 focus.updated_at = Instant::now();
1229
1230 shell.request_redraw();
1231 }
1232
1233 shell.capture_event();
1234 }
1235 keyboard::Key::Named(key::Named::Escape) => {
1236 state.is_focused = None;
1237 state.is_dragging = None;
1238 state.is_pasting = None;
1239
1240 state.keyboard_modifiers =
1241 keyboard::Modifiers::default();
1242
1243 shell.capture_event();
1244 }
1245 _ => {}
1246 }
1247 }
1248 }
1249 Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => {
1250 let state = state::<Renderer>(tree);
1251
1252 if state.is_focused.is_some()
1253 && let keyboard::Key::Character("v") = key.as_ref()
1254 {
1255 state.is_pasting = None;
1256
1257 shell.capture_event();
1258 }
1259
1260 state.is_pasting = None;
1261 }
1262 Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
1263 let state = state::<Renderer>(tree);
1264
1265 state.keyboard_modifiers = *modifiers;
1266 }
1267 Event::InputMethod(event) => match event {
1268 input_method::Event::Opened | input_method::Event::Closed => {
1269 let state = state::<Renderer>(tree);
1270
1271 state.preedit =
1272 matches!(event, input_method::Event::Opened)
1273 .then(input_method::Preedit::new);
1274
1275 shell.request_redraw();
1276 }
1277 input_method::Event::Preedit(content, selection) => {
1278 let state = state::<Renderer>(tree);
1279
1280 if state.is_focused.is_some() {
1281 state.preedit = Some(input_method::Preedit {
1282 content: content.to_owned(),
1283 selection: selection.clone(),
1284 text_size: self.size,
1285 });
1286
1287 shell.request_redraw();
1288 }
1289 }
1290 input_method::Event::Commit(text) => {
1291 let state = state::<Renderer>(tree);
1292
1293 if let Some(focus) = &mut state.is_focused {
1294 let Some(on_input) = &self.on_input else {
1295 return;
1296 };
1297
1298 let mut editor =
1299 Editor::new(&mut self.value, &mut state.cursor);
1300 editor.paste(Value::new(text));
1301
1302 focus.updated_at = Instant::now();
1303 state.is_pasting = None;
1304
1305 let message = (on_input)(editor.contents());
1306 shell.publish(message);
1307 shell.capture_event();
1308
1309 update_cache(state, &self.value);
1310 }
1311 }
1312 },
1313 Event::Window(window::Event::Unfocused) => {
1314 let state = state::<Renderer>(tree);
1315
1316 if let Some(focus) = &mut state.is_focused {
1317 focus.is_window_focused = false;
1318 }
1319 }
1320 Event::Window(window::Event::Focused) => {
1321 let state = state::<Renderer>(tree);
1322
1323 if let Some(focus) = &mut state.is_focused {
1324 focus.is_window_focused = true;
1325 focus.updated_at = Instant::now();
1326
1327 shell.request_redraw();
1328 }
1329 }
1330 Event::Window(window::Event::RedrawRequested(now)) => {
1331 let state = state::<Renderer>(tree);
1332
1333 if let Some(focus) = &mut state.is_focused
1334 && focus.is_window_focused
1335 {
1336 if matches!(
1337 state.cursor.state(&self.value),
1338 cursor::State::Index(_)
1339 ) {
1340 focus.now = *now;
1341
1342 let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS
1343 - (*now - focus.updated_at).as_millis()
1344 % CURSOR_BLINK_INTERVAL_MILLIS;
1345
1346 shell.request_redraw_at(
1347 *now + Duration::from_millis(
1348 millis_until_redraw as u64,
1349 ),
1350 );
1351 }
1352
1353 shell.request_input_method(&self.input_method(
1354 state,
1355 layout,
1356 &self.value,
1357 ));
1358 }
1359 }
1360 _ => {}
1361 }
1362
1363 let state = state::<Renderer>(tree);
1364 let is_disabled = self.on_input.is_none();
1365
1366 let status = if is_disabled {
1367 Status::Disabled
1368 } else if state.is_focused() {
1369 Status::Focused {
1370 is_hovered: cursor.is_over(layout.bounds()),
1371 }
1372 } else if cursor.is_over(layout.bounds()) {
1373 Status::Hovered
1374 } else {
1375 Status::Active
1376 };
1377
1378 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
1379 self.last_status = Some(status);
1380 } else if self
1381 .last_status
1382 .is_some_and(|last_status| status != last_status)
1383 {
1384 shell.request_redraw();
1385 }
1386 }
1387
1388 fn draw(
1389 &self,
1390 tree: &Tree,
1391 renderer: &mut Renderer,
1392 theme: &Theme,
1393 _style: &renderer::Style,
1394 layout: Layout<'_>,
1395 cursor: mouse::Cursor,
1396 viewport: &Rectangle,
1397 ) {
1398 self.draw(tree, renderer, theme, layout, cursor, None, viewport);
1399 }
1400
1401 fn mouse_interaction(
1402 &self,
1403 _tree: &Tree,
1404 layout: Layout<'_>,
1405 cursor: mouse::Cursor,
1406 _viewport: &Rectangle,
1407 _renderer: &Renderer,
1408 ) -> mouse::Interaction {
1409 if cursor.is_over(layout.bounds()) {
1410 if self.on_input.is_none() {
1411 mouse::Interaction::Idle
1412 } else {
1413 mouse::Interaction::Text
1414 }
1415 } else {
1416 mouse::Interaction::default()
1417 }
1418 }
1419}
1420
1421impl<'a, Message, Theme, Renderer> From<TextInput<'a, Message, Theme, Renderer>>
1422 for Element<'a, Message, Theme, Renderer>
1423where
1424 Message: Clone + 'a,
1425 Theme: Catalog + 'a,
1426 Renderer: text::Renderer + 'a,
1427{
1428 fn from(
1429 text_input: TextInput<'a, Message, Theme, Renderer>,
1430 ) -> Element<'a, Message, Theme, Renderer> {
1431 Element::new(text_input)
1432 }
1433}
1434
1435#[derive(Debug, Clone)]
1437pub struct Icon<Font> {
1438 pub font: Font,
1440 pub code_point: char,
1442 pub size: Option<Pixels>,
1444 pub spacing: f32,
1446 pub side: Side,
1448}
1449
1450#[derive(Debug, Clone)]
1452pub enum Side {
1453 Left,
1455 Right,
1457}
1458
1459#[derive(Debug, Default, Clone)]
1461pub struct State<P: text::Paragraph> {
1462 value: paragraph::Plain<P>,
1463 placeholder: paragraph::Plain<P>,
1464 icon: paragraph::Plain<P>,
1465 is_focused: Option<Focus>,
1466 is_dragging: Option<Drag>,
1467 is_pasting: Option<Value>,
1468 preedit: Option<input_method::Preedit>,
1469 last_click: Option<mouse::Click>,
1470 cursor: Cursor,
1471 keyboard_modifiers: keyboard::Modifiers,
1472 }
1474
1475fn state<Renderer: text::Renderer>(
1476 tree: &mut Tree,
1477) -> &mut State<Renderer::Paragraph> {
1478 tree.state.downcast_mut::<State<Renderer::Paragraph>>()
1479}
1480
1481#[derive(Debug, Clone)]
1482struct Focus {
1483 updated_at: Instant,
1484 now: Instant,
1485 is_window_focused: bool,
1486}
1487
1488#[derive(Debug, Clone)]
1489enum Drag {
1490 Select,
1491 SelectWords { anchor: usize },
1492}
1493
1494impl<P: text::Paragraph> State<P> {
1495 pub fn new() -> Self {
1497 Self::default()
1498 }
1499
1500 pub fn is_focused(&self) -> bool {
1502 self.is_focused.is_some()
1503 }
1504
1505 pub fn cursor(&self) -> Cursor {
1507 self.cursor
1508 }
1509
1510 pub fn focus(&mut self) {
1512 let now = Instant::now();
1513
1514 self.is_focused = Some(Focus {
1515 updated_at: now,
1516 now,
1517 is_window_focused: true,
1518 });
1519
1520 self.move_cursor_to_end();
1521 }
1522
1523 pub fn unfocus(&mut self) {
1525 self.is_focused = None;
1526 }
1527
1528 pub fn move_cursor_to_front(&mut self) {
1530 self.cursor.move_to(0);
1531 }
1532
1533 pub fn move_cursor_to_end(&mut self) {
1535 self.cursor.move_to(usize::MAX);
1536 }
1537
1538 pub fn move_cursor_to(&mut self, position: usize) {
1540 self.cursor.move_to(position);
1541 }
1542
1543 pub fn select_all(&mut self) {
1545 self.cursor.select_range(0, usize::MAX);
1546 }
1547
1548 pub fn select_range(&mut self, start: usize, end: usize) {
1550 self.cursor.select_range(start, end);
1551 }
1552}
1553
1554impl<P: text::Paragraph> operation::Focusable for State<P> {
1555 fn is_focused(&self) -> bool {
1556 State::is_focused(self)
1557 }
1558
1559 fn focus(&mut self) {
1560 State::focus(self);
1561 }
1562
1563 fn unfocus(&mut self) {
1564 State::unfocus(self);
1565 }
1566}
1567
1568impl<P: text::Paragraph> operation::TextInput for State<P> {
1569 fn text(&self) -> &str {
1570 if self.value.content().is_empty() {
1571 self.placeholder.content()
1572 } else {
1573 self.value.content()
1574 }
1575 }
1576
1577 fn move_cursor_to_front(&mut self) {
1578 State::move_cursor_to_front(self);
1579 }
1580
1581 fn move_cursor_to_end(&mut self) {
1582 State::move_cursor_to_end(self);
1583 }
1584
1585 fn move_cursor_to(&mut self, position: usize) {
1586 State::move_cursor_to(self, position);
1587 }
1588
1589 fn select_all(&mut self) {
1590 State::select_all(self);
1591 }
1592
1593 fn select_range(&mut self, start: usize, end: usize) {
1594 State::select_range(self, start, end);
1595 }
1596}
1597
1598fn offset<P: text::Paragraph>(
1599 text_bounds: Rectangle,
1600 value: &Value,
1601 state: &State<P>,
1602) -> f32 {
1603 if state.is_focused() {
1604 let cursor = state.cursor();
1605
1606 let focus_position = match cursor.state(value) {
1607 cursor::State::Index(i) => i,
1608 cursor::State::Selection { end, .. } => end,
1609 };
1610
1611 let (_, offset) = measure_cursor_and_scroll_offset(
1612 state.value.raw(),
1613 text_bounds,
1614 focus_position,
1615 );
1616
1617 offset
1618 } else {
1619 0.0
1620 }
1621}
1622
1623fn measure_cursor_and_scroll_offset(
1624 paragraph: &impl text::Paragraph,
1625 text_bounds: Rectangle,
1626 cursor_index: usize,
1627) -> (f32, f32) {
1628 let grapheme_position = paragraph
1629 .grapheme_position(0, cursor_index)
1630 .unwrap_or(Point::ORIGIN);
1631
1632 let offset = ((grapheme_position.x + 5.0) - text_bounds.width).max(0.0);
1633
1634 (grapheme_position.x, offset)
1635}
1636
1637fn find_cursor_position<P: text::Paragraph>(
1640 text_bounds: Rectangle,
1641 value: &Value,
1642 state: &State<P>,
1643 x: f32,
1644) -> Option<usize> {
1645 let offset = offset(text_bounds, value, state);
1646 let value = value.to_string();
1647
1648 let char_offset = state
1649 .value
1650 .raw()
1651 .hit_test(Point::new(x + offset, text_bounds.height / 2.0))
1652 .map(text::Hit::cursor)?;
1653
1654 Some(
1655 unicode_segmentation::UnicodeSegmentation::graphemes(
1656 &value[..char_offset.min(value.len())],
1657 true,
1658 )
1659 .count(),
1660 )
1661}
1662
1663fn replace_paragraph<Renderer>(
1664 renderer: &Renderer,
1665 state: &mut State<Renderer::Paragraph>,
1666 layout: Layout<'_>,
1667 value: &Value,
1668 font: Option<Renderer::Font>,
1669 text_size: Option<Pixels>,
1670 line_height: text::LineHeight,
1671) where
1672 Renderer: text::Renderer,
1673{
1674 let font = font.unwrap_or_else(|| renderer.default_font());
1675 let text_size = text_size.unwrap_or_else(|| renderer.default_size());
1676
1677 let mut children_layout = layout.children();
1678 let text_bounds = children_layout.next().unwrap().bounds();
1679
1680 state.value = paragraph::Plain::new(Text {
1681 font,
1682 line_height,
1683 content: value.to_string(),
1684 bounds: Size::new(f32::INFINITY, text_bounds.height),
1685 size: text_size,
1686 align_x: text::Alignment::Default,
1687 align_y: alignment::Vertical::Center,
1688 shaping: text::Shaping::Advanced,
1689 wrapping: text::Wrapping::default(),
1690 });
1691}
1692
1693const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500;
1694
1695#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1697pub enum Status {
1698 Active,
1700 Hovered,
1702 Focused {
1704 is_hovered: bool,
1706 },
1707 Disabled,
1709}
1710
1711#[derive(Debug, Clone, Copy, PartialEq)]
1713pub struct Style {
1714 pub background: Background,
1716 pub border: Border,
1718 pub icon: Color,
1720 pub placeholder: Color,
1722 pub value: Color,
1724 pub selection: Color,
1726}
1727
1728pub trait Catalog: Sized {
1730 type Class<'a>;
1732
1733 fn default<'a>() -> Self::Class<'a>;
1735
1736 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
1738}
1739
1740pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
1744
1745impl Catalog for Theme {
1746 type Class<'a> = StyleFn<'a, Self>;
1747
1748 fn default<'a>() -> Self::Class<'a> {
1749 Box::new(default)
1750 }
1751
1752 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
1753 class(self, status)
1754 }
1755}
1756
1757pub fn default(theme: &Theme, status: Status) -> Style {
1759 let palette = theme.extended_palette();
1760
1761 let active = Style {
1762 background: Background::Color(palette.background.base.color),
1763 border: Border {
1764 radius: 2.0.into(),
1765 width: 1.0,
1766 color: palette.background.strong.color,
1767 },
1768 icon: palette.background.weak.text,
1769 placeholder: palette.secondary.base.color,
1770 value: palette.background.base.text,
1771 selection: palette.primary.weak.color,
1772 };
1773
1774 match status {
1775 Status::Active => active,
1776 Status::Hovered => Style {
1777 border: Border {
1778 color: palette.background.base.text,
1779 ..active.border
1780 },
1781 ..active
1782 },
1783 Status::Focused { .. } => Style {
1784 border: Border {
1785 color: palette.primary.strong.color,
1786 ..active.border
1787 },
1788 ..active
1789 },
1790 Status::Disabled => Style {
1791 background: Background::Color(palette.background.weak.color),
1792 value: active.placeholder,
1793 placeholder: palette.background.strongest.color,
1794 ..active
1795 },
1796 }
1797}
1798
1799fn alignment_offset(
1800 text_bounds_width: f32,
1801 text_min_width: f32,
1802 alignment: alignment::Horizontal,
1803) -> f32 {
1804 if text_min_width > text_bounds_width {
1805 0.0
1806 } else {
1807 match alignment {
1808 alignment::Horizontal::Left => 0.0,
1809 alignment::Horizontal::Center => {
1810 (text_bounds_width - text_min_width) / 2.0
1811 }
1812 alignment::Horizontal::Right => text_bounds_width - text_min_width,
1813 }
1814 }
1815}