1use super::*;
4use iced_widget::core::text::Paragraph;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7enum LabelMode {
8 Placeholder,
9 Floating,
10}
11
12fn input_layer_style_alpha(
13 theme: &Theme,
14 status: iced_text_input::Status,
15 content_alpha: f32,
16) -> iced_text_input::Style {
17 let mut style = text_input_style::default(theme, status);
18
19 style.background = Background::Color(Color::TRANSPARENT);
20 style.border.width = 0.0;
21 style.border.color = Color::TRANSPARENT;
22 style.icon = alpha_color(style.icon, content_alpha);
23 style.placeholder = Color::TRANSPARENT;
24 style.value = alpha_color(style.value, content_alpha);
25 style.selection = alpha_color(style.selection, content_alpha);
26
27 style
28}
29
30fn status_style(
31 theme: &Theme,
32 is_enabled: bool,
33 is_error: bool,
34 is_focused: bool,
35 is_hovered: bool,
36) -> (Color, f32, Color) {
37 let colors = theme.colors();
38
39 if !is_enabled {
40 (
41 alpha_color(
42 colors.surface.text,
43 tokens::component::text_field::DISABLED_OUTLINE_OPACITY,
44 ),
45 tokens::component::text_field::OUTLINE_WIDTH,
46 alpha_color(
47 colors.surface.text,
48 tokens::component::text_field::DISABLED_LABEL_TEXT_OPACITY,
49 ),
50 )
51 } else if is_error {
52 let outline_width = if is_focused {
53 tokens::component::text_field::FOCUS_OUTLINE_WIDTH
54 } else if is_hovered {
55 tokens::component::text_field::HOVER_OUTLINE_WIDTH
56 } else {
57 tokens::component::text_field::OUTLINE_WIDTH
58 };
59
60 (colors.error.color, outline_width, colors.error.color)
61 } else if is_focused {
62 (
63 colors.primary.color,
64 tokens::component::text_field::FOCUS_OUTLINE_WIDTH,
65 colors.primary.color,
66 )
67 } else if is_hovered {
68 (
69 colors.surface.text,
70 tokens::component::text_field::HOVER_OUTLINE_WIDTH,
71 colors.surface.text,
72 )
73 } else {
74 (
75 colors.outline.color,
76 tokens::component::text_field::OUTLINE_WIDTH,
77 colors.surface.text_variant,
78 )
79 }
80}
81
82pub struct TextInput<'a, Message, Renderer = iced_widget::Renderer>
84where
85 Renderer: iced_widget::core::Renderer + core_text::Renderer,
86{
87 label: text::Fragment<'a>,
88 value: String,
89 is_populated: bool,
90 is_enabled: bool,
91 is_secure: bool,
92 is_error: bool,
93 content_alpha: f32,
94 width: Length,
95 font: Option<Renderer::Font>,
96 label_mode: LabelMode,
97 input: IcedTextInput<'a, Message, Theme, Renderer>,
98}
99
100impl<Message, Renderer> std::fmt::Debug for TextInput<'_, Message, Renderer>
101where
102 Renderer: iced_widget::core::Renderer + core_text::Renderer,
103{
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 f.debug_struct("TextInput")
106 .field("is_populated", &self.is_populated)
107 .field("is_enabled", &self.is_enabled)
108 .field("is_secure", &self.is_secure)
109 .field("is_error", &self.is_error)
110 .field("content_alpha", &self.content_alpha)
111 .field("width", &self.width)
112 .field("label_mode", &self.label_mode)
113 .finish_non_exhaustive()
114 }
115}
116
117impl<'a, Message, Renderer> TextInput<'a, Message, Renderer>
118where
119 Message: Clone + 'a,
120 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
121{
122 pub fn new(label: impl text::IntoFragment<'a>, value: &str) -> Self {
123 Self::with_mode(label, value, LabelMode::Floating)
124 }
125
126 fn placeholder(label: impl text::IntoFragment<'a>, value: &str) -> Self {
127 Self::with_mode(label, value, LabelMode::Placeholder)
128 }
129
130 fn with_mode(label: impl text::IntoFragment<'a>, value: &str, label_mode: LabelMode) -> Self {
131 let input = IcedTextInput::new("", value)
132 .width(Length::Fill)
133 .padding(Padding {
134 top: tokens::component::text_field::TOP_SPACE,
135 right: tokens::component::text_field::TRAILING_SPACE,
136 bottom: tokens::component::text_field::BOTTOM_SPACE,
137 left: tokens::component::text_field::LEADING_SPACE,
138 })
139 .size(tokens::component::text_field::INPUT_TEXT_SIZE)
140 .line_height(absolute_line_height(
141 tokens::component::text_field::INPUT_TEXT_LINE_HEIGHT,
142 ))
143 .style(|theme, status| input_layer_style_alpha(theme, status, 1.0));
144
145 Self {
146 label: label.into_fragment(),
147 value: value.to_owned(),
148 is_populated: !value.is_empty(),
149 is_enabled: false,
150 is_secure: false,
151 is_error: false,
152 content_alpha: 1.0,
153 width: Length::Fill,
154 font: None,
155 label_mode,
156 input,
157 }
158 }
159
160 pub fn id(mut self, id: impl Into<core_widget::Id>) -> Self {
161 self.input = self.input.id(id);
162 self
163 }
164
165 pub fn secure(mut self, is_secure: bool) -> Self {
166 self.is_secure = is_secure;
167 self.input = self.input.secure(is_secure);
168 self
169 }
170
171 pub fn on_input(mut self, on_input: impl Fn(String) -> Message + 'a) -> Self {
172 self.is_enabled = true;
173 self.input = self.input.on_input(on_input);
174 self
175 }
176
177 pub fn on_input_maybe(mut self, on_input: Option<impl Fn(String) -> Message + 'a>) -> Self {
178 self.is_enabled = on_input.is_some();
179 self.input = self.input.on_input_maybe(on_input);
180 self
181 }
182
183 pub fn on_submit(mut self, message: Message) -> Self {
184 self.input = self.input.on_submit(message);
185 self
186 }
187
188 pub fn on_paste(mut self, on_paste: impl Fn(String) -> Message + 'a) -> Self {
189 self.input = self.input.on_paste(on_paste);
190 self
191 }
192
193 pub fn error(mut self, is_error: bool) -> Self {
194 self.is_error = is_error;
195 self
196 }
197
198 pub fn alpha(mut self, content_alpha: f32) -> Self {
199 let content_alpha = content_alpha.clamp(0.0, 1.0);
200
201 self.content_alpha = content_alpha;
202 self.input = self
203 .input
204 .style(move |theme, status| input_layer_style_alpha(theme, status, content_alpha));
205
206 self
207 }
208
209 pub fn width(mut self, width: impl Into<Length>) -> Self {
210 self.width = width.into();
211 self.input = self.input.width(self.width);
212 self
213 }
214
215 pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
216 let font = font.into();
217 self.font = Some(font);
218 self.input = self.input.font(font);
219 self
220 }
221}
222
223impl<Message, Renderer> Widget<Message, Theme, Renderer> for TextInput<'_, Message, Renderer>
224where
225 Message: Clone,
226 Renderer: iced_widget::core::Renderer + core_text::Renderer,
227{
228 fn tag(&self) -> tree::Tag {
229 tree::Tag::of::<TextFieldState<Renderer::Paragraph>>()
230 }
231
232 fn state(&self) -> tree::State {
233 tree::State::new(TextFieldState::<Renderer::Paragraph>::new(
234 self.label_mode == LabelMode::Floating && self.is_populated,
235 ))
236 }
237
238 fn children(&self) -> Vec<Tree> {
239 let input: &dyn Widget<Message, Theme, Renderer> = &self.input;
240
241 vec![Tree::new(input)]
242 }
243
244 fn diff(&self, tree: &mut Tree) {
245 let state = tree
246 .state
247 .downcast_mut::<TextFieldState<Renderer::Paragraph>>();
248
249 let target = if self.label_mode == LabelMode::Floating {
250 bool_value(self.is_populated || state.is_focused)
251 } else {
252 0.0
253 };
254
255 state.label_float.set_target(
256 target,
257 Instant::now(),
258 duration_ms(tokens::component::text_field::LABEL_TRANSITION_DURATION_MS),
259 tokens::component::text_field::LABEL_TRANSITION_EASING,
260 );
261
262 if tree.children.is_empty() {
263 tree.children = self.children();
264 } else {
265 self.input.diff(&mut tree.children[0]);
266 tree.children.truncate(1);
267 }
268 }
269
270 fn size(&self) -> Size<Length> {
271 Size {
272 width: self.width,
273 height: Length::Fixed(tokens::component::text_field::CONTAINER_HEIGHT),
274 }
275 }
276
277 fn layout(
278 &mut self,
279 tree: &mut Tree,
280 renderer: &Renderer,
281 limits: &layout::Limits,
282 ) -> layout::Node {
283 let state = tree
284 .state
285 .downcast_mut::<TextFieldState<Renderer::Paragraph>>();
286
287 let label_size = tokens::component::text_field::LABEL_TEXT_SIZE;
288 let label_line_height = tokens::component::text_field::LABEL_TEXT_LINE_HEIGHT;
289 let floating_label_size = tokens::component::text_field::LABEL_TEXT_POPULATED_SIZE;
290 let floating_label_line_height =
291 tokens::component::text_field::LABEL_TEXT_POPULATED_LINE_HEIGHT;
292
293 let label_node = core_widget::text::layout(
294 &mut state.label,
295 renderer,
296 &layout::Limits::NONE,
297 self.label.as_ref(),
298 core_widget::text::Format {
299 width: Length::Shrink,
300 height: Length::Shrink,
301 line_height: absolute_line_height(label_line_height),
302 size: Some(Pixels(label_size)),
303 font: self.font,
304 align_x: text::Alignment::Default,
305 align_y: alignment::Vertical::Top,
306 shaping: text::Shaping::Advanced,
307 wrapping: text::Wrapping::None,
308 },
309 );
310
311 let floating_label_node = core_widget::text::layout(
312 &mut state.floating_label,
313 renderer,
314 &layout::Limits::NONE,
315 self.label.as_ref(),
316 core_widget::text::Format {
317 width: Length::Shrink,
318 height: Length::Shrink,
319 line_height: absolute_line_height(floating_label_line_height),
320 size: Some(Pixels(floating_label_size)),
321 font: self.font,
322 align_x: text::Alignment::Default,
323 align_y: alignment::Vertical::Top,
324 shaping: text::Shaping::Advanced,
325 wrapping: text::Wrapping::None,
326 },
327 );
328
329 let intrinsic = Size::new(
330 label_node
331 .size()
332 .width
333 .max(floating_label_node.size().width)
334 + tokens::component::text_field::LEADING_SPACE
335 + tokens::component::text_field::TRAILING_SPACE,
336 tokens::component::text_field::CONTAINER_HEIGHT,
337 );
338 let size = limits.resolve(
339 self.width,
340 Length::Fixed(tokens::component::text_field::CONTAINER_HEIGHT),
341 intrinsic,
342 );
343 let child_limits = layout::Limits::new(size, size);
344 let input = <IcedTextInput<'_, Message, Theme, Renderer> as Widget<
345 Message,
346 Theme,
347 Renderer,
348 >>::layout(
349 &mut self.input,
350 &mut tree.children[0],
351 renderer,
352 &child_limits,
353 );
354
355 layout::Node::with_children(size, vec![input.move_to(Point::ORIGIN)])
356 }
357
358 fn operate(
359 &mut self,
360 tree: &mut Tree,
361 layout: Layout<'_>,
362 renderer: &Renderer,
363 operation: &mut dyn core_widget::Operation,
364 ) {
365 operation.text(None, layout.bounds(), self.label.as_ref());
366 operation.traverse(&mut |operation| {
367 self.input.operate(
368 &mut tree.children[0],
369 layout.children().next().unwrap(),
370 renderer,
371 operation,
372 );
373 });
374 }
375
376 fn update(
377 &mut self,
378 tree: &mut Tree,
379 event: &Event,
380 layout: Layout<'_>,
381 cursor: mouse::Cursor,
382 renderer: &Renderer,
383 clipboard: &mut dyn Clipboard,
384 shell: &mut Shell<'_, Message>,
385 viewport: &Rectangle,
386 ) {
387 let bounds = layout.bounds();
388 let visible_bounds = bounds.intersection(viewport);
389 let input_layout = layout.children().next().unwrap();
390
391 let was_focused = {
392 let state = tree
393 .state
394 .downcast_ref::<TextFieldState<Renderer::Paragraph>>();
395
396 state.is_focused
397 };
398
399 let activation = {
400 let state = tree
401 .state
402 .downcast_mut::<TextFieldState<Renderer::Paragraph>>();
403 let activation = text_input_activation(
404 self.is_enabled,
405 &mut state.touch_activation,
406 event,
407 visible_bounds,
408 cursor,
409 );
410
411 match event {
412 Event::Keyboard(iced_widget::core::keyboard::Event::KeyPressed { key, .. })
413 if matches!(
414 key.as_ref(),
415 iced_widget::core::keyboard::Key::Named(
416 iced_widget::core::keyboard::key::Named::Escape
417 )
418 ) =>
419 {
420 state.is_focused = false;
421 if state.clear_ime_preedit() {
422 shell.request_redraw();
423 }
424 }
425 Event::InputMethod(input_method::Event::Preedit(content, _)) => {
426 if state.set_ime_preedit(content) {
427 shell.request_redraw();
428 }
429 }
430 Event::InputMethod(
431 input_method::Event::Opened
432 | input_method::Event::Closed
433 | input_method::Event::Commit(_),
434 ) => {
435 if state.clear_ime_preedit() {
436 shell.request_redraw();
437 }
438 }
439 Event::Window(window::Event::RedrawRequested(now)) => {
440 if state.label_float.advance(*now) {
441 shell.request_redraw();
442 }
443 }
444 _ => {}
445 }
446
447 activation
448 };
449
450 update_mobile_text_input(
451 &mut self.input,
452 &mut tree.children[0],
453 event,
454 input_layout,
455 activation,
456 renderer,
457 clipboard,
458 shell,
459 viewport,
460 );
461
462 normalize_windows_ime_request(shell.input_method_mut(), bounds);
463
464 let is_focused = {
465 let input_state = tree.children[0]
466 .state
467 .downcast_ref::<iced_text_input::State<Renderer::Paragraph>>();
468
469 input_state.is_focused()
470 };
471
472 {
473 let state = tree
474 .state
475 .downcast_mut::<TextFieldState<Renderer::Paragraph>>();
476 state.is_focused = is_focused;
477
478 if was_focused != is_focused {
479 sync_mobile_keyboard(was_focused, is_focused, activation.request_mobile_keyboard);
480
481 shell.request_redraw();
482 }
483
484 let target = if self.label_mode == LabelMode::Floating {
485 bool_value(self.is_populated || state.is_focused)
486 } else {
487 0.0
488 };
489 state.label_float.set_target(
490 target,
491 Instant::now(),
492 duration_ms(tokens::component::text_field::LABEL_TRANSITION_DURATION_MS),
493 tokens::component::text_field::LABEL_TRANSITION_EASING,
494 );
495
496 if state.is_animating() {
497 shell.request_redraw();
498 }
499 }
500
501 if was_focused == is_focused {
502 sync_mobile_keyboard(was_focused, is_focused, activation.request_mobile_keyboard);
503 }
504 }
505
506 fn mouse_interaction(
507 &self,
508 tree: &Tree,
509 layout: Layout<'_>,
510 cursor: mouse::Cursor,
511 viewport: &Rectangle,
512 renderer: &Renderer,
513 ) -> mouse::Interaction {
514 self.input.mouse_interaction(
515 &tree.children[0],
516 layout.children().next().unwrap(),
517 cursor,
518 viewport,
519 renderer,
520 )
521 }
522
523 fn draw(
524 &self,
525 tree: &Tree,
526 renderer: &mut Renderer,
527 theme: &Theme,
528 defaults: &renderer::Style,
529 layout: Layout<'_>,
530 cursor: mouse::Cursor,
531 viewport: &Rectangle,
532 ) {
533 let state = tree
534 .state
535 .downcast_ref::<TextFieldState<Renderer::Paragraph>>();
536 let bounds = layout.bounds();
537 register_mobile_text_region(self.is_enabled, bounds, viewport);
538
539 let progress = if self.label_mode == LabelMode::Floating {
540 state.label_float.value.clamp(0.0, 1.0)
541 } else {
542 0.0
543 };
544 let is_hovered = cursor.is_over(bounds);
545 let (outline_color, outline_width, label_color) = status_style(
546 theme,
547 self.is_enabled,
548 self.is_error,
549 state.is_focused,
550 is_hovered,
551 );
552 let outline_color = alpha_color(outline_color, self.content_alpha);
553 let label_color = alpha_color(label_color, self.content_alpha);
554 let label_width = state.label.raw().min_bounds().width;
555 let floating_label_width = state.floating_label.raw().min_bounds().width;
556 let label_line_height = tokens::component::text_field::LABEL_TEXT_LINE_HEIGHT;
557 let floating_label_line_height =
558 tokens::component::text_field::LABEL_TEXT_POPULATED_LINE_HEIGHT;
559 let floating_label_y = -floating_label_line_height / 2.0;
560 let label_y = bounds.y
561 + lerp(
562 tokens::component::text_field::TOP_SPACE,
563 floating_label_y,
564 progress,
565 );
566 let label_x = bounds.x + tokens::component::text_field::LEADING_SPACE;
567 let label_notch = text_field_floating_label_notch(
568 bounds,
569 label_x,
570 label_width,
571 floating_label_width,
572 progress,
573 );
574
575 draw_text_field_outline(
576 renderer,
577 bounds,
578 Background::Color(Color::TRANSPARENT),
579 Border {
580 color: outline_color,
581 width: outline_width,
582 radius: tokens::component::text_field::CONTAINER_SHAPE.into(),
583 },
584 label_notch,
585 );
586
587 let input_layout = layout.children().next().unwrap();
588 let caretless_input;
589 let input = if state.ime_preedit_active && self.is_enabled && should_suppress_ime_caret() {
590 let content_alpha = self.content_alpha;
591 let mut input = IcedTextInput::new("", self.value.as_str())
594 .width(Length::Fill)
595 .padding(Padding {
596 top: tokens::component::text_field::TOP_SPACE,
597 right: tokens::component::text_field::TRAILING_SPACE,
598 bottom: tokens::component::text_field::BOTTOM_SPACE,
599 left: tokens::component::text_field::LEADING_SPACE,
600 })
601 .size(tokens::component::text_field::INPUT_TEXT_SIZE)
602 .line_height(absolute_line_height(
603 tokens::component::text_field::INPUT_TEXT_LINE_HEIGHT,
604 ))
605 .style(move |theme, status| input_layer_style_alpha(theme, status, content_alpha));
606
607 if self.is_secure {
608 input = input.secure(true);
609 }
610
611 if let Some(font) = self.font {
612 input = input.font(font);
613 }
614
615 caretless_input = input;
616 &caretless_input
617 } else {
618 &self.input
619 };
620
621 <IcedTextInput<'_, Message, Theme, Renderer> as Widget<Message, Theme, Renderer>>::draw(
622 input,
623 &tree.children[0],
624 renderer,
625 theme,
626 defaults,
627 input_layout,
628 cursor,
629 viewport,
630 );
631
632 if self.label_mode == LabelMode::Placeholder && self.is_populated {
633 return;
634 }
635
636 if progress < 0.99 {
637 core_widget::text::draw(
638 renderer,
639 defaults,
640 Rectangle {
641 x: label_x,
642 y: label_y,
643 width: label_width,
644 height: label_line_height,
645 },
646 state.label.raw(),
647 core_widget::text::Style {
648 color: Some(alpha_color(label_color, 1.0 - progress)),
649 },
650 viewport,
651 );
652 }
653
654 if progress > 0.01 {
655 core_widget::text::draw(
656 renderer,
657 defaults,
658 Rectangle {
659 x: label_x,
660 y: label_y,
661 width: floating_label_width,
662 height: floating_label_line_height,
663 },
664 state.floating_label.raw(),
665 core_widget::text::Style {
666 color: Some(alpha_color(label_color, progress)),
667 },
668 viewport,
669 );
670 }
671 }
672}
673
674impl<'a, Message, Renderer> From<TextInput<'a, Message, Renderer>>
675 for Element<'a, Message, Theme, Renderer>
676where
677 Message: Clone + 'a,
678 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
679{
680 fn from(text_input: TextInput<'a, Message, Renderer>) -> Self {
681 Element::new(text_input)
682 }
683}
684
685pub fn outlined<'a, Message, Renderer>(
686 label: impl text::IntoFragment<'a>,
687 value: &str,
688) -> TextInput<'a, Message, Renderer>
689where
690 Message: Clone + 'a,
691 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
692{
693 TextInput::new(label, value)
694}
695
696pub fn outlined_floating<'a, Message, Renderer>(
697 label: impl text::IntoFragment<'a>,
698 value: &str,
699) -> TextInput<'a, Message, Renderer>
700where
701 Message: Clone + 'a,
702 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
703{
704 TextInput::new(label, value)
705}
706
707pub fn outlined_placeholder<'a, Message, Renderer>(
708 label: impl text::IntoFragment<'a>,
709 value: &str,
710) -> TextInput<'a, Message, Renderer>
711where
712 Message: Clone + 'a,
713 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
714{
715 TextInput::placeholder(label, value)
716}
717
718#[cfg(test)]
719#[path = "../../../tests/widget/component/text_input.rs"]
720mod tests;