Skip to main content

floem/style/
values.rs

1//! Core style property value trait and implementations.
2
3use floem_reactive::{RwSignal, SignalGet, SignalUpdate as _};
4use floem_renderer::Renderer;
5use floem_renderer::text::{LineHeightValue, Weight};
6use peniko::color::{HueDirection, palette};
7use peniko::kurbo::{self, Point, Stroke};
8use peniko::{
9    Brush, Color, ColorStop, ColorStops, Gradient, GradientKind, InterpolationAlphaSpace,
10    LinearGradientPosition,
11};
12use smallvec::SmallVec;
13use std::fmt::Debug;
14use taffy::GridTemplateComponent;
15use taffy::prelude::{auto, fr};
16
17#[cfg(not(target_arch = "wasm32"))]
18use std::time::Duration;
19#[cfg(target_arch = "wasm32")]
20use web_time::Duration;
21
22use taffy::style::{
23    AlignContent, AlignItems, BoxSizing, Display, FlexDirection, FlexWrap, Overflow, Position,
24};
25use taffy::{
26    geometry::{MinMax, Size},
27    prelude::{GridPlacement, Line},
28    style::{LengthPercentage, MaxTrackSizingFunction, MinTrackSizingFunction},
29};
30
31use crate::AnyView;
32use crate::prelude::ViewTuple;
33use crate::theme::StyleThemeExt;
34use crate::unit::{Pct, Px, PxPct, PxPctAuto};
35use crate::view::ViewTupleFlat;
36use crate::view::{IntoView, View};
37use crate::views::{ContainerExt, Decorators, Label, Stack, TooltipExt, canvas};
38
39use super::FontSize;
40
41pub enum CombineResult<T> {
42    Other,  // The result is semantically `other` - caller can reuse it
43    New(T), // A new value was created
44}
45
46pub trait StylePropValue: Clone + PartialEq + Debug {
47    fn debug_view(&self) -> Option<Box<dyn View>> {
48        None
49    }
50
51    fn interpolate(&self, _other: &Self, _value: f64) -> Option<Self> {
52        None
53    }
54
55    fn combine(&self, _other: &Self) -> CombineResult<Self> {
56        CombineResult::Other
57    }
58
59    /// Compute a content-based hash for this value.
60    ///
61    /// This hash is used for style caching - identical values should produce
62    /// identical hashes. The default implementation uses the Debug representation,
63    /// which works for most types. Types that implement Hash can override this
64    /// for better performance.
65    fn content_hash(&self) -> u64 {
66        use std::hash::{Hash, Hasher};
67        let mut hasher = rustc_hash::FxHasher::default();
68        // Use Debug representation as a stable string for hashing
69        let debug_str = format!("{:?}", self);
70        debug_str.hash(&mut hasher);
71        hasher.finish()
72    }
73}
74
75impl StylePropValue for i32 {
76    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
77        Some((*self as f64 + (*other as f64 - *self as f64) * value).round() as i32)
78    }
79}
80impl StylePropValue for bool {}
81impl StylePropValue for f32 {
82    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
83        Some(*self * (1.0 - value as f32) + *other * value as f32)
84    }
85}
86impl StylePropValue for u16 {
87    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
88        Some((*self as f64 + (*other as f64 - *self as f64) * value).round() as u16)
89    }
90}
91impl StylePropValue for usize {
92    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
93        Some((*self as f64 + (*other as f64 - *self as f64) * value).round() as usize)
94    }
95}
96impl StylePropValue for f64 {
97    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
98        Some(*self * (1.0 - value) + *other * value)
99    }
100}
101impl StylePropValue for Overflow {}
102impl StylePropValue for Display {}
103impl StylePropValue for Position {}
104impl StylePropValue for FlexDirection {}
105impl StylePropValue for FlexWrap {}
106impl StylePropValue for AlignItems {}
107impl StylePropValue for BoxSizing {}
108impl StylePropValue for AlignContent {}
109impl StylePropValue for GridTemplateComponent<String> {}
110impl StylePropValue for MinTrackSizingFunction {}
111impl StylePropValue for MaxTrackSizingFunction {}
112impl<T: StylePropValue, M: StylePropValue> StylePropValue for MinMax<T, M> {}
113impl<T: StylePropValue> StylePropValue for Line<T> {}
114impl StylePropValue for taffy::GridAutoFlow {}
115impl StylePropValue for GridPlacement {}
116
117impl<A: smallvec::Array> StylePropValue for SmallVec<A>
118where
119    <A as smallvec::Array>::Item: StylePropValue,
120{
121    fn debug_view(&self) -> Option<Box<dyn View>> {
122        if self.is_empty() {
123            return Some(
124                Label::new("smallvec\n[]")
125                    .style(|s| s.with_theme(|s, t| s.color(t.text_muted())))
126                    .into_any(),
127            );
128        }
129
130        let count = self.len();
131        let is_spilled = self.spilled();
132
133        // Create a preview that shows count and whether it has spilled to heap
134        let preview = Label::derived(move || {
135            if is_spilled {
136                format!("smallvec\n[{}] (heap)", count)
137            } else {
138                format!("smallvec\n[{}] (inline)", count)
139            }
140        })
141        .style(|s| {
142            s.padding(2.0)
143                .padding_horiz(6.0)
144                .items_center()
145                .justify_center()
146                .text_align(floem_renderer::text::Align::Center)
147                .border(1.)
148                .border_radius(5.0)
149                .margin_left(6.0)
150                .with_theme(|s, t| s.color(t.text()).border_color(t.border()))
151                .with_context_opt::<FontSize, _>(|s, fs| s.font_size(fs * 0.85))
152        });
153
154        // Clone items for the tooltip view
155        let items = self.clone();
156
157        let tooltip_view = move || {
158            Stack::vertical_from_iter(items.iter().enumerate().map(|(i, item)| {
159                let index_label = Label::new(format!("[{}]", i))
160                    .style(|s| s.with_theme(|s, t| s.color(t.text_muted())));
161
162                let item_view = item.debug_view().unwrap_or_else(|| {
163                    Label::new(format!("{:?}", item))
164                        .style(|s| s.flex_grow(1.0))
165                        .into_any()
166                });
167
168                Stack::new((index_label, item_view))
169                    .style(|s| s.items_center().gap(8.0).padding(4.0))
170            }))
171            .style(|s| s.gap(4.0))
172        };
173
174        // Return the tooltip view wrapped in the preview
175        Some(
176            Stack::new((preview, tooltip_view()))
177                .style(|s| s.gap(8.0))
178                .into_any(),
179        )
180    }
181
182    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
183        self.iter().zip(other.iter()).try_fold(
184            SmallVec::with_capacity(self.len()),
185            |mut acc, (v1, v2)| {
186                if let Some(interpolated) = v1.interpolate(v2, value) {
187                    acc.push(interpolated);
188                    Some(acc)
189                } else {
190                    None
191                }
192            },
193        )
194    }
195}
196impl StylePropValue for String {}
197impl StylePropValue for Weight {
198    fn debug_view(&self) -> Option<Box<dyn View>> {
199        let clone = *self;
200        Some(
201            format!("{clone:?}")
202                .style(move |s| s.font_weight(clone))
203                .into_any(),
204        )
205    }
206    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
207        self.0.interpolate(&other.0, value).map(Weight)
208    }
209}
210impl StylePropValue for crate::text::Style {
211    fn debug_view(&self) -> Option<Box<dyn View>> {
212        let clone = *self;
213        Some(
214            format!("{clone:?}")
215                .style(move |s| s.font_style(clone))
216                .into_any(),
217        )
218    }
219}
220impl StylePropValue for crate::text::Align {}
221impl StylePropValue for LineHeightValue {
222    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
223        match (self, other) {
224            (LineHeightValue::Normal(v1), LineHeightValue::Normal(v2)) => {
225                v1.interpolate(v2, value).map(LineHeightValue::Normal)
226            }
227            (LineHeightValue::Px(v1), LineHeightValue::Px(v2)) => {
228                v1.interpolate(v2, value).map(LineHeightValue::Px)
229            }
230            _ => None,
231        }
232    }
233}
234impl StylePropValue for Size<LengthPercentage> {}
235
236impl<T: StylePropValue> StylePropValue for Option<T> {
237    fn debug_view(&self) -> Option<Box<dyn View>> {
238        self.as_ref().and_then(|v| v.debug_view())
239    }
240
241    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
242        self.as_ref().and_then(|this| {
243            other
244                .as_ref()
245                .and_then(|other| this.interpolate(other, value).map(Some))
246        })
247    }
248}
249impl<T: StylePropValue + 'static> StylePropValue for Vec<T> {
250    fn debug_view(&self) -> Option<Box<dyn View>> {
251        if self.is_empty() {
252            return Some(
253                Label::new("[]")
254                    .style(|s| s.with_theme(|s, t| s.color(t.text_muted())))
255                    .into_any(),
256            );
257        }
258
259        let count = self.len();
260        let _preview = Label::derived(move || format!("[{}]", count)).style(|s| {
261            s.padding(2.0)
262                .padding_horiz(6.0)
263                .border(1.)
264                .border_radius(5.0)
265                .margin_left(6.0)
266                .with_theme(|s, t| s.color(t.text()).border_color(t.border()))
267                .with_context_opt::<FontSize, _>(|s, fs| s.font_size(fs * 0.85))
268        });
269
270        let items = self.clone();
271        let tooltip_view = move || {
272            Stack::vertical_from_iter(items.iter().enumerate().map(|(i, item)| {
273                let index_label = Label::new(format!("[{}]", i))
274                    .style(|s| s.with_theme(|s, t| s.color(t.text_muted())));
275
276                let item_view = item.debug_view().unwrap_or_else(|| {
277                    Label::new(format!("{:?}", item))
278                        .style(|s| s.flex_grow(1.0))
279                        .into_any()
280                });
281
282                Stack::new((index_label, item_view))
283                    .style(|s| s.items_center().gap(8.0).padding(4.0))
284            }))
285            .style(|s| s.gap(4.0))
286        };
287
288        Some(
289            // preview
290            tooltip_view().into_any(),
291        )
292    }
293
294    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
295        self.iter().zip(other.iter()).try_fold(
296            Vec::with_capacity(self.len()),
297            |mut acc, (v1, v2)| {
298                if let Some(interpolated) = v1.interpolate(v2, value) {
299                    acc.push(interpolated);
300                    Some(acc)
301                } else {
302                    None
303                }
304            },
305        )
306    }
307}
308impl StylePropValue for Px {
309    fn debug_view(&self) -> Option<Box<dyn View>> {
310        Some(Label::new(format!("{} px", self.0)).into_any())
311    }
312    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
313        self.0.interpolate(&other.0, value).map(Px)
314    }
315}
316impl StylePropValue for Pct {
317    fn debug_view(&self) -> Option<Box<dyn View>> {
318        Some(Label::new(format!("{}%", self.0)).into_any())
319    }
320    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
321        self.0.interpolate(&other.0, value).map(Pct)
322    }
323}
324impl StylePropValue for PxPctAuto {
325    fn debug_view(&self) -> Option<Box<dyn View>> {
326        let label = match self {
327            Self::Px(v) => format!("{v} px"),
328            Self::Pct(v) => format!("{v}%"),
329            Self::Auto => "auto".to_string(),
330        };
331        Some(Label::new(label).into_any())
332    }
333    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
334        match (self, other) {
335            (Self::Px(v1), Self::Px(v2)) => Some(Self::Px(v1 + (v2 - v1) * value)),
336            (Self::Pct(v1), Self::Pct(v2)) => Some(Self::Pct(v1 + (v2 - v1) * value)),
337            (Self::Auto, Self::Auto) => Some(Self::Auto),
338            // TODO: Figure out some way to get in the relevant layout information in order to interpolate between pixels and percent
339            _ => None,
340        }
341    }
342}
343impl StylePropValue for PxPct {
344    fn debug_view(&self) -> Option<Box<dyn View>> {
345        let label = match self {
346            Self::Px(v) => format!("{v} px"),
347            Self::Pct(v) => format!("{v}%"),
348        };
349        Some(Label::new(label).into_any())
350    }
351
352    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
353        match (self, other) {
354            (Self::Px(v1), Self::Px(v2)) => Some(Self::Px(v1 + (v2 - v1) * value)),
355            (Self::Pct(v1), Self::Pct(v2)) => Some(Self::Pct(v1 + (v2 - v1) * value)),
356            // TODO: Figure out some way to get in the relevant layout information in order to interpolate between pixels and percent
357            _ => None,
358        }
359    }
360}
361
362pub(crate) fn views(views: impl ViewTuple) -> Vec<AnyView> {
363    views.into_views()
364}
365
366impl StylePropValue for Color {
367    fn debug_view(&self) -> Option<Box<dyn View>> {
368        let color = *self;
369        let swatch = ()
370            .style(move |s| {
371                s.background(color)
372                    .width(22.0)
373                    .height(14.0)
374                    .border(1.)
375                    .border_color(palette::css::WHITE.with_alpha(0.5))
376                    .border_radius(5.0)
377            })
378            .container()
379            .style(|s| {
380                s.border(1.)
381                    .border_color(palette::css::BLACK.with_alpha(0.5))
382                    .border_radius(5.0)
383            });
384
385        let tooltip_view = move || {
386            // Convert to RGBA8 for standard representations
387            let c = color.to_rgba8();
388            let (r, g, b, a) = (c.r, c.g, c.b, c.a);
389
390            // Hex representation
391            let hex = if a == 255 {
392                format!("#{:02X}{:02X}{:02X}", r, g, b)
393            } else {
394                format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a)
395            };
396
397            // RGBA string
398            let rgba_str = format!("rgba({}, {}, {}, {:.3})", r, g, b, a as f32 / 255.0);
399
400            // Alpha percentage
401            let alpha_str = format!(
402                "{:.1}% ({:.3})",
403                (a as f32 / 255.0) * 100.0,
404                a as f32 / 255.0
405            );
406
407            let components = color.components;
408            let color_space_str = format!("{:?}", color.cs);
409
410            let hex = views((
411                "Hex:".style(|s| s.font_bold().min_width(80.0).justify_end()),
412                Label::derived(move || hex.clone()),
413            ));
414            let rgba = views((
415                "RGBA:".style(|s| s.font_bold().min_width(80.0).justify_end()),
416                Label::derived(move || rgba_str.clone()),
417            ));
418            let components = views((
419                "Components:".style(|s| s.font_bold().min_width(80.0).justify_end()),
420                (
421                    Label::derived(move || format!("[0]: {:.3}", components[0])),
422                    Label::derived(move || format!("[1]: {:.3}", components[1])),
423                    Label::derived(move || format!("[2]: {:.3}", components[2])),
424                    Label::derived(move || format!("[3]: {:.3}", components[3])),
425                )
426                    .v_stack()
427                    .style(|s| s.gap(2.0)),
428            ));
429            let color_space = views((
430                "Color Space:".style(|s| s.font_bold().min_width(80.0).justify_end()),
431                Label::derived(move || color_space_str.clone()),
432            ));
433            let alpha = views((
434                "Alpha:".style(|s| s.font_bold().min_width(80.0).justify_end()),
435                Label::derived(move || alpha_str.clone()),
436            ));
437            (hex, rgba, components, color_space, alpha)
438                .flatten()
439                .style(|s| {
440                    s.grid()
441                        .grid_template_columns([auto(), fr(1.)])
442                        .justify_center()
443                        .items_center()
444                        .row_gap(20)
445                        .col_gap(10)
446                        .padding(30)
447                })
448        };
449
450        Some(
451            swatch
452                .tooltip(tooltip_view)
453                .style(|s| s.items_center())
454                .into_any(),
455        )
456    }
457
458    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
459        Some(self.lerp(*other, value as f32, HueDirection::default()))
460    }
461}
462
463impl StylePropValue for Gradient {
464    fn debug_view(&self) -> Option<Box<dyn View>> {
465        let box_width = 22.;
466        let box_height = 14.;
467        let mut grad = self.clone();
468        grad.kind = match grad.kind {
469            GradientKind::Linear(LinearGradientPosition { start, end }) => {
470                let dx = end.x - start.x;
471                let dy = end.y - start.y;
472
473                let scale_x = box_width / dx.abs();
474                let scale_y = box_height / dy.abs();
475                let scale = scale_x.min(scale_y);
476
477                let new_dx = dx * scale;
478                let new_dy = dy * scale;
479
480                let new_start = Point {
481                    x: if dx > 0.0 { 0.0 } else { box_width },
482                    y: if dy > 0.0 { 0.0 } else { box_height },
483                };
484
485                let new_end = Point {
486                    x: new_start.x + new_dx,
487                    y: new_start.y + new_dy,
488                };
489
490                GradientKind::Linear(LinearGradientPosition {
491                    start: new_start,
492                    end: new_end,
493                })
494            }
495            _ => grad.kind,
496        };
497        let color = ().style(move |s| {
498            s.background(grad.clone())
499                .width(box_width)
500                .height(box_height)
501                .border(1.)
502                .border_color(palette::css::WHITE.with_alpha(0.5))
503                .border_radius(5.0)
504        });
505        let color = color.container().style(|s| {
506            s.border(1.)
507                .border_color(palette::css::BLACK.with_alpha(0.5))
508                .border_radius(5.0)
509                .margin_left(6.0)
510        });
511        Some(
512            Stack::new((Label::new(format!("{self:?}")), color))
513                .style(|s| s.items_center())
514                .into_any(),
515        )
516    }
517
518    fn interpolate(&self, _other: &Self, _value: f64) -> Option<Self> {
519        None
520    }
521}
522
523// this is necessary because Stroke doesn't impl partial eq. it probably should...
524#[derive(Clone, Debug, Default)]
525pub struct StrokeWrap(pub Stroke);
526impl StrokeWrap {
527    pub fn new(width: f64) -> Self {
528        Self(Stroke::new(width))
529    }
530}
531impl PartialEq for StrokeWrap {
532    fn eq(&self, other: &Self) -> bool {
533        let self_stroke = &self.0;
534        let other_stroke = &other.0;
535
536        self_stroke.width == other_stroke.width
537            && self_stroke.join == other_stroke.join
538            && self_stroke.miter_limit == other_stroke.miter_limit
539            && self_stroke.start_cap == other_stroke.start_cap
540            && self_stroke.end_cap == other_stroke.end_cap
541            && self_stroke.dash_pattern == other_stroke.dash_pattern
542            && self_stroke.dash_offset == other_stroke.dash_offset
543    }
544}
545impl From<Stroke> for StrokeWrap {
546    fn from(value: Stroke) -> Self {
547        Self(value)
548    }
549}
550impl From<f32> for StrokeWrap {
551    fn from(value: f32) -> Self {
552        Self(Stroke::new(value.into()))
553    }
554}
555impl From<f64> for StrokeWrap {
556    fn from(value: f64) -> Self {
557        Self(Stroke::new(value))
558    }
559}
560impl From<i32> for StrokeWrap {
561    fn from(value: i32) -> Self {
562        Self(Stroke::new(value.into()))
563    }
564}
565impl StylePropValue for StrokeWrap {
566    fn debug_view(&self) -> Option<Box<dyn View>> {
567        let stroke = self.0.clone();
568        let clone = stroke.clone();
569
570        let color = RwSignal::new(palette::css::RED);
571
572        // Visual preview of the stroke
573        let preview = canvas(move |cx, size| {
574            cx.stroke(
575                &kurbo::Line::new(
576                    Point::new(0., size.height / 2.),
577                    Point::new(size.width, size.height / 2.),
578                ),
579                color.get(),
580                &clone,
581            );
582        })
583        .style(move |s| s.width(80.0).height(20.0))
584        .container()
585        .style(move |s| {
586            s.with_theme(move |s, t| {
587                color.set(t.primary());
588                s.border_color(t.border())
589            })
590            .padding(4.0)
591        });
592
593        let tooltip_view = move || {
594            let stroke = stroke.clone();
595
596            let width_row = views((
597                "Width:".style(|s| s.font_bold().min_width(100.0).justify_end()),
598                Label::derived(move || format!("{:.1}px", stroke.width)),
599            ));
600
601            let join_row = views((
602                "Join:".style(|s| s.font_bold().min_width(100.0).justify_end()),
603                Label::derived(move || format!("{:?}", stroke.join)),
604            ));
605
606            let miter_row = views((
607                "Miter Limit:".style(|s| s.font_bold().min_width(100.0).justify_end()),
608                Label::derived(move || format!("{:.2}", stroke.miter_limit)),
609            ));
610
611            let start_cap_row = views((
612                "Start Cap:".style(|s| s.font_bold().min_width(100.0).justify_end()),
613                Label::derived(move || format!("{:?}", stroke.start_cap)),
614            ));
615
616            let end_cap_row = views((
617                "End Cap:".style(|s| s.font_bold().min_width(100.0).justify_end()),
618                Label::derived(move || format!("{:?}", stroke.end_cap)),
619            ));
620
621            let pattern_clone = stroke.dash_pattern.clone();
622
623            let dash_pattern_row = views((
624                "Dash Pattern:".style(|s| s.font_bold().min_width(100.0).justify_end()),
625                Label::derived(move || {
626                    if pattern_clone.is_empty() {
627                        "Solid".to_string()
628                    } else {
629                        format!("{:?}", pattern_clone.as_slice())
630                    }
631                }),
632            ));
633
634            let dash_offset_row = if !stroke.dash_pattern.is_empty() {
635                Some(views((
636                    "Dash Offset:".style(|s| s.font_bold().min_width(100.0).justify_end()),
637                    Label::derived(move || format!("{:.1}", stroke.dash_offset)),
638                )))
639            } else {
640                None
641            };
642
643            let mut rows = vec![
644                width_row.into_any(),
645                join_row.into_any(),
646                miter_row.into_any(),
647                start_cap_row.into_any(),
648                end_cap_row.into_any(),
649                dash_pattern_row.into_any(),
650            ];
651
652            if let Some(offset_row) = dash_offset_row {
653                rows.push(offset_row.into_any());
654            }
655
656            Stack::vertical_from_iter(rows).style(|s| {
657                s.grid()
658                    .grid_template_columns([auto(), fr(1.)])
659                    .justify_center()
660                    .items_center()
661                    .row_gap(12)
662                    .col_gap(10)
663                    .padding(20)
664            })
665        };
666
667        Some(
668            preview
669                .tooltip(tooltip_view)
670                .style(|s| s.items_center())
671                .into_any(),
672        )
673    }
674}
675impl StylePropValue for Brush {
676    fn debug_view(&self) -> Option<Box<dyn View>> {
677        match self {
678            Brush::Solid(color) => color.debug_view(),
679            Brush::Gradient(grad) => grad.debug_view(),
680            Brush::Image(_) => None,
681        }
682    }
683
684    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
685        match (self, other) {
686            (Brush::Solid(color), Brush::Solid(other)) => Some(Self::Solid(color.lerp(
687                *other,
688                value as f32,
689                HueDirection::default(),
690            ))),
691            (Brush::Gradient(gradient), Brush::Solid(solid)) => {
692                let interpolated_stops: Vec<ColorStop> = gradient
693                    .stops
694                    .iter()
695                    .map(|stop| {
696                        let interpolated_color = stop.color.to_alpha_color().lerp(
697                            *solid,
698                            value as f32,
699                            HueDirection::default(),
700                        );
701                        ColorStop::from((stop.offset, interpolated_color))
702                    })
703                    .collect();
704                Some(Brush::Gradient(Gradient {
705                    kind: gradient.kind,
706                    extend: gradient.extend,
707                    interpolation_cs: gradient.interpolation_cs,
708                    hue_direction: gradient.hue_direction,
709                    stops: ColorStops::from(&*interpolated_stops),
710                    interpolation_alpha_space: InterpolationAlphaSpace::Premultiplied,
711                }))
712            }
713            (Brush::Solid(solid), Brush::Gradient(gradient)) => {
714                let interpolated_stops: Vec<ColorStop> = gradient
715                    .stops
716                    .iter()
717                    .map(|stop| {
718                        let interpolated_color = solid.lerp(
719                            stop.color.to_alpha_color(),
720                            value as f32,
721                            HueDirection::default(),
722                        );
723                        ColorStop::from((stop.offset, interpolated_color))
724                    })
725                    .collect();
726                Some(Brush::Gradient(Gradient {
727                    kind: gradient.kind,
728                    extend: gradient.extend,
729                    interpolation_cs: gradient.interpolation_cs,
730                    hue_direction: gradient.hue_direction,
731                    stops: ColorStops::from(&*interpolated_stops),
732                    interpolation_alpha_space: InterpolationAlphaSpace::Premultiplied,
733                }))
734            }
735
736            (Brush::Gradient(gradient1), Brush::Gradient(gradient2)) => {
737                gradient1.interpolate(gradient2, value).map(Brush::Gradient)
738            }
739            _ => None,
740        }
741    }
742}
743impl StylePropValue for Duration {
744    fn debug_view(&self) -> Option<Box<dyn View>> {
745        None
746    }
747
748    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
749        self.as_secs_f64()
750            .interpolate(&other.as_secs_f64(), value)
751            .map(Duration::from_secs_f64)
752    }
753}
754
755impl StylePropValue for super::Angle {
756    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
757        let self_rad = self.to_radians();
758        let other_rad = other.to_radians();
759        self_rad
760            .interpolate(&other_rad, value)
761            .map(super::Angle::Rad)
762    }
763}
764
765impl StylePropValue for super::AnchorAbout {
766    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
767        Some(Self {
768            x: self.x + (other.x - self.x) * value,
769            y: self.y + (other.y - self.y) * value,
770        })
771    }
772}
773
774/// Internal storage for style property values in the style map.
775///
776/// Unlike `StyleValue<T>` which is used in the public API, `StyleMapValue<T>`
777/// is the internal representation stored in the style hashmap.
778#[derive(Debug, Clone, Copy, PartialEq, Eq)]
779pub enum StyleMapValue<T> {
780    /// Value inserted by animation interpolation
781    Animated(T),
782    /// Value set directly
783    Val(T),
784    /// Use the default value for the style, typically from the underlying `ComputedStyle`
785    Unset,
786}
787
788impl<T> StyleMapValue<T> {
789    pub(crate) fn as_ref(&self) -> Option<&T> {
790        match self {
791            Self::Val(v) => Some(v),
792            Self::Animated(v) => Some(v),
793            Self::Unset => None,
794        }
795    }
796}
797
798/// The value for a [`Style`] property in the public API.
799///
800/// This represents the result of reading a style property, with additional
801/// states like `Base` that indicate inheritance from parent styles.
802#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
803pub enum StyleValue<T> {
804    /// Value inserted by animation interpolation
805    Animated(T),
806    /// Value set directly
807    Val(T),
808    /// Use the default value for the style, typically from the underlying `ComputedStyle`.
809    Unset,
810    /// Use whatever the base style is. For an overriding style like hover, this uses the base
811    /// style. For the base style, this is equivalent to `Unset`.
812    #[default]
813    Base,
814}
815
816impl<T> StyleValue<T> {
817    pub fn map<U>(self, f: impl FnOnce(T) -> U) -> StyleValue<U> {
818        match self {
819            Self::Val(x) => StyleValue::Val(f(x)),
820            Self::Animated(x) => StyleValue::Animated(f(x)),
821            Self::Unset => StyleValue::Unset,
822            Self::Base => StyleValue::Base,
823        }
824    }
825
826    pub fn unwrap_or(self, default: T) -> T {
827        match self {
828            Self::Val(x) => x,
829            Self::Animated(x) => x,
830            Self::Unset => default,
831            Self::Base => default,
832        }
833    }
834
835    pub fn unwrap_or_else(self, f: impl FnOnce() -> T) -> T {
836        match self {
837            Self::Val(x) => x,
838            Self::Animated(x) => x,
839            Self::Unset => f(),
840            Self::Base => f(),
841        }
842    }
843
844    pub fn as_mut(&mut self) -> Option<&mut T> {
845        match self {
846            Self::Val(x) => Some(x),
847            Self::Animated(x) => Some(x),
848            Self::Unset => None,
849            Self::Base => None,
850        }
851    }
852}
853
854impl<T> From<T> for StyleValue<T> {
855    fn from(x: T) -> Self {
856        Self::Val(x)
857    }
858}