Skip to main content

egui_shadcn/
typography.rs

1use crate::theme::Theme;
2use crate::tokens::mix;
3use egui::{
4    Align, Color32, CornerRadius, FontFamily, FontId, Frame, Response, RichText, Sense, Stroke, Ui,
5    WidgetText, pos2, vec2,
6};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum TextAs {
10    Span,
11    Div,
12    Label,
13    P,
14}
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum TextWeight {
18    Light,
19    Regular,
20    Medium,
21    Bold,
22    ExtraBold,
23}
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum TextAlign {
27    Left,
28    Center,
29    Right,
30}
31
32impl TextAlign {
33    fn to_egui(self) -> Align {
34        match self {
35            TextAlign::Left => Align::Min,
36            TextAlign::Center => Align::Center,
37            TextAlign::Right => Align::Max,
38        }
39    }
40}
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub enum TextTrim {
44    Normal,
45    Start,
46    End,
47    Both,
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub enum TextWrap {
52    Wrap,
53    NoWrap,
54    Pretty,
55    Balance,
56}
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum TypographyColor {
60    Default,
61    Muted,
62    Primary,
63}
64
65impl TypographyColor {
66    fn resolve(self, theme: &Theme, high_contrast: bool) -> Color32 {
67        match self {
68            TypographyColor::Default => theme.palette.foreground,
69            TypographyColor::Muted => {
70                if high_contrast {
71                    mix(
72                        theme.palette.muted_foreground,
73                        theme.palette.foreground,
74                        0.25,
75                    )
76                } else {
77                    theme.palette.muted_foreground
78                }
79            }
80            TypographyColor::Primary => theme.palette.primary,
81        }
82    }
83}
84
85#[derive(Clone, Copy, Debug, PartialEq, Eq)]
86pub enum HeadingAs {
87    H1,
88    H2,
89    H3,
90    H4,
91    H5,
92    H6,
93}
94
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub enum LinkUnderline {
97    Auto,
98    Always,
99    Hover,
100    None,
101}
102
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104pub enum CodeVariant {
105    Solid,
106    Soft,
107    Outline,
108    Ghost,
109}
110
111#[derive(Clone, Copy, Debug, PartialEq, Eq)]
112pub enum ShadcnTypographyVariant {
113    H1,
114    H2,
115    H3,
116    H4,
117    P,
118    Lead,
119    Large,
120    Small,
121    Muted,
122    InlineCode,
123    Blockquote,
124}
125
126#[derive(Clone, Debug)]
127pub struct TextProps {
128    pub text: WidgetText,
129    pub as_tag: TextAs,
130    pub size: Option<f32>,
131    pub weight: Option<TextWeight>,
132    pub align: Option<TextAlign>,
133    pub trim: Option<TextTrim>,
134    pub truncate: bool,
135    pub wrap: Option<TextWrap>,
136    pub color: Option<TypographyColor>,
137    pub high_contrast: bool,
138    pub italic: bool,
139    pub monospace: bool,
140    pub underline: bool,
141}
142
143impl TextProps {
144    pub fn new(text: impl Into<WidgetText>) -> Self {
145        Self {
146            text: text.into(),
147            as_tag: TextAs::Span,
148            size: None,
149            weight: None,
150            align: None,
151            trim: None,
152            truncate: false,
153            wrap: None,
154            color: None,
155            high_contrast: false,
156            italic: false,
157            monospace: false,
158            underline: false,
159        }
160    }
161
162    pub fn as_tag(mut self, as_tag: TextAs) -> Self {
163        self.as_tag = as_tag;
164        self
165    }
166
167    pub fn size(mut self, size: f32) -> Self {
168        self.size = Some(size);
169        self
170    }
171
172    pub fn weight(mut self, weight: TextWeight) -> Self {
173        self.weight = Some(weight);
174        self
175    }
176
177    pub fn align(mut self, align: TextAlign) -> Self {
178        self.align = Some(align);
179        self
180    }
181
182    pub fn trim(mut self, trim: TextTrim) -> Self {
183        self.trim = Some(trim);
184        self
185    }
186
187    pub fn truncate(mut self, truncate: bool) -> Self {
188        self.truncate = truncate;
189        self
190    }
191
192    pub fn wrap(mut self, wrap: TextWrap) -> Self {
193        self.wrap = Some(wrap);
194        self
195    }
196
197    pub fn color(mut self, color: TypographyColor) -> Self {
198        self.color = Some(color);
199        self
200    }
201
202    pub fn high_contrast(mut self, high_contrast: bool) -> Self {
203        self.high_contrast = high_contrast;
204        self
205    }
206
207    pub fn italic(mut self, italic: bool) -> Self {
208        self.italic = italic;
209        self
210    }
211
212    pub fn monospace(mut self, monospace: bool) -> Self {
213        self.monospace = monospace;
214        self
215    }
216
217    pub fn underline(mut self, underline: bool) -> Self {
218        self.underline = underline;
219        self
220    }
221}
222
223#[derive(Clone, Debug)]
224pub struct HeadingProps {
225    pub text: WidgetText,
226    pub as_tag: HeadingAs,
227    pub size: Option<f32>,
228    pub weight: Option<TextWeight>,
229    pub align: Option<TextAlign>,
230    pub trim: Option<TextTrim>,
231    pub truncate: bool,
232    pub wrap: Option<TextWrap>,
233    pub color: Option<TypographyColor>,
234    pub high_contrast: bool,
235}
236
237impl HeadingProps {
238    pub fn new(text: impl Into<WidgetText>) -> Self {
239        Self {
240            text: text.into(),
241            as_tag: HeadingAs::H1,
242            size: Some(30.0),
243            weight: None,
244            align: None,
245            trim: None,
246            truncate: false,
247            wrap: None,
248            color: None,
249            high_contrast: false,
250        }
251    }
252
253    pub fn as_tag(mut self, as_tag: HeadingAs) -> Self {
254        self.as_tag = as_tag;
255        self
256    }
257
258    pub fn size(mut self, size: f32) -> Self {
259        self.size = Some(size);
260        self
261    }
262
263    pub fn weight(mut self, weight: TextWeight) -> Self {
264        self.weight = Some(weight);
265        self
266    }
267
268    pub fn align(mut self, align: TextAlign) -> Self {
269        self.align = Some(align);
270        self
271    }
272
273    pub fn trim(mut self, trim: TextTrim) -> Self {
274        self.trim = Some(trim);
275        self
276    }
277
278    pub fn truncate(mut self, truncate: bool) -> Self {
279        self.truncate = truncate;
280        self
281    }
282
283    pub fn wrap(mut self, wrap: TextWrap) -> Self {
284        self.wrap = Some(wrap);
285        self
286    }
287
288    pub fn color(mut self, color: TypographyColor) -> Self {
289        self.color = Some(color);
290        self
291    }
292
293    pub fn high_contrast(mut self, high_contrast: bool) -> Self {
294        self.high_contrast = high_contrast;
295        self
296    }
297}
298
299#[derive(Clone, Debug)]
300pub struct LinkProps {
301    pub text: WidgetText,
302    pub underline: LinkUnderline,
303    pub size: Option<f32>,
304    pub weight: Option<TextWeight>,
305    pub trim: Option<TextTrim>,
306    pub truncate: bool,
307    pub wrap: Option<TextWrap>,
308    pub color: Option<TypographyColor>,
309    pub high_contrast: bool,
310}
311
312impl LinkProps {
313    pub fn new(text: impl Into<WidgetText>) -> Self {
314        Self {
315            text: text.into(),
316            underline: LinkUnderline::Auto,
317            size: None,
318            weight: None,
319            trim: None,
320            truncate: false,
321            wrap: None,
322            color: Some(TypographyColor::Primary),
323            high_contrast: false,
324        }
325    }
326
327    pub fn underline(mut self, underline: LinkUnderline) -> Self {
328        self.underline = underline;
329        self
330    }
331
332    pub fn size(mut self, size: f32) -> Self {
333        self.size = Some(size);
334        self
335    }
336
337    pub fn weight(mut self, weight: TextWeight) -> Self {
338        self.weight = Some(weight);
339        self
340    }
341
342    pub fn trim(mut self, trim: TextTrim) -> Self {
343        self.trim = Some(trim);
344        self
345    }
346
347    pub fn truncate(mut self, truncate: bool) -> Self {
348        self.truncate = truncate;
349        self
350    }
351
352    pub fn wrap(mut self, wrap: TextWrap) -> Self {
353        self.wrap = Some(wrap);
354        self
355    }
356
357    pub fn color(mut self, color: TypographyColor) -> Self {
358        self.color = Some(color);
359        self
360    }
361
362    pub fn high_contrast(mut self, high_contrast: bool) -> Self {
363        self.high_contrast = high_contrast;
364        self
365    }
366}
367
368#[derive(Clone, Debug)]
369pub struct CodeProps {
370    pub text: WidgetText,
371    pub variant: CodeVariant,
372    pub size: Option<f32>,
373    pub weight: Option<TextWeight>,
374    pub color: Option<TypographyColor>,
375    pub high_contrast: bool,
376    pub truncate: bool,
377    pub wrap: Option<TextWrap>,
378}
379
380impl CodeProps {
381    pub fn new(text: impl Into<WidgetText>) -> Self {
382        Self {
383            text: text.into(),
384            variant: CodeVariant::Soft,
385            size: None,
386            weight: None,
387            color: None,
388            high_contrast: false,
389            truncate: false,
390            wrap: None,
391        }
392    }
393
394    pub fn variant(mut self, variant: CodeVariant) -> Self {
395        self.variant = variant;
396        self
397    }
398
399    pub fn size(mut self, size: f32) -> Self {
400        self.size = Some(size);
401        self
402    }
403
404    pub fn weight(mut self, weight: TextWeight) -> Self {
405        self.weight = Some(weight);
406        self
407    }
408
409    pub fn color(mut self, color: TypographyColor) -> Self {
410        self.color = Some(color);
411        self
412    }
413
414    pub fn high_contrast(mut self, high_contrast: bool) -> Self {
415        self.high_contrast = high_contrast;
416        self
417    }
418
419    pub fn truncate(mut self, truncate: bool) -> Self {
420        self.truncate = truncate;
421        self
422    }
423
424    pub fn wrap(mut self, wrap: TextWrap) -> Self {
425        self.wrap = Some(wrap);
426        self
427    }
428}
429
430#[derive(Clone, Debug)]
431pub struct BlockquoteProps {
432    pub text: WidgetText,
433    pub size: Option<f32>,
434    pub high_contrast: bool,
435    pub truncate: bool,
436    pub wrap: Option<TextWrap>,
437}
438
439impl BlockquoteProps {
440    pub fn new(text: impl Into<WidgetText>) -> Self {
441        Self {
442            text: text.into(),
443            size: None,
444            high_contrast: false,
445            truncate: false,
446            wrap: None,
447        }
448    }
449
450    pub fn size(mut self, size: f32) -> Self {
451        self.size = Some(size);
452        self
453    }
454
455    pub fn high_contrast(mut self, high_contrast: bool) -> Self {
456        self.high_contrast = high_contrast;
457        self
458    }
459
460    pub fn truncate(mut self, truncate: bool) -> Self {
461        self.truncate = truncate;
462        self
463    }
464
465    pub fn wrap(mut self, wrap: TextWrap) -> Self {
466        self.wrap = Some(wrap);
467        self
468    }
469}
470
471#[derive(Clone, Debug)]
472pub struct TypographyProps {
473    pub text: WidgetText,
474    pub variant: ShadcnTypographyVariant,
475    pub align: Option<TextAlign>,
476}
477
478impl TypographyProps {
479    pub fn new(text: impl Into<WidgetText>) -> Self {
480        Self {
481            text: text.into(),
482            variant: ShadcnTypographyVariant::P,
483            align: None,
484        }
485    }
486
487    pub fn variant(mut self, variant: ShadcnTypographyVariant) -> Self {
488        self.variant = variant;
489        self
490    }
491
492    pub fn align(mut self, align: TextAlign) -> Self {
493        self.align = Some(align);
494        self
495    }
496}
497
498#[derive(Clone, Copy, Debug, PartialEq)]
499pub struct ResolvedTextStyle {
500    pub size: f32,
501    pub monospace: bool,
502    pub strong: bool,
503    pub italic: bool,
504    pub underline: bool,
505    pub color: TypographyColor,
506}
507
508pub fn resolve_shadcn_style(variant: ShadcnTypographyVariant) -> ResolvedTextStyle {
509    match variant {
510        ShadcnTypographyVariant::H1 => ResolvedTextStyle {
511            size: 36.0,
512            monospace: false,
513            strong: true,
514            italic: false,
515            underline: false,
516            color: TypographyColor::Default,
517        },
518        ShadcnTypographyVariant::H2 => ResolvedTextStyle {
519            size: 30.0,
520            monospace: false,
521            strong: true,
522            italic: false,
523            underline: false,
524            color: TypographyColor::Default,
525        },
526        ShadcnTypographyVariant::H3 => ResolvedTextStyle {
527            size: 24.0,
528            monospace: false,
529            strong: true,
530            italic: false,
531            underline: false,
532            color: TypographyColor::Default,
533        },
534        ShadcnTypographyVariant::H4 => ResolvedTextStyle {
535            size: 20.0,
536            monospace: false,
537            strong: true,
538            italic: false,
539            underline: false,
540            color: TypographyColor::Default,
541        },
542        ShadcnTypographyVariant::Lead => ResolvedTextStyle {
543            size: 20.0,
544            monospace: false,
545            strong: false,
546            italic: false,
547            underline: false,
548            color: TypographyColor::Muted,
549        },
550        ShadcnTypographyVariant::Large => ResolvedTextStyle {
551            size: 18.0,
552            monospace: false,
553            strong: true,
554            italic: false,
555            underline: false,
556            color: TypographyColor::Default,
557        },
558        ShadcnTypographyVariant::Small => ResolvedTextStyle {
559            size: 14.0,
560            monospace: false,
561            strong: true,
562            italic: false,
563            underline: false,
564            color: TypographyColor::Default,
565        },
566        ShadcnTypographyVariant::Muted => ResolvedTextStyle {
567            size: 14.0,
568            monospace: false,
569            strong: false,
570            italic: false,
571            underline: false,
572            color: TypographyColor::Muted,
573        },
574        ShadcnTypographyVariant::InlineCode => ResolvedTextStyle {
575            size: 14.0,
576            monospace: true,
577            strong: true,
578            italic: false,
579            underline: false,
580            color: TypographyColor::Default,
581        },
582        ShadcnTypographyVariant::Blockquote => ResolvedTextStyle {
583            size: 14.0,
584            monospace: false,
585            strong: false,
586            italic: true,
587            underline: false,
588            color: TypographyColor::Default,
589        },
590        ShadcnTypographyVariant::P => ResolvedTextStyle {
591            size: 14.0,
592            monospace: false,
593            strong: false,
594            italic: false,
595            underline: false,
596            color: TypographyColor::Default,
597        },
598    }
599}
600
601fn widget_text_to_plain(text: WidgetText) -> String {
602    match text {
603        WidgetText::Text(t) => t,
604        WidgetText::RichText(rt) => rt.text().to_string(),
605        WidgetText::Galley(g) => g.text().to_string(),
606        WidgetText::LayoutJob(job) => job.text.to_string(),
607    }
608}
609
610#[derive(Clone, Copy, Debug, Default)]
611struct TextStyleOptions {
612    size: Option<f32>,
613    weight: Option<TextWeight>,
614    italic: bool,
615    monospace: bool,
616    underline: bool,
617    color: Option<TypographyColor>,
618    high_contrast: bool,
619}
620
621fn resolve_text_style(theme: &Theme, opts: TextStyleOptions) -> ResolvedTextStyle {
622    let size = opts.size.unwrap_or(14.0);
623    let strong = matches!(
624        opts.weight.unwrap_or(TextWeight::Regular),
625        TextWeight::Medium | TextWeight::Bold | TextWeight::ExtraBold
626    );
627    let mut resolved = ResolvedTextStyle {
628        size,
629        monospace: opts.monospace,
630        strong,
631        italic: opts.italic,
632        underline: opts.underline,
633        color: opts.color.unwrap_or(TypographyColor::Default),
634    };
635    if opts.high_contrast && resolved.color == TypographyColor::Muted {
636        resolved.color = TypographyColor::Default;
637    }
638    let _ = theme;
639    resolved
640}
641
642fn label_for_text(
643    ui: &mut Ui,
644    rich: RichText,
645    align: Option<TextAlign>,
646    wrap: Option<TextWrap>,
647) -> Response {
648    let wrap = wrap.unwrap_or(TextWrap::Wrap);
649    let mut label = egui::Label::new(rich);
650    if !matches!(wrap, TextWrap::NoWrap) {
651        label = label.wrap();
652    }
653    if let Some(align) = align {
654        ui.with_layout(egui::Layout::top_down(align.to_egui()), |ui| ui.add(label))
655            .inner
656    } else {
657        ui.add(label)
658    }
659}
660
661pub fn text(ui: &mut Ui, theme: &Theme, props: TextProps) -> Response {
662    let resolved = resolve_text_style(
663        theme,
664        TextStyleOptions {
665            size: props.size,
666            weight: props.weight,
667            italic: props.italic,
668            monospace: props.monospace,
669            underline: props.underline,
670            color: props.color,
671            high_contrast: props.high_contrast,
672        },
673    );
674
675    let color = resolved.color.resolve(theme, props.high_contrast);
676    let mut rich = RichText::new(widget_text_to_plain(props.text));
677    rich = rich.color(color);
678    rich = rich.font(FontId::new(
679        resolved.size,
680        if resolved.monospace {
681            FontFamily::Monospace
682        } else {
683            FontFamily::Proportional
684        },
685    ));
686    if resolved.strong {
687        rich = rich.strong();
688    }
689    if resolved.italic {
690        rich = rich.italics();
691    }
692    if resolved.underline {
693        rich = rich.underline();
694    }
695
696    let _ = props.as_tag;
697    let _ = props.trim;
698    let _ = props.truncate;
699
700    label_for_text(ui, rich, props.align, props.wrap)
701}
702
703pub fn heading(ui: &mut Ui, theme: &Theme, props: HeadingProps) -> Response {
704    let mut resolved = resolve_text_style(
705        theme,
706        TextStyleOptions {
707            size: props.size,
708            weight: props.weight,
709            color: props.color,
710            high_contrast: props.high_contrast,
711            ..Default::default()
712        },
713    );
714    if !resolved.strong {
715        resolved.strong = true;
716    }
717
718    let color = resolved.color.resolve(theme, props.high_contrast);
719    let rich = RichText::new(widget_text_to_plain(props.text))
720        .color(color)
721        .font(FontId::proportional(resolved.size))
722        .strong();
723
724    let _ = props.as_tag;
725    let _ = props.trim;
726    let _ = props.truncate;
727
728    label_for_text(ui, rich, props.align, props.wrap)
729}
730
731pub fn link(ui: &mut Ui, theme: &Theme, props: LinkProps) -> Response {
732    let resolved = resolve_text_style(
733        theme,
734        TextStyleOptions {
735            size: props.size,
736            weight: props.weight,
737            color: props.color,
738            high_contrast: props.high_contrast,
739            ..Default::default()
740        },
741    );
742    let base_color = resolved.color.resolve(theme, props.high_contrast);
743    let mut rich = RichText::new(widget_text_to_plain(props.text))
744        .color(base_color)
745        .font(FontId::proportional(resolved.size));
746    if resolved.strong {
747        rich = rich.strong();
748    }
749
750    let base = ui.add(egui::Label::new(rich).sense(Sense::click()).wrap());
751    let underline = match props.underline {
752        LinkUnderline::None => false,
753        LinkUnderline::Always => true,
754        LinkUnderline::Hover => base.hovered(),
755        LinkUnderline::Auto => base.hovered(),
756    };
757    if underline {
758        let painter = ui.painter();
759        let y = base.rect.bottom() - 1.0;
760        painter.line_segment(
761            [pos2(base.rect.left(), y), pos2(base.rect.right(), y)],
762            Stroke::new(1.0, base_color),
763        );
764    }
765
766    let _ = props.trim;
767    let _ = props.truncate;
768    let _ = props.wrap;
769
770    base
771}
772
773pub fn code(ui: &mut Ui, theme: &Theme, props: CodeProps) -> Response {
774    let resolved = resolve_text_style(
775        theme,
776        TextStyleOptions {
777            size: props.size.or(Some(14.0)),
778            weight: props.weight.or(Some(TextWeight::Bold)),
779            monospace: true,
780            color: props.color,
781            high_contrast: props.high_contrast,
782            ..Default::default()
783        },
784    );
785    let fg = resolved.color.resolve(theme, props.high_contrast);
786    let rounding = CornerRadius::same(4);
787
788    let (fill, stroke) = match props.variant {
789        CodeVariant::Soft => (theme.palette.muted, Stroke::NONE),
790        CodeVariant::Solid => (theme.palette.primary, Stroke::NONE),
791        CodeVariant::Outline => (Color32::TRANSPARENT, Stroke::new(1.0, theme.palette.border)),
792        CodeVariant::Ghost => (Color32::TRANSPARENT, Stroke::NONE),
793    };
794
795    let rich = RichText::new(widget_text_to_plain(props.text))
796        .font(FontId::new(resolved.size, FontFamily::Monospace))
797        .color(match props.variant {
798            CodeVariant::Solid => theme.palette.primary_foreground,
799            _ => fg,
800        })
801        .strong();
802
803    let inner_margin = vec2(6.0, 4.0);
804    let frame = Frame::NONE
805        .fill(fill)
806        .stroke(stroke)
807        .corner_radius(rounding)
808        .inner_margin(inner_margin);
809
810    let response = frame
811        .show(ui, |ui| ui.add(egui::Label::new(rich).wrap()))
812        .inner;
813
814    let _ = props.truncate;
815    let _ = props.wrap;
816
817    response
818}
819
820pub fn blockquote(ui: &mut Ui, theme: &Theme, props: BlockquoteProps) -> Response {
821    let size = props.size.unwrap_or(14.0);
822    let fg = theme.palette.foreground;
823    let rich = RichText::new(widget_text_to_plain(props.text))
824        .font(FontId::proportional(size))
825        .color(fg)
826        .italics();
827
828    let indent = 24.0;
829    let border_width = 2.0;
830    let response = ui
831        .horizontal(|ui| {
832            ui.add_space(indent);
833            ui.add(egui::Label::new(rich).wrap())
834        })
835        .inner;
836    let border_x = response.rect.left() - indent + border_width * 0.5;
837    ui.painter().line_segment(
838        [
839            pos2(border_x, response.rect.top()),
840            pos2(border_x, response.rect.bottom()),
841        ],
842        Stroke::new(border_width, theme.palette.border),
843    );
844
845    let _ = props.truncate;
846    let _ = props.wrap;
847
848    response
849}
850
851pub fn typography(ui: &mut Ui, theme: &Theme, props: TypographyProps) -> Response {
852    match props.variant {
853        ShadcnTypographyVariant::InlineCode => code(
854            ui,
855            theme,
856            CodeProps::new(props.text).variant(CodeVariant::Soft),
857        ),
858        ShadcnTypographyVariant::Blockquote => {
859            blockquote(ui, theme, BlockquoteProps::new(props.text))
860        }
861        variant => {
862            let resolved = resolve_shadcn_style(variant);
863            let color = resolved.color.resolve(theme, false);
864            let mut rich = RichText::new(widget_text_to_plain(props.text))
865                .font(FontId::new(
866                    resolved.size,
867                    if resolved.monospace {
868                        FontFamily::Monospace
869                    } else {
870                        FontFamily::Proportional
871                    },
872                ))
873                .color(color);
874            if resolved.strong {
875                rich = rich.strong();
876            }
877            if resolved.italic {
878                rich = rich.italics();
879            }
880            if resolved.underline {
881                rich = rich.underline();
882            }
883
884            let response = label_for_text(ui, rich, props.align, Some(TextWrap::Wrap));
885            if variant == ShadcnTypographyVariant::H2 {
886                let padding_bottom = 8.0;
887                ui.add_space(padding_bottom);
888                let y = response.rect.bottom() + padding_bottom;
889                ui.painter().line_segment(
890                    [
891                        pos2(response.rect.left(), y),
892                        pos2(response.rect.right(), y),
893                    ],
894                    Stroke::new(1.0, theme.palette.border),
895                );
896            }
897            response
898        }
899    }
900}