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