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