Skip to main content

floem/style/
components.rs

1//! Style component types for borders, shadows, padding, and margins.
2//!
3//! This module provides composite style types that represent multi-value
4//! CSS properties like borders and padding.
5
6use floem_renderer::text::Weight;
7use peniko::color::palette;
8use peniko::{Brush, Color};
9
10use crate::theme::StyleThemeExt;
11use crate::unit::{PxPct, PxPctAuto};
12use crate::view::{IntoView, View};
13use crate::views::{ContainerExt, Decorators, Stack, TooltipExt};
14
15use super::values::{CombineResult, StrokeWrap, StylePropValue};
16
17/// Pointer event handling mode
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum PointerEvents {
20    Auto,
21    None,
22}
23
24/// Text overflow behavior
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum TextOverflow {
27    Wrap,
28    Clip,
29    Ellipsis,
30}
31
32/// Cursor style
33#[derive(Debug, Clone, Copy, PartialEq, Default)]
34pub enum CursorStyle {
35    #[default]
36    Default,
37    Pointer,
38    Progress,
39    Wait,
40    Crosshair,
41    Text,
42    Move,
43    Grab,
44    Grabbing,
45    ColResize,
46    RowResize,
47    WResize,
48    EResize,
49    SResize,
50    NResize,
51    NwResize,
52    NeResize,
53    SwResize,
54    SeResize,
55    NeswResize,
56    NwseResize,
57}
58
59/// Structure holding data about the shadow.
60#[derive(Debug, Clone, Copy, PartialEq)]
61pub struct BoxShadow {
62    pub blur_radius: PxPct,
63    pub color: Color,
64    pub spread: PxPct,
65
66    pub left_offset: PxPct,
67    pub right_offset: PxPct,
68    pub top_offset: PxPct,
69    pub bottom_offset: PxPct,
70}
71
72impl BoxShadow {
73    /// Create new default shadow.
74    pub fn new() -> Self {
75        Self::default()
76    }
77
78    /// Specifies shadow blur. The larger this value, the bigger the blur,
79    /// so the shadow becomes bigger and lighter.
80    pub fn blur_radius(mut self, radius: impl Into<PxPct>) -> Self {
81        self.blur_radius = radius.into();
82        self
83    }
84
85    /// Specifies shadow blur spread. Positive values will cause the shadow
86    /// to expand and grow bigger, negative values will cause the shadow to shrink.
87    pub fn spread(mut self, spread: impl Into<PxPct>) -> Self {
88        self.spread = spread.into();
89        self
90    }
91
92    /// Specifies color for the current shadow.
93    pub fn color(mut self, color: impl Into<Color>) -> Self {
94        self.color = color.into();
95        self
96    }
97
98    /// Specifies the offset of the left edge.
99    pub fn left_offset(mut self, left_offset: impl Into<PxPct>) -> Self {
100        self.left_offset = left_offset.into();
101        self
102    }
103
104    /// Specifies the offset of the right edge.
105    pub fn right_offset(mut self, right_offset: impl Into<PxPct>) -> Self {
106        self.right_offset = right_offset.into();
107        self
108    }
109
110    /// Specifies the offset of the top edge.
111    pub fn top_offset(mut self, top_offset: impl Into<PxPct>) -> Self {
112        self.top_offset = top_offset.into();
113        self
114    }
115
116    /// Specifies the offset of the bottom edge.
117    pub fn bottom_offset(mut self, bottom_offset: impl Into<PxPct>) -> Self {
118        self.bottom_offset = bottom_offset.into();
119        self
120    }
121
122    /// Specifies the offset on vertical axis.
123    /// Negative offset value places the shadow above the element.
124    pub fn v_offset(mut self, v_offset: impl Into<PxPct>) -> Self {
125        let offset = v_offset.into();
126        self.top_offset = -offset;
127        self.bottom_offset = offset;
128        self
129    }
130
131    /// Specifies the offset on horizontal axis.
132    /// Negative offset value places the shadow to the left of the element.
133    pub fn h_offset(mut self, h_offset: impl Into<PxPct>) -> Self {
134        let offset = h_offset.into();
135        self.left_offset = -offset;
136        self.right_offset = offset;
137        self
138    }
139}
140
141impl Default for BoxShadow {
142    fn default() -> Self {
143        Self {
144            blur_radius: PxPct::Px(0.),
145            color: palette::css::BLACK,
146            spread: PxPct::Px(0.),
147            left_offset: PxPct::Px(0.),
148            right_offset: PxPct::Px(0.),
149            top_offset: PxPct::Px(0.),
150            bottom_offset: PxPct::Px(0.),
151        }
152    }
153}
154
155/// Structure holding border widths for all four sides
156#[derive(Debug, Clone, PartialEq, Default)]
157pub struct Border {
158    pub left: Option<StrokeWrap>,
159    pub top: Option<StrokeWrap>,
160    pub right: Option<StrokeWrap>,
161    pub bottom: Option<StrokeWrap>,
162}
163
164impl Border {
165    pub fn new() -> Self {
166        Self::default()
167    }
168
169    pub fn all(border: impl Into<StrokeWrap>) -> Self {
170        let border = border.into();
171        Self {
172            left: Some(border.clone()),
173            top: Some(border.clone()),
174            right: Some(border.clone()),
175            bottom: Some(border),
176        }
177    }
178
179    pub fn left(mut self, border: impl Into<StrokeWrap>) -> Self {
180        self.left = Some(border.into());
181        self
182    }
183
184    pub fn top(mut self, border: impl Into<StrokeWrap>) -> Self {
185        self.top = Some(border.into());
186        self
187    }
188
189    pub fn right(mut self, border: impl Into<StrokeWrap>) -> Self {
190        self.right = Some(border.into());
191        self
192    }
193
194    pub fn bottom(mut self, border: impl Into<StrokeWrap>) -> Self {
195        self.bottom = Some(border.into());
196        self
197    }
198
199    pub fn horiz(mut self, border: impl Into<StrokeWrap>) -> Self {
200        let border = border.into();
201        self.left = Some(border.clone());
202        self.right = Some(border);
203        self
204    }
205
206    pub fn vert(mut self, border: impl Into<StrokeWrap>) -> Self {
207        let border = border.into();
208        self.top = Some(border.clone());
209        self.bottom = Some(border);
210        self
211    }
212}
213
214impl StylePropValue for Border {
215    fn debug_view(&self) -> Option<Box<dyn View>> {
216        let border = self.clone();
217        let details_view = move || {
218            let sides = [
219                ("Left:", border.left),
220                ("Top:", border.top),
221                ("Right:", border.right),
222                ("Bottom:", border.bottom),
223            ];
224
225            Stack::vertical_from_iter(
226                sides
227                    .into_iter()
228                    .filter_map(|(l, v)| v.map(|v| (l, v)))
229                    .map(|(label, value)| {
230                        Stack::horizontal((
231                            label.style(|s| s.font_weight(Weight::BOLD).width(80.0)),
232                            value.debug_view().unwrap(),
233                        ))
234                        .style(|s| s.items_center().gap(4.0))
235                        .into_any()
236                    }),
237            )
238            .style(|s| s.gap(4.0).padding(8.0))
239        };
240        Some(details_view().into_any())
241    }
242
243    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
244        Some(Self {
245            left: self.left.interpolate(&other.left, value)?,
246            top: self.top.interpolate(&other.top, value)?,
247            right: self.right.interpolate(&other.right, value)?,
248            bottom: self.bottom.interpolate(&other.bottom, value)?,
249        })
250    }
251
252    fn combine(&self, other: &Self) -> CombineResult<Self> {
253        let result = Border {
254            left: other.left.clone().or_else(|| self.left.clone()),
255            top: other.top.clone().or_else(|| self.top.clone()),
256            right: other.right.clone().or_else(|| self.right.clone()),
257            bottom: other.bottom.clone().or_else(|| self.bottom.clone()),
258        };
259
260        if result == *other {
261            CombineResult::Other
262        } else {
263            CombineResult::New(result)
264        }
265    }
266}
267
268/// Structure holding border colors for all four sides
269#[derive(Debug, Clone, PartialEq, Default)]
270pub struct BorderColor {
271    pub left: Option<Brush>,
272    pub top: Option<Brush>,
273    pub right: Option<Brush>,
274    pub bottom: Option<Brush>,
275}
276
277impl BorderColor {
278    pub fn new() -> Self {
279        Self::default()
280    }
281
282    pub fn all(color: impl Into<Brush>) -> Self {
283        let color = color.into();
284        Self {
285            left: Some(color.clone()),
286            top: Some(color.clone()),
287            right: Some(color.clone()),
288            bottom: Some(color),
289        }
290    }
291
292    pub fn left(mut self, color: impl Into<Brush>) -> Self {
293        self.left = Some(color.into());
294        self
295    }
296
297    pub fn top(mut self, color: impl Into<Brush>) -> Self {
298        self.top = Some(color.into());
299        self
300    }
301
302    pub fn right(mut self, color: impl Into<Brush>) -> Self {
303        self.right = Some(color.into());
304        self
305    }
306
307    pub fn bottom(mut self, color: impl Into<Brush>) -> Self {
308        self.bottom = Some(color.into());
309        self
310    }
311
312    pub fn horiz(mut self, color: impl Into<Brush>) -> Self {
313        let color = color.into();
314        self.left = Some(color.clone());
315        self.right = Some(color);
316        self
317    }
318
319    pub fn vert(mut self, color: impl Into<Brush>) -> Self {
320        let color = color.into();
321        self.top = Some(color.clone());
322        self.bottom = Some(color);
323        self
324    }
325}
326
327impl StylePropValue for BorderColor {
328    fn debug_view(&self) -> Option<Box<dyn View>> {
329        let border_color = self.clone();
330        let details_view = move || {
331            let sides = [
332                ("Left:", border_color.left),
333                ("Top:", border_color.top),
334                ("Right:", border_color.right),
335                ("Bottom:", border_color.bottom),
336            ];
337
338            Stack::vertical_from_iter(
339                sides
340                    .into_iter()
341                    .filter_map(|(l, v)| v.map(|v| (l, v)))
342                    .map(|(label, color)| {
343                        Stack::horizontal((
344                            label.style(|s| s.font_weight(Weight::BOLD).width(80.0)),
345                            color.debug_view().unwrap(),
346                        ))
347                        .style(|s| s.items_center().gap(4.0))
348                    }),
349            )
350            .style(|s| s.gap(4.0).padding(8.0))
351        };
352        Some(details_view().into_any())
353    }
354
355    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
356        Some(Self {
357            left: self.left.interpolate(&other.left, value)?,
358            top: self.top.interpolate(&other.top, value)?,
359            right: self.right.interpolate(&other.right, value)?,
360            bottom: self.bottom.interpolate(&other.bottom, value)?,
361        })
362    }
363
364    fn combine(&self, other: &Self) -> CombineResult<Self> {
365        let result = BorderColor {
366            left: other.left.clone().or_else(|| self.left.clone()),
367            top: other.top.clone().or_else(|| self.top.clone()),
368            right: other.right.clone().or_else(|| self.right.clone()),
369            bottom: other.bottom.clone().or_else(|| self.bottom.clone()),
370        };
371
372        if result == *other {
373            CombineResult::Other
374        } else {
375            CombineResult::New(result)
376        }
377    }
378}
379
380/// Structure holding border radius for all four corners
381#[derive(Debug, Clone, Copy, PartialEq, Default)]
382pub struct BorderRadius {
383    pub top_left: Option<PxPct>,
384    pub top_right: Option<PxPct>,
385    pub bottom_left: Option<PxPct>,
386    pub bottom_right: Option<PxPct>,
387}
388
389impl BorderRadius {
390    pub fn new() -> Self {
391        Self::default()
392    }
393
394    pub fn all(radius: impl Into<PxPct>) -> Self {
395        let radius = radius.into();
396        Self {
397            top_left: Some(radius),
398            top_right: Some(radius),
399            bottom_left: Some(radius),
400            bottom_right: Some(radius),
401        }
402    }
403
404    pub fn top_left(mut self, radius: impl Into<PxPct>) -> Self {
405        self.top_left = Some(radius.into());
406        self
407    }
408
409    pub fn top_right(mut self, radius: impl Into<PxPct>) -> Self {
410        self.top_right = Some(radius.into());
411        self
412    }
413
414    pub fn bottom_left(mut self, radius: impl Into<PxPct>) -> Self {
415        self.bottom_left = Some(radius.into());
416        self
417    }
418
419    pub fn bottom_right(mut self, radius: impl Into<PxPct>) -> Self {
420        self.bottom_right = Some(radius.into());
421        self
422    }
423
424    pub fn top(mut self, radius: impl Into<PxPct>) -> Self {
425        let radius = radius.into();
426        self.top_left = Some(radius);
427        self.top_right = Some(radius);
428        self
429    }
430
431    pub fn bottom(mut self, radius: impl Into<PxPct>) -> Self {
432        let radius = radius.into();
433        self.bottom_left = Some(radius);
434        self.bottom_right = Some(radius);
435        self
436    }
437
438    pub fn left(mut self, radius: impl Into<PxPct>) -> Self {
439        let radius = radius.into();
440        self.top_left = Some(radius);
441        self.bottom_left = Some(radius);
442        self
443    }
444
445    pub fn right(mut self, radius: impl Into<PxPct>) -> Self {
446        let radius = radius.into();
447        self.top_right = Some(radius);
448        self.bottom_right = Some(radius);
449        self
450    }
451
452    /// Resolve border radii to absolute pixels given the min side of the element.
453    /// Percentage values are resolved relative to the min side.
454    pub fn resolve_border_radii(&self, min_side: f64) -> peniko::kurbo::RoundedRectRadii {
455        fn resolve(val: Option<PxPct>, min_side: f64) -> f64 {
456            match val {
457                Some(PxPct::Px(px)) => px,
458                Some(PxPct::Pct(pct)) => min_side * pct / 100.0,
459                None => 0.0,
460            }
461        }
462        peniko::kurbo::RoundedRectRadii {
463            top_left: resolve(self.top_left, min_side),
464            top_right: resolve(self.top_right, min_side),
465            bottom_right: resolve(self.bottom_right, min_side),
466            bottom_left: resolve(self.bottom_left, min_side),
467        }
468    }
469}
470
471impl StylePropValue for BorderRadius {
472    fn debug_view(&self) -> Option<Box<dyn View>> {
473        let border_radius = *self;
474        let details_view = move || {
475            let corners = [
476                ("Top Left:", border_radius.top_left),
477                ("Top Right:", border_radius.top_right),
478                ("Bottom Left:", border_radius.bottom_left),
479                ("Bottom Right:", border_radius.bottom_right),
480            ];
481
482            Stack::vertical_from_iter(
483                corners
484                    .into_iter()
485                    .filter_map(|(l, v)| v.map(|v| (l, v)))
486                    .map(|(label, radius)| {
487                        Stack::horizontal((
488                            label.style(|s| s.font_weight(Weight::BOLD).width(80.0)),
489                            radius.debug_view().unwrap(),
490                        ))
491                        .style(|s| s.items_center().gap(4.0))
492                    }),
493            )
494            .style(|s| s.gap(4.0).padding(8.0))
495        };
496        Some(details_view().into_any())
497    }
498
499    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
500        Some(Self {
501            top_left: self.top_left.interpolate(&other.top_left, value)?,
502            top_right: self.top_right.interpolate(&other.top_right, value)?,
503            bottom_left: self.bottom_left.interpolate(&other.bottom_left, value)?,
504            bottom_right: self.bottom_right.interpolate(&other.bottom_right, value)?,
505        })
506    }
507
508    fn combine(&self, other: &Self) -> CombineResult<Self> {
509        let result = BorderRadius {
510            top_left: other.top_left.or(self.top_left),
511            top_right: other.top_right.or(self.top_right),
512            bottom_left: other.bottom_left.or(self.bottom_left),
513            bottom_right: other.bottom_right.or(self.bottom_right),
514        };
515
516        if result == *other {
517            CombineResult::Other
518        } else {
519            CombineResult::New(result)
520        }
521    }
522}
523
524/// Structure holding padding values for all four sides
525#[derive(Debug, Clone, Copy, PartialEq, Default)]
526pub struct Padding {
527    pub left: Option<PxPct>,
528    pub top: Option<PxPct>,
529    pub right: Option<PxPct>,
530    pub bottom: Option<PxPct>,
531}
532
533impl Padding {
534    pub fn new() -> Self {
535        Self::default()
536    }
537
538    pub fn all(padding: impl Into<PxPct>) -> Self {
539        let padding = padding.into();
540        Self {
541            left: Some(padding),
542            top: Some(padding),
543            right: Some(padding),
544            bottom: Some(padding),
545        }
546    }
547
548    pub fn left(mut self, padding: impl Into<PxPct>) -> Self {
549        self.left = Some(padding.into());
550        self
551    }
552
553    pub fn top(mut self, padding: impl Into<PxPct>) -> Self {
554        self.top = Some(padding.into());
555        self
556    }
557
558    pub fn right(mut self, padding: impl Into<PxPct>) -> Self {
559        self.right = Some(padding.into());
560        self
561    }
562
563    pub fn bottom(mut self, padding: impl Into<PxPct>) -> Self {
564        self.bottom = Some(padding.into());
565        self
566    }
567
568    pub fn horiz(mut self, padding: impl Into<PxPct>) -> Self {
569        let padding = padding.into();
570        self.left = Some(padding);
571        self.right = Some(padding);
572        self
573    }
574
575    pub fn vert(mut self, padding: impl Into<PxPct>) -> Self {
576        let padding = padding.into();
577        self.top = Some(padding);
578        self.bottom = Some(padding);
579        self
580    }
581}
582
583impl StylePropValue for Padding {
584    fn debug_view(&self) -> Option<Box<dyn View>> {
585        let padding = *self;
586        let details_view = move || {
587            let sides = [
588                ("Left:", padding.left),
589                ("Top:", padding.top),
590                ("Right:", padding.right),
591                ("Bottom:", padding.bottom),
592            ];
593
594            Stack::vertical_from_iter(
595                sides
596                    .into_iter()
597                    .filter_map(|(l, v)| v.map(|v| (l, v)))
598                    .map(|(label, padding)| {
599                        Stack::horizontal((
600                            label.style(|s| s.font_weight(Weight::BOLD).width(80.0)),
601                            padding.debug_view().unwrap(),
602                        ))
603                        .style(|s| s.items_center().gap(4.0))
604                    }),
605            )
606            .style(|s| s.gap(4.0).padding(8.0))
607        };
608        Some(details_view().into_any())
609    }
610
611    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
612        Some(Self {
613            left: self.left.interpolate(&other.left, value)?,
614            top: self.top.interpolate(&other.top, value)?,
615            right: self.right.interpolate(&other.right, value)?,
616            bottom: self.bottom.interpolate(&other.bottom, value)?,
617        })
618    }
619
620    fn combine(&self, other: &Self) -> CombineResult<Self> {
621        let result = Padding {
622            left: other.left.or(self.left),
623            top: other.top.or(self.top),
624            right: other.right.or(self.right),
625            bottom: other.bottom.or(self.bottom),
626        };
627
628        if result == *other {
629            CombineResult::Other
630        } else {
631            CombineResult::New(result)
632        }
633    }
634}
635
636/// Structure holding margin values for all four sides
637#[derive(Debug, Clone, Copy, PartialEq, Default)]
638pub struct Margin {
639    pub left: Option<PxPctAuto>,
640    pub top: Option<PxPctAuto>,
641    pub right: Option<PxPctAuto>,
642    pub bottom: Option<PxPctAuto>,
643}
644
645impl Margin {
646    pub fn new() -> Self {
647        Self::default()
648    }
649
650    pub fn all(margin: impl Into<PxPctAuto>) -> Self {
651        let margin = margin.into();
652        Self {
653            left: Some(margin),
654            top: Some(margin),
655            right: Some(margin),
656            bottom: Some(margin),
657        }
658    }
659
660    pub fn left(mut self, margin: impl Into<PxPctAuto>) -> Self {
661        self.left = Some(margin.into());
662        self
663    }
664
665    pub fn top(mut self, margin: impl Into<PxPctAuto>) -> Self {
666        self.top = Some(margin.into());
667        self
668    }
669
670    pub fn right(mut self, margin: impl Into<PxPctAuto>) -> Self {
671        self.right = Some(margin.into());
672        self
673    }
674
675    pub fn bottom(mut self, margin: impl Into<PxPctAuto>) -> Self {
676        self.bottom = Some(margin.into());
677        self
678    }
679
680    pub fn horiz(mut self, margin: impl Into<PxPctAuto>) -> Self {
681        let margin = margin.into();
682        self.left = Some(margin);
683        self.right = Some(margin);
684        self
685    }
686
687    pub fn vert(mut self, margin: impl Into<PxPctAuto>) -> Self {
688        let margin = margin.into();
689        self.top = Some(margin);
690        self.bottom = Some(margin);
691        self
692    }
693}
694
695impl StylePropValue for Margin {
696    fn debug_view(&self) -> Option<Box<dyn View>> {
697        let margin = *self;
698        let details_view = move || {
699            let sides = [
700                ("Left:", margin.left),
701                ("Top:", margin.top),
702                ("Right:", margin.right),
703                ("Bottom:", margin.bottom),
704            ];
705
706            Stack::vertical_from_iter(
707                sides
708                    .into_iter()
709                    .filter_map(|(l, v)| v.map(|v| (l, v)))
710                    .map(|(label, margin)| {
711                        Stack::horizontal((
712                            label.style(|s| s.font_weight(Weight::BOLD).width(80.0)),
713                            margin.debug_view().unwrap(),
714                        ))
715                        .style(|s| s.items_center().gap(4.0))
716                    }),
717            )
718            .style(|s| s.gap(4.0).padding(8.0))
719        };
720        Some(details_view().into_any())
721    }
722
723    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
724        Some(Self {
725            left: self.left.interpolate(&other.left, value)?,
726            top: self.top.interpolate(&other.top, value)?,
727            right: self.right.interpolate(&other.right, value)?,
728            bottom: self.bottom.interpolate(&other.bottom, value)?,
729        })
730    }
731
732    fn combine(&self, other: &Self) -> CombineResult<Self> {
733        let result = Margin {
734            left: other.left.or(self.left),
735            top: other.top.or(self.top),
736            right: other.right.or(self.right),
737            bottom: other.bottom.or(self.bottom),
738        };
739
740        if result == *other {
741            CombineResult::Other
742        } else {
743            CombineResult::New(result)
744        }
745    }
746}
747
748// Simple StylePropValue implementations for enums
749impl StylePropValue for CursorStyle {}
750impl StylePropValue for TextOverflow {}
751impl StylePropValue for PointerEvents {}
752
753impl StylePropValue for BoxShadow {
754    fn debug_view(&self) -> Option<Box<dyn View>> {
755        // Create a preview container that shows a visual representation of the shadow
756        let shadow = *self;
757
758        // Shadow preview box
759        let shadow_preview =
760            ().style(move |s| s.width(50.0).height(50.0))
761                .container()
762                .style(move |s| {
763                    s.with_theme(|s, t| {
764                        s.background(Color::TRANSPARENT)
765                            .border_color(t.border())
766                            .border(1.)
767                            .border_radius(t.border_radius())
768                    })
769                    .apply_box_shadows(vec![shadow])
770                    .margin(10.0)
771                });
772
773        // Create a details section showing the shadow properties
774        let details_view = move || {
775            Stack::vertical((
776                Stack::horizontal((
777                    "Color:".style(|s| s.font_weight(Weight::BOLD).width(80.0)),
778                    shadow.color.debug_view().unwrap(),
779                ))
780                .style(|s| s.items_center().gap(4.0)),
781                Stack::horizontal((
782                    "Blur:".style(|s| s.font_weight(Weight::BOLD).width(80.0)),
783                    format!("{:?}", shadow.blur_radius),
784                ))
785                .style(|s| s.items_center().gap(4.0)),
786                Stack::horizontal((
787                    "Spread:".style(|s| s.font_weight(Weight::BOLD).width(80.0)),
788                    format!("{:?}", shadow.spread),
789                ))
790                .style(|s| s.items_center().gap(4.0)),
791                Stack::horizontal((
792                    "Offset:".style(|s| s.font_weight(Weight::BOLD).width(80.0)),
793                    format!(
794                        "L: {:?}, R: {:?}, T: {:?}, B: {:?}",
795                        shadow.left_offset,
796                        shadow.right_offset,
797                        shadow.top_offset,
798                        shadow.bottom_offset
799                    ),
800                ))
801                .style(|s| s.items_center().gap(4.0)),
802            ))
803            .style(|s| s.gap(4.0).padding(8.0))
804        };
805
806        // Combine preview and details
807        let view = shadow_preview.tooltip(details_view);
808
809        Some(view.into_any())
810    }
811
812    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
813        Some(Self {
814            blur_radius: self
815                .blur_radius
816                .interpolate(&other.blur_radius, value)
817                .unwrap(),
818            color: self.color.interpolate(&other.color, value).unwrap(),
819            spread: self.spread.interpolate(&other.spread, value).unwrap(),
820            left_offset: self
821                .left_offset
822                .interpolate(&other.left_offset, value)
823                .unwrap(),
824            right_offset: self
825                .right_offset
826                .interpolate(&other.right_offset, value)
827                .unwrap(),
828            top_offset: self
829                .top_offset
830                .interpolate(&other.top_offset, value)
831                .unwrap(),
832            bottom_offset: self
833                .bottom_offset
834                .interpolate(&other.bottom_offset, value)
835                .unwrap(),
836        })
837    }
838}