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