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::event::{self, Event};
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 Background, Border, Color, Element, Layout, Length, Padding, Pixels, Point,
61 Rectangle, Shell, Size, Theme, Vector, Widget,
62};
63use crate::runtime::task::{self, Task};
64use crate::runtime::Action;
65
66#[allow(missing_debug_implementations)]
99pub struct TextInput<
100 'a,
101 Message,
102 Theme = crate::Theme,
103 Renderer = crate::Renderer,
104> where
105 Theme: Catalog,
106 Renderer: text::Renderer,
107{
108 id: Option<Id>,
109 placeholder: String,
110 value: Value,
111 is_secure: bool,
112 font: Option<Renderer::Font>,
113 width: Length,
114 padding: Padding,
115 size: Option<Pixels>,
116 line_height: text::LineHeight,
117 alignment: alignment::Horizontal,
118 on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
119 on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>,
120 on_submit: Option<Message>,
121 icon: Option<Icon<Renderer::Font>>,
122 class: Theme::Class<'a>,
123}
124
125pub const DEFAULT_PADDING: Padding = Padding::new(5.0);
127
128impl<'a, Message, Theme, Renderer> TextInput<'a, Message, Theme, Renderer>
129where
130 Message: Clone,
131 Theme: Catalog,
132 Renderer: text::Renderer,
133{
134 pub fn new(placeholder: &str, value: &str) -> Self {
137 TextInput {
138 id: None,
139 placeholder: String::from(placeholder),
140 value: Value::new(value),
141 is_secure: false,
142 font: None,
143 width: Length::Fill,
144 padding: DEFAULT_PADDING,
145 size: None,
146 line_height: text::LineHeight::default(),
147 alignment: alignment::Horizontal::Left,
148 on_input: None,
149 on_paste: None,
150 on_submit: None,
151 icon: None,
152 class: Theme::default(),
153 }
154 }
155
156 pub fn id(mut self, id: impl Into<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 &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 horizontal_alignment: alignment::Horizontal::Left,
321 vertical_alignment: alignment::Vertical::Center,
322 shaping: text::Shaping::Advanced,
323 wrapping: text::Wrapping::default(),
324 };
325
326 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 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 horizontal_alignment: alignment::Horizontal::Center,
346 vertical_alignment: alignment::Vertical::Center,
347 shaping: text::Shaping::Advanced,
348 wrapping: text::Wrapping::default(),
349 };
350
351 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 pub fn draw(
398 &self,
399 tree: &Tree,
400 renderer: &mut Renderer,
401 theme: &Theme,
402 layout: Layout<'_>,
403 cursor: mouse::Cursor,
404 value: Option<&Value>,
405 viewport: &Rectangle,
406 ) {
407 let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
408 let value = value.unwrap_or(&self.value);
409 let is_disabled = self.on_input.is_none();
410
411 let secure_value = self.is_secure.then(|| value.secure());
412 let value = secure_value.as_ref().unwrap_or(value);
413
414 let bounds = layout.bounds();
415
416 let mut children_layout = layout.children();
417 let text_bounds = children_layout.next().unwrap().bounds();
418
419 let is_mouse_over = cursor.is_over(bounds);
420
421 let status = if is_disabled {
422 Status::Disabled
423 } else if state.is_focused() {
424 Status::Focused
425 } else if is_mouse_over {
426 Status::Hovered
427 } else {
428 Status::Active
429 };
430
431 let style = theme.style(&self.class, status);
432
433 renderer.fill_quad(
434 renderer::Quad {
435 bounds,
436 border: style.border,
437 ..renderer::Quad::default()
438 },
439 style.background,
440 );
441
442 if self.icon.is_some() {
443 let icon_layout = children_layout.next().unwrap();
444
445 renderer.fill_paragraph(
446 state.icon.raw(),
447 icon_layout.bounds().center(),
448 style.icon,
449 *viewport,
450 );
451 }
452
453 let text = value.to_string();
454
455 let (cursor, offset, is_selecting) = if let Some(focus) = state
456 .is_focused
457 .as_ref()
458 .filter(|focus| focus.is_window_focused)
459 {
460 match state.cursor.state(value) {
461 cursor::State::Index(position) => {
462 let (text_value_width, offset) =
463 measure_cursor_and_scroll_offset(
464 state.value.raw(),
465 text_bounds,
466 position,
467 );
468
469 let is_cursor_visible = !is_disabled
470 && ((focus.now - focus.updated_at).as_millis()
471 / CURSOR_BLINK_INTERVAL_MILLIS)
472 % 2
473 == 0;
474
475 let cursor = if is_cursor_visible {
476 Some((
477 renderer::Quad {
478 bounds: Rectangle {
479 x: (text_bounds.x + text_value_width)
480 .floor(),
481 y: text_bounds.y,
482 width: 1.0,
483 height: text_bounds.height,
484 },
485 ..renderer::Quad::default()
486 },
487 style.value,
488 ))
489 } else {
490 None
491 };
492
493 (cursor, offset, false)
494 }
495 cursor::State::Selection { start, end } => {
496 let left = start.min(end);
497 let right = end.max(start);
498
499 let (left_position, left_offset) =
500 measure_cursor_and_scroll_offset(
501 state.value.raw(),
502 text_bounds,
503 left,
504 );
505
506 let (right_position, right_offset) =
507 measure_cursor_and_scroll_offset(
508 state.value.raw(),
509 text_bounds,
510 right,
511 );
512
513 let width = right_position - left_position;
514
515 (
516 Some((
517 renderer::Quad {
518 bounds: Rectangle {
519 x: text_bounds.x + left_position,
520 y: text_bounds.y,
521 width,
522 height: text_bounds.height,
523 },
524 ..renderer::Quad::default()
525 },
526 style.selection,
527 )),
528 if end == right {
529 right_offset
530 } else {
531 left_offset
532 },
533 true,
534 )
535 }
536 }
537 } else {
538 (None, 0.0, false)
539 };
540
541 let draw = |renderer: &mut Renderer, viewport| {
542 let paragraph = if text.is_empty() {
543 state.placeholder.raw()
544 } else {
545 state.value.raw()
546 };
547
548 let alignment_offset = alignment_offset(
549 text_bounds.width,
550 paragraph.min_width(),
551 self.alignment,
552 );
553
554 if let Some((cursor, color)) = cursor {
555 renderer.with_translation(
556 Vector::new(alignment_offset - offset, 0.0),
557 |renderer| {
558 renderer.fill_quad(cursor, color);
559 },
560 );
561 } else {
562 renderer.with_translation(Vector::ZERO, |_| {});
563 }
564
565 renderer.fill_paragraph(
566 paragraph,
567 Point::new(text_bounds.x, text_bounds.center_y())
568 + Vector::new(alignment_offset - offset, 0.0),
569 if text.is_empty() {
570 style.placeholder
571 } else {
572 style.value
573 },
574 viewport,
575 );
576 };
577
578 if is_selecting {
579 renderer
580 .with_layer(text_bounds, |renderer| draw(renderer, *viewport));
581 } else {
582 draw(renderer, text_bounds);
583 }
584 }
585}
586
587impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
588 for TextInput<'a, Message, Theme, Renderer>
589where
590 Message: Clone,
591 Theme: Catalog,
592 Renderer: text::Renderer,
593{
594 fn tag(&self) -> tree::Tag {
595 tree::Tag::of::<State<Renderer::Paragraph>>()
596 }
597
598 fn state(&self) -> tree::State {
599 tree::State::new(State::<Renderer::Paragraph>::new())
600 }
601
602 fn diff(&mut self, tree: &mut Tree) {
603 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
604
605 if self.on_input.is_none() {
607 state.is_pasting = None;
608 }
609 }
610
611 fn size(&self) -> Size<Length> {
612 Size {
613 width: self.width,
614 height: Length::Shrink,
615 }
616 }
617
618 fn layout(
619 &self,
620 tree: &mut Tree,
621 renderer: &Renderer,
622 limits: &layout::Limits,
623 ) -> layout::Node {
624 self.layout(tree, renderer, limits, None)
625 }
626
627 fn operate(
628 &self,
629 tree: &mut Tree,
630 _layout: Layout<'_>,
631 _renderer: &Renderer,
632 operation: &mut dyn Operation,
633 ) {
634 let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
635
636 operation.focusable(state, self.id.as_ref().map(|id| &id.0));
637 operation.text_input(state, self.id.as_ref().map(|id| &id.0));
638 }
639
640 fn on_event(
641 &mut self,
642 tree: &mut Tree,
643 event: Event,
644 layout: Layout<'_>,
645 cursor: mouse::Cursor,
646 renderer: &Renderer,
647 clipboard: &mut dyn Clipboard,
648 shell: &mut Shell<'_, Message>,
649 _viewport: &Rectangle,
650 ) -> event::Status {
651 let update_cache = |state, value| {
652 replace_paragraph(
653 renderer,
654 state,
655 layout,
656 value,
657 self.font,
658 self.size,
659 self.line_height,
660 );
661 };
662
663 match event {
664 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
665 | Event::Touch(touch::Event::FingerPressed { .. }) => {
666 let state = state::<Renderer>(tree);
667
668 let click_position = cursor.position_over(layout.bounds());
669
670 state.is_focused = if click_position.is_some() {
671 state.is_focused.or_else(|| {
672 let now = Instant::now();
673
674 Some(Focus {
675 updated_at: now,
676 now,
677 is_window_focused: true,
678 })
679 })
680 } else {
681 None
682 };
683
684 if let Some(cursor_position) = click_position {
685 let text_layout = layout.children().next().unwrap();
686
687 let target = {
688 let text_bounds = text_layout.bounds();
689
690 let alignment_offset = alignment_offset(
691 text_bounds.width,
692 state.value.raw().min_width(),
693 self.alignment,
694 );
695
696 cursor_position.x - text_bounds.x - alignment_offset
697 };
698
699 let click = mouse::Click::new(
700 cursor_position,
701 mouse::Button::Left,
702 state.last_click,
703 );
704
705 match click.kind() {
706 click::Kind::Single => {
707 let position = if target > 0.0 {
708 let value = if self.is_secure {
709 self.value.secure()
710 } else {
711 self.value.clone()
712 };
713
714 find_cursor_position(
715 text_layout.bounds(),
716 &value,
717 state,
718 target,
719 )
720 } else {
721 None
722 }
723 .unwrap_or(0);
724
725 if state.keyboard_modifiers.shift() {
726 state.cursor.select_range(
727 state.cursor.start(&self.value),
728 position,
729 );
730 } else {
731 state.cursor.move_to(position);
732 }
733 state.is_dragging = true;
734 }
735 click::Kind::Double => {
736 if self.is_secure {
737 state.cursor.select_all(&self.value);
738 } else {
739 let position = find_cursor_position(
740 text_layout.bounds(),
741 &self.value,
742 state,
743 target,
744 )
745 .unwrap_or(0);
746
747 state.cursor.select_range(
748 self.value.previous_start_of_word(position),
749 self.value.next_end_of_word(position),
750 );
751 }
752
753 state.is_dragging = false;
754 }
755 click::Kind::Triple => {
756 state.cursor.select_all(&self.value);
757 state.is_dragging = false;
758 }
759 }
760
761 state.last_click = Some(click);
762
763 return event::Status::Captured;
764 }
765 }
766 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
767 | Event::Touch(touch::Event::FingerLifted { .. })
768 | Event::Touch(touch::Event::FingerLost { .. }) => {
769 state::<Renderer>(tree).is_dragging = false;
770 }
771 Event::Mouse(mouse::Event::CursorMoved { position })
772 | Event::Touch(touch::Event::FingerMoved { position, .. }) => {
773 let state = state::<Renderer>(tree);
774
775 if state.is_dragging {
776 let text_layout = layout.children().next().unwrap();
777
778 let target = {
779 let text_bounds = text_layout.bounds();
780
781 let alignment_offset = alignment_offset(
782 text_bounds.width,
783 state.value.raw().min_width(),
784 self.alignment,
785 );
786
787 position.x - text_bounds.x - alignment_offset
788 };
789
790 let value = if self.is_secure {
791 self.value.secure()
792 } else {
793 self.value.clone()
794 };
795
796 let position = find_cursor_position(
797 text_layout.bounds(),
798 &value,
799 state,
800 target,
801 )
802 .unwrap_or(0);
803
804 state
805 .cursor
806 .select_range(state.cursor.start(&value), position);
807
808 return event::Status::Captured;
809 }
810 }
811 Event::Keyboard(keyboard::Event::KeyPressed {
812 key, text, ..
813 }) => {
814 let state = state::<Renderer>(tree);
815
816 if let Some(focus) = &mut state.is_focused {
817 let modifiers = state.keyboard_modifiers;
818 focus.updated_at = Instant::now();
819
820 match key.as_ref() {
821 keyboard::Key::Character("c")
822 if state.keyboard_modifiers.command()
823 && !self.is_secure =>
824 {
825 if let Some((start, end)) =
826 state.cursor.selection(&self.value)
827 {
828 clipboard.write(
829 clipboard::Kind::Standard,
830 self.value.select(start, end).to_string(),
831 );
832 }
833
834 return event::Status::Captured;
835 }
836 keyboard::Key::Character("x")
837 if state.keyboard_modifiers.command()
838 && !self.is_secure =>
839 {
840 let Some(on_input) = &self.on_input else {
841 return event::Status::Ignored;
842 };
843
844 if let Some((start, end)) =
845 state.cursor.selection(&self.value)
846 {
847 clipboard.write(
848 clipboard::Kind::Standard,
849 self.value.select(start, end).to_string(),
850 );
851 }
852
853 let mut editor =
854 Editor::new(&mut self.value, &mut state.cursor);
855 editor.delete();
856
857 let message = (on_input)(editor.contents());
858 shell.publish(message);
859
860 update_cache(state, &self.value);
861
862 return event::Status::Captured;
863 }
864 keyboard::Key::Character("v")
865 if state.keyboard_modifiers.command()
866 && !state.keyboard_modifiers.alt() =>
867 {
868 let Some(on_input) = &self.on_input else {
869 return event::Status::Ignored;
870 };
871
872 let content = match state.is_pasting.take() {
873 Some(content) => content,
874 None => {
875 let content: String = clipboard
876 .read(clipboard::Kind::Standard)
877 .unwrap_or_default()
878 .chars()
879 .filter(|c| !c.is_control())
880 .collect();
881
882 Value::new(&content)
883 }
884 };
885
886 let mut editor =
887 Editor::new(&mut self.value, &mut state.cursor);
888
889 editor.paste(content.clone());
890
891 let message = if let Some(paste) = &self.on_paste {
892 (paste)(editor.contents())
893 } else {
894 (on_input)(editor.contents())
895 };
896 shell.publish(message);
897
898 state.is_pasting = Some(content);
899
900 update_cache(state, &self.value);
901
902 return event::Status::Captured;
903 }
904 keyboard::Key::Character("a")
905 if state.keyboard_modifiers.command() =>
906 {
907 state.cursor.select_all(&self.value);
908
909 return event::Status::Captured;
910 }
911 _ => {}
912 }
913
914 if let Some(text) = text {
915 let Some(on_input) = &self.on_input else {
916 return event::Status::Ignored;
917 };
918
919 state.is_pasting = None;
920
921 if let Some(c) =
922 text.chars().next().filter(|c| !c.is_control())
923 {
924 let mut editor =
925 Editor::new(&mut self.value, &mut state.cursor);
926
927 editor.insert(c);
928
929 let message = (on_input)(editor.contents());
930 shell.publish(message);
931
932 focus.updated_at = Instant::now();
933
934 update_cache(state, &self.value);
935
936 return event::Status::Captured;
937 }
938 }
939
940 match key.as_ref() {
941 keyboard::Key::Named(key::Named::Enter) => {
942 if let Some(on_submit) = self.on_submit.clone() {
943 shell.publish(on_submit);
944 }
945 }
946 keyboard::Key::Named(key::Named::Backspace) => {
947 let Some(on_input) = &self.on_input else {
948 return event::Status::Ignored;
949 };
950
951 if modifiers.jump()
952 && state.cursor.selection(&self.value).is_none()
953 {
954 if self.is_secure {
955 let cursor_pos =
956 state.cursor.end(&self.value);
957 state.cursor.select_range(0, cursor_pos);
958 } else {
959 state
960 .cursor
961 .select_left_by_words(&self.value);
962 }
963 }
964
965 let mut editor =
966 Editor::new(&mut self.value, &mut state.cursor);
967 editor.backspace();
968
969 let message = (on_input)(editor.contents());
970 shell.publish(message);
971
972 update_cache(state, &self.value);
973 }
974 keyboard::Key::Named(key::Named::Delete) => {
975 let Some(on_input) = &self.on_input else {
976 return event::Status::Ignored;
977 };
978
979 if modifiers.jump()
980 && state.cursor.selection(&self.value).is_none()
981 {
982 if self.is_secure {
983 let cursor_pos =
984 state.cursor.end(&self.value);
985 state.cursor.select_range(
986 cursor_pos,
987 self.value.len(),
988 );
989 } else {
990 state
991 .cursor
992 .select_right_by_words(&self.value);
993 }
994 }
995
996 let mut editor =
997 Editor::new(&mut self.value, &mut state.cursor);
998 editor.delete();
999
1000 let message = (on_input)(editor.contents());
1001 shell.publish(message);
1002
1003 update_cache(state, &self.value);
1004 }
1005 keyboard::Key::Named(key::Named::Home) => {
1006 if modifiers.shift() {
1007 state.cursor.select_range(
1008 state.cursor.start(&self.value),
1009 0,
1010 );
1011 } else {
1012 state.cursor.move_to(0);
1013 }
1014 }
1015 keyboard::Key::Named(key::Named::End) => {
1016 if modifiers.shift() {
1017 state.cursor.select_range(
1018 state.cursor.start(&self.value),
1019 self.value.len(),
1020 );
1021 } else {
1022 state.cursor.move_to(self.value.len());
1023 }
1024 }
1025 keyboard::Key::Named(key::Named::ArrowLeft)
1026 if modifiers.macos_command() =>
1027 {
1028 if modifiers.shift() {
1029 state.cursor.select_range(
1030 state.cursor.start(&self.value),
1031 0,
1032 );
1033 } else {
1034 state.cursor.move_to(0);
1035 }
1036 }
1037 keyboard::Key::Named(key::Named::ArrowRight)
1038 if modifiers.macos_command() =>
1039 {
1040 if modifiers.shift() {
1041 state.cursor.select_range(
1042 state.cursor.start(&self.value),
1043 self.value.len(),
1044 );
1045 } else {
1046 state.cursor.move_to(self.value.len());
1047 }
1048 }
1049 keyboard::Key::Named(key::Named::ArrowLeft) => {
1050 if modifiers.jump() && !self.is_secure {
1051 if modifiers.shift() {
1052 state
1053 .cursor
1054 .select_left_by_words(&self.value);
1055 } else {
1056 state
1057 .cursor
1058 .move_left_by_words(&self.value);
1059 }
1060 } else if modifiers.shift() {
1061 state.cursor.select_left(&self.value);
1062 } else {
1063 state.cursor.move_left(&self.value);
1064 }
1065 }
1066 keyboard::Key::Named(key::Named::ArrowRight) => {
1067 if modifiers.jump() && !self.is_secure {
1068 if modifiers.shift() {
1069 state
1070 .cursor
1071 .select_right_by_words(&self.value);
1072 } else {
1073 state
1074 .cursor
1075 .move_right_by_words(&self.value);
1076 }
1077 } else if modifiers.shift() {
1078 state.cursor.select_right(&self.value);
1079 } else {
1080 state.cursor.move_right(&self.value);
1081 }
1082 }
1083 keyboard::Key::Named(key::Named::Escape) => {
1084 state.is_focused = None;
1085 state.is_dragging = false;
1086 state.is_pasting = None;
1087
1088 state.keyboard_modifiers =
1089 keyboard::Modifiers::default();
1090 }
1091 keyboard::Key::Named(
1092 key::Named::Tab
1093 | key::Named::ArrowUp
1094 | key::Named::ArrowDown,
1095 ) => {
1096 return event::Status::Ignored;
1097 }
1098 _ => {}
1099 }
1100
1101 return event::Status::Captured;
1102 }
1103 }
1104 Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => {
1105 let state = state::<Renderer>(tree);
1106
1107 if state.is_focused.is_some() {
1108 match key.as_ref() {
1109 keyboard::Key::Character("v") => {
1110 state.is_pasting = None;
1111 }
1112 keyboard::Key::Named(
1113 key::Named::Tab
1114 | key::Named::ArrowUp
1115 | key::Named::ArrowDown,
1116 ) => {
1117 return event::Status::Ignored;
1118 }
1119 _ => {}
1120 }
1121
1122 return event::Status::Captured;
1123 }
1124
1125 state.is_pasting = None;
1126 }
1127 Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
1128 let state = state::<Renderer>(tree);
1129
1130 state.keyboard_modifiers = modifiers;
1131 }
1132 Event::Window(window::Event::Unfocused) => {
1133 let state = state::<Renderer>(tree);
1134
1135 if let Some(focus) = &mut state.is_focused {
1136 focus.is_window_focused = false;
1137 }
1138 }
1139 Event::Window(window::Event::Focused) => {
1140 let state = state::<Renderer>(tree);
1141
1142 if let Some(focus) = &mut state.is_focused {
1143 focus.is_window_focused = true;
1144 focus.updated_at = Instant::now();
1145
1146 shell.request_redraw(window::RedrawRequest::NextFrame);
1147 }
1148 }
1149 Event::Window(window::Event::RedrawRequested(now)) => {
1150 let state = state::<Renderer>(tree);
1151
1152 if let Some(focus) = &mut state.is_focused {
1153 if focus.is_window_focused {
1154 focus.now = now;
1155
1156 let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS
1157 - (now - focus.updated_at).as_millis()
1158 % CURSOR_BLINK_INTERVAL_MILLIS;
1159
1160 shell.request_redraw(window::RedrawRequest::At(
1161 now + Duration::from_millis(
1162 millis_until_redraw as u64,
1163 ),
1164 ));
1165 }
1166 }
1167 }
1168 _ => {}
1169 }
1170
1171 event::Status::Ignored
1172 }
1173
1174 fn draw(
1175 &self,
1176 tree: &Tree,
1177 renderer: &mut Renderer,
1178 theme: &Theme,
1179 _style: &renderer::Style,
1180 layout: Layout<'_>,
1181 cursor: mouse::Cursor,
1182 viewport: &Rectangle,
1183 ) {
1184 self.draw(tree, renderer, theme, layout, cursor, None, viewport);
1185 }
1186
1187 fn mouse_interaction(
1188 &self,
1189 _state: &Tree,
1190 layout: Layout<'_>,
1191 cursor: mouse::Cursor,
1192 _viewport: &Rectangle,
1193 _renderer: &Renderer,
1194 ) -> mouse::Interaction {
1195 if cursor.is_over(layout.bounds()) {
1196 if self.on_input.is_none() {
1197 mouse::Interaction::Idle
1198 } else {
1199 mouse::Interaction::Text
1200 }
1201 } else {
1202 mouse::Interaction::default()
1203 }
1204 }
1205}
1206
1207impl<'a, Message, Theme, Renderer> From<TextInput<'a, Message, Theme, Renderer>>
1208 for Element<'a, Message, Theme, Renderer>
1209where
1210 Message: Clone + 'a,
1211 Theme: Catalog + 'a,
1212 Renderer: text::Renderer + 'a,
1213{
1214 fn from(
1215 text_input: TextInput<'a, Message, Theme, Renderer>,
1216 ) -> Element<'a, Message, Theme, Renderer> {
1217 Element::new(text_input)
1218 }
1219}
1220
1221#[derive(Debug, Clone)]
1223pub struct Icon<Font> {
1224 pub font: Font,
1226 pub code_point: char,
1228 pub size: Option<Pixels>,
1230 pub spacing: f32,
1232 pub side: Side,
1234}
1235
1236#[derive(Debug, Clone)]
1238pub enum Side {
1239 Left,
1241 Right,
1243}
1244
1245#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1247pub struct Id(widget::Id);
1248
1249impl Id {
1250 pub fn new(id: impl Into<std::borrow::Cow<'static, str>>) -> Self {
1252 Self(widget::Id::new(id))
1253 }
1254
1255 pub fn unique() -> Self {
1259 Self(widget::Id::unique())
1260 }
1261}
1262
1263impl From<Id> for widget::Id {
1264 fn from(id: Id) -> Self {
1265 id.0
1266 }
1267}
1268
1269impl From<&'static str> for Id {
1270 fn from(id: &'static str) -> Self {
1271 Self::new(id)
1272 }
1273}
1274
1275impl From<String> for Id {
1276 fn from(id: String) -> Self {
1277 Self::new(id)
1278 }
1279}
1280
1281pub fn focus<T>(id: impl Into<Id>) -> Task<T> {
1283 task::effect(Action::widget(operation::focusable::focus(id.into().0)))
1284}
1285
1286pub fn move_cursor_to_end<T>(id: impl Into<Id>) -> Task<T> {
1289 task::effect(Action::widget(operation::text_input::move_cursor_to_end(
1290 id.into().0,
1291 )))
1292}
1293
1294pub fn move_cursor_to_front<T>(id: impl Into<Id>) -> Task<T> {
1297 task::effect(Action::widget(operation::text_input::move_cursor_to_front(
1298 id.into().0,
1299 )))
1300}
1301
1302pub fn move_cursor_to<T>(id: impl Into<Id>, position: usize) -> Task<T> {
1305 task::effect(Action::widget(operation::text_input::move_cursor_to(
1306 id.into().0,
1307 position,
1308 )))
1309}
1310
1311pub fn select_all<T>(id: impl Into<Id>) -> Task<T> {
1313 task::effect(Action::widget(operation::text_input::select_all(
1314 id.into().0,
1315 )))
1316}
1317
1318#[derive(Debug, Default, Clone)]
1320pub struct State<P: text::Paragraph> {
1321 value: paragraph::Plain<P>,
1322 placeholder: paragraph::Plain<P>,
1323 icon: paragraph::Plain<P>,
1324 is_focused: Option<Focus>,
1325 is_dragging: bool,
1326 is_pasting: Option<Value>,
1327 last_click: Option<mouse::Click>,
1328 cursor: Cursor,
1329 keyboard_modifiers: keyboard::Modifiers,
1330 }
1332
1333fn state<Renderer: text::Renderer>(
1334 tree: &mut Tree,
1335) -> &mut State<Renderer::Paragraph> {
1336 tree.state.downcast_mut::<State<Renderer::Paragraph>>()
1337}
1338
1339#[derive(Debug, Clone, Copy)]
1340struct Focus {
1341 updated_at: Instant,
1342 now: Instant,
1343 is_window_focused: bool,
1344}
1345
1346impl<P: text::Paragraph> State<P> {
1347 pub fn new() -> Self {
1349 Self::default()
1350 }
1351
1352 pub fn is_focused(&self) -> bool {
1354 self.is_focused.is_some()
1355 }
1356
1357 pub fn cursor(&self) -> Cursor {
1359 self.cursor
1360 }
1361
1362 pub fn focus(&mut self) {
1364 let now = Instant::now();
1365
1366 self.is_focused = Some(Focus {
1367 updated_at: now,
1368 now,
1369 is_window_focused: true,
1370 });
1371
1372 self.move_cursor_to_end();
1373 }
1374
1375 pub fn unfocus(&mut self) {
1377 self.is_focused = None;
1378 }
1379
1380 pub fn move_cursor_to_front(&mut self) {
1382 self.cursor.move_to(0);
1383 }
1384
1385 pub fn move_cursor_to_end(&mut self) {
1387 self.cursor.move_to(usize::MAX);
1388 }
1389
1390 pub fn move_cursor_to(&mut self, position: usize) {
1392 self.cursor.move_to(position);
1393 }
1394
1395 pub fn select_all(&mut self) {
1397 self.cursor.select_range(0, usize::MAX);
1398 }
1399}
1400
1401impl<P: text::Paragraph> operation::Focusable for State<P> {
1402 fn is_focused(&self) -> bool {
1403 State::is_focused(self)
1404 }
1405
1406 fn focus(&mut self) {
1407 State::focus(self);
1408 }
1409
1410 fn unfocus(&mut self) {
1411 State::unfocus(self);
1412 }
1413}
1414
1415impl<P: text::Paragraph> operation::TextInput for State<P> {
1416 fn move_cursor_to_front(&mut self) {
1417 State::move_cursor_to_front(self);
1418 }
1419
1420 fn move_cursor_to_end(&mut self) {
1421 State::move_cursor_to_end(self);
1422 }
1423
1424 fn move_cursor_to(&mut self, position: usize) {
1425 State::move_cursor_to(self, position);
1426 }
1427
1428 fn select_all(&mut self) {
1429 State::select_all(self);
1430 }
1431}
1432
1433fn offset<P: text::Paragraph>(
1434 text_bounds: Rectangle,
1435 value: &Value,
1436 state: &State<P>,
1437) -> f32 {
1438 if state.is_focused() {
1439 let cursor = state.cursor();
1440
1441 let focus_position = match cursor.state(value) {
1442 cursor::State::Index(i) => i,
1443 cursor::State::Selection { end, .. } => end,
1444 };
1445
1446 let (_, offset) = measure_cursor_and_scroll_offset(
1447 state.value.raw(),
1448 text_bounds,
1449 focus_position,
1450 );
1451
1452 offset
1453 } else {
1454 0.0
1455 }
1456}
1457
1458fn measure_cursor_and_scroll_offset(
1459 paragraph: &impl text::Paragraph,
1460 text_bounds: Rectangle,
1461 cursor_index: usize,
1462) -> (f32, f32) {
1463 let grapheme_position = paragraph
1464 .grapheme_position(0, cursor_index)
1465 .unwrap_or(Point::ORIGIN);
1466
1467 let offset = ((grapheme_position.x + 5.0) - text_bounds.width).max(0.0);
1468
1469 (grapheme_position.x, offset)
1470}
1471
1472fn find_cursor_position<P: text::Paragraph>(
1475 text_bounds: Rectangle,
1476 value: &Value,
1477 state: &State<P>,
1478 x: f32,
1479) -> Option<usize> {
1480 let offset = offset(text_bounds, value, state);
1481 let value = value.to_string();
1482
1483 let char_offset = state
1484 .value
1485 .raw()
1486 .hit_test(Point::new(x + offset, text_bounds.height / 2.0))
1487 .map(text::Hit::cursor)?;
1488
1489 Some(
1490 unicode_segmentation::UnicodeSegmentation::graphemes(
1491 &value[..char_offset.min(value.len())],
1492 true,
1493 )
1494 .count(),
1495 )
1496}
1497
1498fn replace_paragraph<Renderer>(
1499 renderer: &Renderer,
1500 state: &mut State<Renderer::Paragraph>,
1501 layout: Layout<'_>,
1502 value: &Value,
1503 font: Option<Renderer::Font>,
1504 text_size: Option<Pixels>,
1505 line_height: text::LineHeight,
1506) where
1507 Renderer: text::Renderer,
1508{
1509 let font = font.unwrap_or_else(|| renderer.default_font());
1510 let text_size = text_size.unwrap_or_else(|| renderer.default_size());
1511
1512 let mut children_layout = layout.children();
1513 let text_bounds = children_layout.next().unwrap().bounds();
1514
1515 state.value = paragraph::Plain::new(Text {
1516 font,
1517 line_height,
1518 content: &value.to_string(),
1519 bounds: Size::new(f32::INFINITY, text_bounds.height),
1520 size: text_size,
1521 horizontal_alignment: alignment::Horizontal::Left,
1522 vertical_alignment: alignment::Vertical::Top,
1523 shaping: text::Shaping::Advanced,
1524 wrapping: text::Wrapping::default(),
1525 });
1526}
1527
1528const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500;
1529
1530#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1532pub enum Status {
1533 Active,
1535 Hovered,
1537 Focused,
1539 Disabled,
1541}
1542
1543#[derive(Debug, Clone, Copy, PartialEq)]
1545pub struct Style {
1546 pub background: Background,
1548 pub border: Border,
1550 pub icon: Color,
1552 pub placeholder: Color,
1554 pub value: Color,
1556 pub selection: Color,
1558}
1559
1560pub trait Catalog: Sized {
1562 type Class<'a>;
1564
1565 fn default<'a>() -> Self::Class<'a>;
1567
1568 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
1570}
1571
1572pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
1576
1577impl Catalog for Theme {
1578 type Class<'a> = StyleFn<'a, Self>;
1579
1580 fn default<'a>() -> Self::Class<'a> {
1581 Box::new(default)
1582 }
1583
1584 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
1585 class(self, status)
1586 }
1587}
1588
1589pub fn default(theme: &Theme, status: Status) -> Style {
1591 let palette = theme.extended_palette();
1592
1593 let active = Style {
1594 background: Background::Color(palette.background.base.color),
1595 border: Border {
1596 radius: 2.0.into(),
1597 width: 1.0,
1598 color: palette.background.strong.color,
1599 },
1600 icon: palette.background.weak.text,
1601 placeholder: palette.background.strong.color,
1602 value: palette.background.base.text,
1603 selection: palette.primary.weak.color,
1604 };
1605
1606 match status {
1607 Status::Active => active,
1608 Status::Hovered => Style {
1609 border: Border {
1610 color: palette.background.base.text,
1611 ..active.border
1612 },
1613 ..active
1614 },
1615 Status::Focused => Style {
1616 border: Border {
1617 color: palette.primary.strong.color,
1618 ..active.border
1619 },
1620 ..active
1621 },
1622 Status::Disabled => Style {
1623 background: Background::Color(palette.background.weak.color),
1624 value: active.placeholder,
1625 ..active
1626 },
1627 }
1628}
1629
1630fn alignment_offset(
1631 text_bounds_width: f32,
1632 text_min_width: f32,
1633 alignment: alignment::Horizontal,
1634) -> f32 {
1635 if text_min_width > text_bounds_width {
1636 0.0
1637 } else {
1638 match alignment {
1639 alignment::Horizontal::Left => 0.0,
1640 alignment::Horizontal::Center => {
1641 (text_bounds_width - text_min_width) / 2.0
1642 }
1643 alignment::Horizontal::Right => text_bounds_width - text_min_width,
1644 }
1645 }
1646}