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