Skip to main content

freya_components/
input.rs

1use std::{
2    borrow::Cow,
3    cell::{
4        Ref,
5        RefCell,
6    },
7    rc::Rc,
8};
9
10use freya_core::{
11    elements::paragraph::{
12        ParagraphHolderInner,
13        cursor_character_rect,
14    },
15    prelude::*,
16};
17use freya_edit::*;
18use torin::{
19    gaps::Gaps,
20    prelude::{
21        Alignment,
22        Area,
23        Content,
24        Direction,
25    },
26    size::Size,
27};
28use tracing::warn;
29
30use crate::{
31    cursor_blink::use_cursor_blink,
32    define_theme,
33    get_theme,
34    scrollviews::{
35        ScrollConfig,
36        ScrollView,
37        use_scroll_controller,
38    },
39};
40
41define_theme! {
42    for = Input;
43    theme_field = theme_layout;
44
45    %[component]
46    pub InputLayout {
47        %[fields]
48        corner_radius: CornerRadius,
49        inner_margin: Gaps,
50    }
51}
52
53define_theme! {
54    for = Input;
55    theme_field = theme_colors;
56
57    %[component]
58    pub InputColors {
59        %[fields]
60        background: Color,
61        focus_background: Color,
62        border_fill: Color,
63        focus_border_fill: Color,
64        color: Color,
65        placeholder_color: Color,
66    }
67}
68
69#[derive(Clone, PartialEq)]
70pub enum InputStyleVariant {
71    Normal,
72    Filled,
73    Flat,
74}
75
76#[derive(Clone, PartialEq)]
77pub enum InputLayoutVariant {
78    Normal,
79    Compact,
80    Expanded,
81}
82
83#[derive(Default, Clone, Copy, PartialEq)]
84pub enum InputMode {
85    #[default]
86    Shown,
87    Hidden(char),
88}
89
90impl InputMode {
91    pub fn new_password() -> Self {
92        Self::Hidden('*')
93    }
94}
95
96#[derive(Debug, Default, PartialEq, Clone, Copy)]
97pub enum InputStatus {
98    /// Default state.
99    #[default]
100    Idle,
101    /// Pointer is hovering the input.
102    Hovering,
103}
104
105#[derive(Clone)]
106pub struct InputValidator {
107    valid: Rc<RefCell<bool>>,
108    text: Rc<RefCell<String>>,
109}
110
111impl InputValidator {
112    pub fn new(text: String) -> Self {
113        Self {
114            valid: Rc::new(RefCell::new(true)),
115            text: Rc::new(RefCell::new(text)),
116        }
117    }
118    pub fn text(&'_ self) -> Ref<'_, String> {
119        self.text.borrow()
120    }
121    pub fn set_valid(&self, is_valid: bool) {
122        *self.valid.borrow_mut() = is_valid;
123    }
124    pub fn is_valid(&self) -> bool {
125        *self.valid.borrow()
126    }
127}
128
129/// Small box to write some text.
130///
131/// ## **Normal**
132///
133/// ```rust
134/// # use freya::prelude::*;
135/// fn app() -> impl IntoElement {
136///     let value = use_state(String::new);
137///     Input::new(value).placeholder("Type here")
138/// }
139/// # use freya_testing::prelude::*;
140/// # launch_doc(|| {
141/// #   rect().center().expanded().child(app())
142/// # }, "./images/gallery_input.png").render();
143/// ```
144/// ## **Filled**
145///
146/// ```rust
147/// # use freya::prelude::*;
148/// fn app() -> impl IntoElement {
149///     let value = use_state(String::new);
150///     Input::new(value).placeholder("Type here").filled()
151/// }
152/// # use freya_testing::prelude::*;
153/// # launch_doc(|| {
154/// #   rect().center().expanded().child(app())
155/// # }, "./images/gallery_filled_input.png").render();
156/// ```
157/// ## **Flat**
158///
159/// ```rust
160/// # use freya::prelude::*;
161/// fn app() -> impl IntoElement {
162///     let value = use_state(String::new);
163///     Input::new(value).placeholder("Type here").flat()
164/// }
165/// # use freya_testing::prelude::*;
166/// # launch_doc(|| {
167/// #   rect().center().expanded().child(app())
168/// # }, "./images/gallery_flat_input.png").render();
169/// ```
170///
171/// # Preview
172/// ![Input Preview][input]
173/// ![Filled Input Preview][filled_input]
174/// ![Flat Input Preview][flat_input]
175#[cfg_attr(feature = "docs",
176    doc = embed_doc_image::embed_image!("input", "images/gallery_input.png"),
177    doc = embed_doc_image::embed_image!("filled_input", "images/gallery_filled_input.png"),
178    doc = embed_doc_image::embed_image!("flat_input", "images/gallery_flat_input.png"),
179)]
180#[derive(Clone, PartialEq)]
181pub struct Input {
182    pub(crate) theme_colors: Option<InputColorsThemePartial>,
183    pub(crate) theme_layout: Option<InputLayoutThemePartial>,
184    value: Writable<String>,
185    placeholder: Option<Cow<'static, str>>,
186    on_validate: Option<EventHandler<InputValidator>>,
187    on_submit: Option<EventHandler<String>>,
188    mode: InputMode,
189    auto_focus: bool,
190    width: Size,
191    enabled: bool,
192    key: DiffKey,
193    style_variant: InputStyleVariant,
194    layout_variant: InputLayoutVariant,
195    text_align: TextAlign,
196    a11y_id: Option<AccessibilityId>,
197    leading: Option<Element>,
198    trailing: Option<Element>,
199    on_pre_key_down: Callback<Event<KeyboardEventData>, bool>,
200}
201
202impl KeyExt for Input {
203    fn write_key(&mut self) -> &mut DiffKey {
204        &mut self.key
205    }
206}
207
208impl Input {
209    pub fn new(value: impl Into<Writable<String>>) -> Self {
210        Input {
211            theme_colors: None,
212            theme_layout: None,
213            value: value.into(),
214            placeholder: None,
215            on_validate: None,
216            on_submit: None,
217            mode: InputMode::default(),
218            auto_focus: false,
219            width: Size::px(150.),
220            enabled: true,
221            key: DiffKey::default(),
222            style_variant: InputStyleVariant::Normal,
223            layout_variant: InputLayoutVariant::Normal,
224            text_align: TextAlign::default(),
225            a11y_id: None,
226            leading: None,
227            trailing: None,
228            on_pre_key_down: Callback::new(|e: Event<KeyboardEventData>| match &e.key {
229                Key::Named(NamedKey::Enter)
230                | Key::Named(NamedKey::Escape)
231                | Key::Named(NamedKey::Shift) => true,
232                Key::Named(NamedKey::Tab) => false,
233                _ => {
234                    e.stop_propagation();
235                    e.prevent_default();
236                    true
237                }
238            }),
239        }
240    }
241
242    pub fn enabled(mut self, enabled: impl Into<bool>) -> Self {
243        self.enabled = enabled.into();
244        self
245    }
246
247    pub fn placeholder(mut self, placeholder: impl Into<Cow<'static, str>>) -> Self {
248        self.placeholder = Some(placeholder.into());
249        self
250    }
251
252    pub fn on_validate(mut self, on_validate: impl Into<EventHandler<InputValidator>>) -> Self {
253        self.on_validate = Some(on_validate.into());
254        self
255    }
256
257    pub fn on_submit(mut self, on_submit: impl Into<EventHandler<String>>) -> Self {
258        self.on_submit = Some(on_submit.into());
259        self
260    }
261
262    pub fn mode(mut self, mode: InputMode) -> Self {
263        self.mode = mode;
264        self
265    }
266
267    pub fn auto_focus(mut self, auto_focus: impl Into<bool>) -> Self {
268        self.auto_focus = auto_focus.into();
269        self
270    }
271
272    pub fn width(mut self, width: impl Into<Size>) -> Self {
273        self.width = width.into();
274        self
275    }
276
277    pub fn theme_colors(mut self, theme: InputColorsThemePartial) -> Self {
278        self.theme_colors = Some(theme);
279        self
280    }
281
282    pub fn theme_layout(mut self, theme: InputLayoutThemePartial) -> Self {
283        self.theme_layout = Some(theme);
284        self
285    }
286
287    pub fn text_align(mut self, text_align: impl Into<TextAlign>) -> Self {
288        self.text_align = text_align.into();
289        self
290    }
291
292    pub fn style_variant(mut self, style_variant: impl Into<InputStyleVariant>) -> Self {
293        self.style_variant = style_variant.into();
294        self
295    }
296
297    pub fn layout_variant(mut self, layout_variant: impl Into<InputLayoutVariant>) -> Self {
298        self.layout_variant = layout_variant.into();
299        self
300    }
301
302    /// Shortcut for [Self::style_variant] with [InputStyleVariant::Filled].
303    pub fn filled(self) -> Self {
304        self.style_variant(InputStyleVariant::Filled)
305    }
306
307    /// Shortcut for [Self::style_variant] with [InputStyleVariant::Flat].
308    pub fn flat(self) -> Self {
309        self.style_variant(InputStyleVariant::Flat)
310    }
311
312    /// Shortcut for [Self::layout_variant] with [InputLayoutVariant::Compact].
313    pub fn compact(self) -> Self {
314        self.layout_variant(InputLayoutVariant::Compact)
315    }
316
317    /// Shortcut for [Self::layout_variant] with [InputLayoutVariant::Expanded].
318    pub fn expanded(self) -> Self {
319        self.layout_variant(InputLayoutVariant::Expanded)
320    }
321
322    pub fn a11y_id(mut self, a11y_id: impl Into<AccessibilityId>) -> Self {
323        self.a11y_id = Some(a11y_id.into());
324        self
325    }
326
327    /// Optional element rendered before the text input.
328    pub fn leading(mut self, leading: impl Into<Element>) -> Self {
329        self.leading = Some(leading.into());
330        self
331    }
332
333    /// Optional element rendered after the text input.
334    pub fn trailing(mut self, trailing: impl Into<Element>) -> Self {
335        self.trailing = Some(trailing.into());
336        self
337    }
338
339    /// Sets a pre-handler called for each key event. Return `true` to let the input process it,
340    /// `false` to skip. The callback may call `stop_propagation()` / `prevent_default()` directly.
341    pub fn on_pre_key_down(
342        mut self,
343        on_pre_key_down: impl Into<Callback<Event<KeyboardEventData>, bool>>,
344    ) -> Self {
345        self.on_pre_key_down = on_pre_key_down.into();
346        self
347    }
348}
349
350impl CornerRadiusExt for Input {
351    fn with_corner_radius(self, corner_radius: f32) -> Self {
352        self.corner_radius(corner_radius)
353    }
354}
355
356impl Component for Input {
357    fn render(&self) -> impl IntoElement {
358        let a11y_id = use_hook(|| self.a11y_id.unwrap_or_else(AccessibilityId::new_unique));
359        let focus = use_focus(a11y_id);
360        let holder = use_state(ParagraphHolder::default);
361        let mut area = use_state(Area::default);
362        let mut viewport_area = use_state(Area::default);
363        let mut scroll_controller = use_scroll_controller(ScrollConfig::default);
364        let mut status = use_state(InputStatus::default);
365        let is_masked = matches!(self.mode, InputMode::Hidden(_));
366        let mut editable = use_editable(
367            || self.value.read().to_string(),
368            move || {
369                EditableConfig::new()
370                    .with_allow_write_clipboard(!is_masked)
371                    .with_select_all_on_double_click(is_masked)
372            },
373        );
374        let mut is_dragging = use_state(|| false);
375        let mut value = self.value.clone();
376
377        let theme_colors = match self.style_variant {
378            InputStyleVariant::Normal => {
379                get_theme!(&self.theme_colors, InputColorsThemePreference, "input")
380            }
381            InputStyleVariant::Filled => get_theme!(
382                &self.theme_colors,
383                InputColorsThemePreference,
384                "filled_input"
385            ),
386            InputStyleVariant::Flat => {
387                get_theme!(&self.theme_colors, InputColorsThemePreference, "flat_input")
388            }
389        };
390        let theme_layout = match self.layout_variant {
391            InputLayoutVariant::Normal => get_theme!(
392                &self.theme_layout,
393                InputLayoutThemePreference,
394                "input_layout"
395            ),
396            InputLayoutVariant::Compact => get_theme!(
397                &self.theme_layout,
398                InputLayoutThemePreference,
399                "compact_input_layout"
400            ),
401            InputLayoutVariant::Expanded => get_theme!(
402                &self.theme_layout,
403                InputLayoutThemePreference,
404                "expanded_input_layout"
405            ),
406        };
407
408        let (mut movement_timeout, cursor_color) =
409            use_cursor_blink(focus() != Focus::Not, theme_colors.color);
410
411        let enabled = use_reactive(&self.enabled);
412        use_drop(move || {
413            if status() == InputStatus::Hovering && enabled() {
414                Cursor::set(CursorIcon::default());
415            }
416        });
417
418        let display_placeholder = value.read().is_empty()
419            && self.placeholder.is_some()
420            && !editable.editor().read().has_preedit();
421        let on_validate = self.on_validate.clone();
422        let on_submit = self.on_submit.clone();
423
424        if *value.read() != editable.editor().read().committed_text() {
425            let mut editor = editable.editor_mut().write();
426            editor.clear_preedit();
427            editor.set(&value.read());
428            editor.editor_history_mut().clear();
429            editor.clear_selection();
430        }
431
432        let mode = self.mode;
433        let text_align = self.text_align;
434        let inner_margin = theme_layout.inner_margin;
435        let mut follow_cursor = move || {
436            if !a11y_id.is_focused() || display_placeholder {
437                return;
438            }
439
440            let holder = holder.peek();
441            let holder = holder.0.borrow();
442            let Some(ParagraphHolderInner {
443                paragraph,
444                scale_factor,
445            }) = holder.as_ref()
446            else {
447                warn!("Paragraph should be build by now.");
448                return;
449            };
450
451            let viewport = viewport_area();
452            if viewport.width() == 0. {
453                return;
454            }
455
456            // Text as currently displayed
457            let editor = editable.editor().peek();
458            let text = match mode {
459                InputMode::Hidden(character) => {
460                    character.to_string().repeat(editor.rope().len_chars())
461                }
462                InputMode::Shown => editor.rope().to_string(),
463            };
464
465            let cursor_rect =
466                cursor_character_rect(paragraph, &text, editor.cursor_pos(), text_align);
467            let cursor_x = cursor_rect.left / (*scale_factor as f32);
468
469            // Visible window start
470            let visible_start = viewport.min_x() - area.peek().min_x();
471
472            // Minimally reveal the cursor
473            if cursor_x < visible_start {
474                scroll_controller.scroll_to_x(-cursor_x as i32);
475            } else if cursor_x + inner_margin.horizontal() > visible_start + viewport.width() {
476                scroll_controller
477                    .scroll_to_x(-(cursor_x + inner_margin.horizontal() - viewport.width()) as i32);
478            }
479        };
480
481        let on_ime_preedit = move |e: Event<ImePreeditEventData>| {
482            let mut editor = editable.editor_mut().write();
483            if e.data().text.is_empty() {
484                editor.clear_preedit();
485            } else {
486                editor.set_preedit(&e.data().text);
487            }
488        };
489
490        let on_pre_key_down = self.on_pre_key_down.clone();
491        let on_key_down = move |e: Event<KeyboardEventData>| {
492            let key = e.key.clone();
493            let modifiers = e.modifiers;
494
495            if !on_pre_key_down.call(e) {
496                return;
497            }
498
499            match &key {
500                // On submit
501                Key::Named(NamedKey::Enter) => {
502                    if let Some(on_submit) = &on_submit {
503                        let text = editable.editor().peek().committed_text();
504                        on_submit.call(text);
505                    }
506                }
507                // On unfocus
508                Key::Named(NamedKey::Escape) => {
509                    a11y_id.request_unfocus();
510                    Cursor::set(CursorIcon::default());
511                }
512                // On change
513                _ => {
514                    movement_timeout.reset();
515                    let previous_history_version =
516                        editable.editor().peek().editor_history().version;
517                    editable.process_event(EditableEvent::KeyDown {
518                        key: &key,
519                        modifiers,
520                    });
521                    let text = editable.editor().read().committed_text();
522
523                    let apply_change = match &on_validate {
524                        Some(on_validate) => {
525                            let mut editor = editable.editor_mut().write();
526                            let validator = InputValidator::new(text.clone());
527                            on_validate.call(validator.clone());
528                            if !validator.is_valid() {
529                                if let Some(selection) = editor.undo() {
530                                    *editor.selection_mut() = selection;
531                                }
532                                editor.editor_history_mut().clear_redos();
533                            }
534                            validator.is_valid()
535                        }
536                        None => true,
537                    };
538
539                    if apply_change {
540                        *value.write() = text;
541                    }
542                    if editable.editor().peek().editor_history().version == previous_history_version
543                    {
544                        follow_cursor();
545                    }
546                }
547            }
548        };
549
550        let on_key_up = move |e: Event<KeyboardEventData>| {
551            e.stop_propagation();
552            editable.process_event(EditableEvent::KeyUp { key: &e.key });
553        };
554
555        let on_input_focus_press = move |e: Event<FocusPressEventData>| {
556            e.stop_propagation();
557            e.prevent_default();
558            if cfg!(target_os = "android") {
559                if a11y_id.is_focused() {
560                    // Require a second press to enabling dragging on Android
561                    is_dragging.set_if_modified(true);
562                }
563            } else {
564                is_dragging.set_if_modified(true);
565            }
566            movement_timeout.reset();
567            if !display_placeholder {
568                let area = area.read().to_f64();
569                let global_location = e.global_location().clamp(area.min(), area.max());
570                let location = (global_location - area.min()).to_point();
571                editable.process_event(EditableEvent::Down {
572                    location,
573                    editor_line: EditorLine::SingleParagraph,
574                    holder: &holder.read(),
575                });
576            }
577            a11y_id.request_focus();
578        };
579
580        let on_focus_press = move |e: Event<FocusPressEventData>| {
581            e.stop_propagation();
582            e.prevent_default();
583            if cfg!(target_os = "android") {
584                if a11y_id.is_focused() {
585                    // Require a second press to enabling dragging on Android
586                    is_dragging.set_if_modified(true);
587                }
588            } else {
589                is_dragging.set_if_modified(true);
590            }
591            movement_timeout.reset();
592            if !display_placeholder {
593                editable.process_event(EditableEvent::Down {
594                    location: e.element_location(),
595                    editor_line: EditorLine::SingleParagraph,
596                    holder: &holder.read(),
597                });
598            }
599            a11y_id.request_focus();
600        };
601
602        let on_global_pointer_move = move |e: Event<PointerEventData>| {
603            if a11y_id.is_focused() && *is_dragging.read() {
604                let mut location = e.global_location();
605                location.x -= area.read().min_x() as f64;
606                location.y -= area.read().min_y() as f64;
607                editable.process_event(EditableEvent::Move {
608                    location,
609                    editor_line: EditorLine::SingleParagraph,
610                    holder: &holder.read(),
611                });
612                follow_cursor();
613            }
614        };
615
616        let on_pointer_enter = move |_| {
617            *status.write() = InputStatus::Hovering;
618            if enabled() {
619                Cursor::set(CursorIcon::Text);
620            } else {
621                Cursor::set(CursorIcon::NotAllowed);
622            }
623        };
624
625        let on_pointer_leave = move |_| {
626            if status() == InputStatus::Hovering {
627                Cursor::set(CursorIcon::default());
628                *status.write() = InputStatus::default();
629            }
630        };
631
632        let on_global_pointer_press = move |_: Event<PointerEventData>| {
633            match *status.read() {
634                InputStatus::Idle if a11y_id.is_focused() => {
635                    editable.process_event(EditableEvent::Release);
636                }
637                InputStatus::Hovering => {
638                    editable.process_event(EditableEvent::Release);
639                }
640                _ => {}
641            };
642
643            if a11y_id.is_focused() {
644                if *is_dragging.read() {
645                    // The input is focused and dragging, but it just clicked so we assume the dragging can stop
646                    is_dragging.set(false);
647                } else {
648                    // The input is focused but not dragging, so the click means it was clicked outside, therefore we can unfocus this input
649                    a11y_id.request_unfocus();
650                }
651            }
652        };
653
654        let on_pointer_press = move |e: Event<PointerEventData>| {
655            e.stop_propagation();
656            e.prevent_default();
657            match *status.read() {
658                InputStatus::Idle if a11y_id.is_focused() => {
659                    editable.process_event(EditableEvent::Release);
660                }
661                InputStatus::Hovering => {
662                    editable.process_event(EditableEvent::Release);
663                }
664                _ => {}
665            };
666
667            if a11y_id.is_focused() {
668                is_dragging.set_if_modified(false);
669            }
670        };
671
672        let on_paragraph_sized = move |e: Event<SizedEventData>| {
673            let text_width_changed = area.peek().width() != e.area.width();
674            area.set_if_modified(e.area);
675            if text_width_changed {
676                follow_cursor();
677            }
678        };
679
680        let (background, cursor_index, text_selection) = if enabled() && focus() != Focus::Not {
681            (
682                theme_colors.focus_background,
683                Some(editable.editor().read().cursor_pos()),
684                editable
685                    .editor()
686                    .read()
687                    .get_visible_selection(EditorLine::SingleParagraph),
688            )
689        } else {
690            (theme_colors.background, None, None)
691        };
692
693        let border = if focus().is_focused() {
694            Border::new()
695                .fill(theme_colors.focus_border_fill)
696                .width(2.)
697                .alignment(BorderAlignment::Inner)
698        } else {
699            Border::new()
700                .fill(theme_colors.border_fill.mul_if(!self.enabled, 0.85))
701                .width(1.)
702                .alignment(BorderAlignment::Inner)
703        };
704
705        let color = if display_placeholder {
706            theme_colors.placeholder_color
707        } else {
708            theme_colors.color
709        };
710
711        let value = self.value.read();
712        let a11y_text: Cow<str> = match (self.mode, &self.placeholder) {
713            (_, Some(ph)) if display_placeholder => Cow::Borrowed(ph.as_ref()),
714            (InputMode::Hidden(ch), _) => Cow::Owned(ch.to_string().repeat(value.len())),
715            (InputMode::Shown, _) => Cow::Borrowed(value.as_ref()),
716        };
717
718        let a11_role = match self.mode {
719            InputMode::Hidden(_) => AccessibilityRole::PasswordInput,
720            _ => AccessibilityRole::TextInput,
721        };
722
723        rect()
724            .a11y_id(a11y_id)
725            .a11y_focusable(self.enabled)
726            .a11y_auto_focus(self.auto_focus)
727            .a11y_alt(a11y_text)
728            .a11y_role(a11_role)
729            .maybe(self.enabled, |el| {
730                el.on_key_up(on_key_up)
731                    .on_key_down(on_key_down)
732                    .on_focus_press(on_input_focus_press)
733                    .on_ime_preedit(on_ime_preedit)
734                    .on_pointer_press(on_pointer_press)
735                    .on_global_pointer_press(on_global_pointer_press)
736                    .on_global_pointer_move(on_global_pointer_move)
737            })
738            .on_pointer_enter(on_pointer_enter)
739            .on_pointer_leave(on_pointer_leave)
740            .width(self.width.clone())
741            .background(background.mul_if(!self.enabled, 0.85))
742            .border(border)
743            .corner_radius(theme_layout.corner_radius)
744            .content(Content::Flex)
745            .direction(Direction::Horizontal)
746            .cross_align(Alignment::center())
747            .maybe_child(
748                self.leading
749                    .clone()
750                    .map(|leading| rect().padding(Gaps::new(0., 0., 0., 8.)).child(leading)),
751            )
752            .child(
753                ScrollView::new_controlled(scroll_controller)
754                    .width(Size::flex(1.))
755                    .height(Size::Inner)
756                    .direction(Direction::Horizontal)
757                    .show_scrollbar(false)
758                    .on_sized(move |e: Event<SizedEventData>| viewport_area.set_if_modified(e.area))
759                    .child(
760                        paragraph()
761                            .holder(holder.read().clone())
762                            .on_sized(on_paragraph_sized)
763                            .min_width(Size::func(move |context| {
764                                Some(context.parent - theme_layout.inner_margin.horizontal())
765                            }))
766                            .maybe(self.enabled, |el| el.on_focus_press(on_focus_press))
767                            .margin(theme_layout.inner_margin)
768                            .cursor_index(cursor_index)
769                            .cursor_color(cursor_color)
770                            .color(color)
771                            .text_align(self.text_align)
772                            .max_lines(1)
773                            .highlights(text_selection.map(|h| vec![h]))
774                            .maybe(display_placeholder, |el| {
775                                el.span(self.placeholder.as_ref().unwrap().to_string())
776                            })
777                            .maybe(!display_placeholder, |el| {
778                                let editor = editable.editor().read();
779                                if editor.has_preedit() {
780                                    let (b, p, a) = editor.preedit_text_segments();
781                                    let (b, p, a) = match self.mode {
782                                        InputMode::Hidden(ch) => {
783                                            let ch = ch.to_string();
784                                            (
785                                                ch.repeat(b.chars().count()),
786                                                ch.repeat(p.chars().count()),
787                                                ch.repeat(a.chars().count()),
788                                            )
789                                        }
790                                        InputMode::Shown => (b, p, a),
791                                    };
792                                    el.span(b)
793                                        .span(
794                                            Span::new(p).text_decoration(TextDecoration::Underline),
795                                        )
796                                        .span(a)
797                                } else {
798                                    let text = match self.mode {
799                                        InputMode::Hidden(ch) => {
800                                            ch.to_string().repeat(editor.rope().len_chars())
801                                        }
802                                        InputMode::Shown => editor.rope().to_string(),
803                                    };
804                                    el.span(text)
805                                }
806                            }),
807                    ),
808            )
809            .maybe_child(
810                self.trailing
811                    .clone()
812                    .map(|trailing| rect().padding(Gaps::new(0., 8., 0., 0.)).child(trailing)),
813            )
814    }
815
816    fn render_key(&self) -> DiffKey {
817        self.key.clone().or(self.default_key())
818    }
819}