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 &mut state.web_input_position,
407 event,
408 visible_bounds,
409 cursor,
410 );
411
412 match event {
413 Event::Keyboard(iced_widget::core::keyboard::Event::KeyPressed { key, .. })
414 if matches!(
415 key.as_ref(),
416 iced_widget::core::keyboard::Key::Named(
417 iced_widget::core::keyboard::key::Named::Escape
418 )
419 ) =>
420 {
421 state.is_focused = false;
422 if state.clear_ime_preedit() {
423 shell.request_redraw();
424 }
425 }
426 Event::InputMethod(input_method::Event::Preedit(content, _))
427 if state.set_ime_preedit(content) =>
428 {
429 shell.request_redraw();
430 }
431 Event::InputMethod(
432 input_method::Event::Opened
433 | input_method::Event::Closed
434 | input_method::Event::Commit(_),
435 ) if state.clear_ime_preedit() => {
436 shell.request_redraw();
437 }
438 Event::Window(window::Event::RedrawRequested(now))
439 if state.label_float.advance(*now) =>
440 {
441 shell.request_redraw();
442 }
443 _ => {}
444 }
445
446 activation
447 };
448
449 update_mobile_text_input(
450 &mut self.input,
451 &mut tree.children[0],
452 event,
453 input_layout,
454 activation,
455 TextInputUpdateContext {
456 renderer,
457 clipboard,
458 shell,
459 viewport,
460 },
461 );
462
463 normalize_windows_ime_request(shell.input_method_mut(), bounds);
464
465 let is_focused = {
466 let input_state = tree.children[0]
467 .state
468 .downcast_ref::<iced_text_input::State<Renderer::Paragraph>>();
469
470 input_state.is_focused()
471 };
472 let input_anchor = web_input_anchor(
473 shell.input_method(),
474 visible_bounds,
475 activation,
476 was_focused,
477 is_focused,
478 );
479
480 {
481 let state = tree
482 .state
483 .downcast_mut::<TextFieldState<Renderer::Paragraph>>();
484 state.is_focused = is_focused;
485
486 if was_focused != is_focused {
487 shell.request_redraw();
488 }
489
490 let target = if self.label_mode == LabelMode::Floating {
491 bool_value(self.is_populated || state.is_focused)
492 } else {
493 0.0
494 };
495 state.label_float.set_target(
496 target,
497 Instant::now(),
498 duration_ms(tokens::component::text_field::LABEL_TRANSITION_DURATION_MS),
499 tokens::component::text_field::LABEL_TRANSITION_EASING,
500 );
501
502 if state.is_animating() {
503 shell.request_redraw();
504 }
505 }
506
507 sync_mobile_keyboard(
508 was_focused,
509 is_focused,
510 activation.request_mobile_keyboard,
511 input_anchor,
512 );
513 }
514
515 fn mouse_interaction(
516 &self,
517 tree: &Tree,
518 layout: Layout<'_>,
519 cursor: mouse::Cursor,
520 viewport: &Rectangle,
521 renderer: &Renderer,
522 ) -> mouse::Interaction {
523 self.input.mouse_interaction(
524 &tree.children[0],
525 layout.children().next().unwrap(),
526 cursor,
527 viewport,
528 renderer,
529 )
530 }
531
532 fn draw(
533 &self,
534 tree: &Tree,
535 renderer: &mut Renderer,
536 theme: &Theme,
537 defaults: &renderer::Style,
538 layout: Layout<'_>,
539 cursor: mouse::Cursor,
540 viewport: &Rectangle,
541 ) {
542 let state = tree
543 .state
544 .downcast_ref::<TextFieldState<Renderer::Paragraph>>();
545 let bounds = layout.bounds();
546 register_mobile_text_region(self.is_enabled, bounds, viewport);
547
548 let progress = if self.label_mode == LabelMode::Floating {
549 state.label_float.value.clamp(0.0, 1.0)
550 } else {
551 0.0
552 };
553 let is_hovered = cursor.is_over(bounds);
554 let (outline_color, outline_width, label_color) = status_style(
555 theme,
556 self.is_enabled,
557 self.is_error,
558 state.is_focused,
559 is_hovered,
560 );
561 let outline_color = alpha_color(outline_color, self.content_alpha);
562 let label_color = alpha_color(label_color, self.content_alpha);
563 let label_width = state.label.raw().min_bounds().width;
564 let floating_label_width = state.floating_label.raw().min_bounds().width;
565 let label_line_height = tokens::component::text_field::LABEL_TEXT_LINE_HEIGHT;
566 let floating_label_line_height =
567 tokens::component::text_field::LABEL_TEXT_POPULATED_LINE_HEIGHT;
568 let floating_label_y = -floating_label_line_height / 2.0;
569 let label_y = bounds.y
570 + lerp(
571 tokens::component::text_field::TOP_SPACE,
572 floating_label_y,
573 progress,
574 );
575 let label_x = bounds.x + tokens::component::text_field::LEADING_SPACE;
576 let label_notch = text_field_floating_label_notch(
577 bounds,
578 label_x,
579 label_width,
580 floating_label_width,
581 progress,
582 );
583
584 draw_text_field_outline(
585 renderer,
586 bounds,
587 Background::Color(Color::TRANSPARENT),
588 Border {
589 color: outline_color,
590 width: outline_width,
591 radius: tokens::component::text_field::CONTAINER_SHAPE.into(),
592 },
593 label_notch,
594 );
595
596 let input_layout = layout.children().next().unwrap();
597 let caretless_input;
598 let input = if state.ime_preedit_active && self.is_enabled && should_suppress_ime_caret() {
599 let content_alpha = self.content_alpha;
600 let mut input = IcedTextInput::new("", self.value.as_str())
603 .width(Length::Fill)
604 .padding(Padding {
605 top: tokens::component::text_field::TOP_SPACE,
606 right: tokens::component::text_field::TRAILING_SPACE,
607 bottom: tokens::component::text_field::BOTTOM_SPACE,
608 left: tokens::component::text_field::LEADING_SPACE,
609 })
610 .size(tokens::component::text_field::INPUT_TEXT_SIZE)
611 .line_height(absolute_line_height(
612 tokens::component::text_field::INPUT_TEXT_LINE_HEIGHT,
613 ))
614 .style(move |theme, status| input_layer_style_alpha(theme, status, content_alpha));
615
616 if self.is_secure {
617 input = input.secure(true);
618 }
619
620 if let Some(font) = self.font {
621 input = input.font(font);
622 }
623
624 caretless_input = input;
625 &caretless_input
626 } else {
627 &self.input
628 };
629
630 <IcedTextInput<'_, Message, Theme, Renderer> as Widget<Message, Theme, Renderer>>::draw(
631 input,
632 &tree.children[0],
633 renderer,
634 theme,
635 defaults,
636 input_layout,
637 cursor,
638 viewport,
639 );
640
641 if self.label_mode == LabelMode::Placeholder && self.is_populated {
642 return;
643 }
644
645 if progress < 0.99 {
646 core_widget::text::draw(
647 renderer,
648 defaults,
649 Rectangle {
650 x: label_x,
651 y: label_y,
652 width: label_width,
653 height: label_line_height,
654 },
655 state.label.raw(),
656 core_widget::text::Style {
657 color: Some(alpha_color(label_color, 1.0 - progress)),
658 },
659 viewport,
660 );
661 }
662
663 if progress > 0.01 {
664 core_widget::text::draw(
665 renderer,
666 defaults,
667 Rectangle {
668 x: label_x,
669 y: label_y,
670 width: floating_label_width,
671 height: floating_label_line_height,
672 },
673 state.floating_label.raw(),
674 core_widget::text::Style {
675 color: Some(alpha_color(label_color, progress)),
676 },
677 viewport,
678 );
679 }
680 }
681}
682
683impl<'a, Message, Renderer> From<TextInput<'a, Message, Renderer>>
684 for Element<'a, Message, Theme, Renderer>
685where
686 Message: Clone + 'a,
687 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
688{
689 fn from(text_input: TextInput<'a, Message, Renderer>) -> Self {
690 Element::new(text_input)
691 }
692}
693
694pub fn outlined<'a, Message, Renderer>(
695 label: impl text::IntoFragment<'a>,
696 value: &str,
697) -> TextInput<'a, Message, Renderer>
698where
699 Message: Clone + 'a,
700 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
701{
702 TextInput::new(label, value)
703}
704
705pub fn outlined_floating<'a, Message, Renderer>(
706 label: impl text::IntoFragment<'a>,
707 value: &str,
708) -> TextInput<'a, Message, Renderer>
709where
710 Message: Clone + 'a,
711 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
712{
713 TextInput::new(label, value)
714}
715
716pub fn outlined_placeholder<'a, Message, Renderer>(
717 label: impl text::IntoFragment<'a>,
718 value: &str,
719) -> TextInput<'a, Message, Renderer>
720where
721 Message: Clone + 'a,
722 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
723{
724 TextInput::placeholder(label, value)
725}
726
727#[cfg(test)]
728#[path = "../../../tests/widget/component/text_input.rs"]
729mod tests;