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::{FontWeight, LineHeightValue};
6use peniko::color::{HueDirection, palette};
7use peniko::kurbo::{self, Affine, Point, Stroke, Vec2};
8use peniko::{
9    Brush, Color, ColorStop, ColorStops, Gradient, GradientKind, InterpolationAlphaSpace,
10    LinearGradientPosition,
11};
12use smallvec::SmallVec;
13use std::collections::HashSet;
14use std::fmt::Debug;
15use std::rc::Rc;
16use taffy::GridTemplateComponent;
17use taffy::prelude::{auto, fr};
18
19#[cfg(not(target_arch = "wasm32"))]
20use std::time::Duration;
21#[cfg(target_arch = "wasm32")]
22use web_time::Duration;
23
24use taffy::style::{
25    AlignContent, AlignItems, BoxSizing, Display, FlexDirection, FlexWrap, Overflow, Position,
26};
27use taffy::{
28    geometry::{MinMax, Size},
29    prelude::{GridPlacement, Line},
30    style::{LengthPercentage, MaxTrackSizingFunction, MinTrackSizingFunction},
31};
32
33use crate::AnyView;
34use crate::prelude::ViewTuple;
35use crate::style::CursorStyle;
36use crate::theme::StyleThemeExt;
37use crate::theme::Theme;
38use crate::unit::{Length, LengthAuto, Pct, Pt};
39use crate::view::ViewTupleFlat;
40use crate::view::{IntoView, View};
41use crate::views::{
42    ButtonClass, ContainerExt, Decorators, Empty, Label, Stack, StackExt, TabSelectorClass,
43    TooltipExt, canvas, dyn_view, svg, tab,
44};
45
46use super::{
47    FontSize, ResponsiveSelectors, StructuralSelectors, Style, StyleDebugGroupInfo, StyleKey,
48    StyleKeyInfo, StylePropRef, Transition,
49};
50
51pub struct ContextValue<T> {
52    pub(crate) eval: Rc<dyn Fn(&Style) -> T>,
53}
54
55impl<T> Clone for ContextValue<T> {
56    fn clone(&self) -> Self {
57        Self {
58            eval: self.eval.clone(),
59        }
60    }
61}
62
63impl<T> std::fmt::Debug for ContextValue<T> {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        f.write_str("ContextValue(..)")
66    }
67}
68
69impl<T> PartialEq for ContextValue<T> {
70    fn eq(&self, other: &Self) -> bool {
71        Rc::ptr_eq(&self.eval, &other.eval)
72    }
73}
74
75impl<T> Eq for ContextValue<T> {}
76
77impl<T> ContextValue<T> {
78    pub(crate) fn new(eval: impl Fn(&Style) -> T + 'static) -> Self {
79        Self {
80            eval: Rc::new(eval),
81        }
82    }
83
84    pub fn resolve(&self, style: &Style) -> T {
85        floem_reactive::Runtime::with_effect(style.effect_context.clone(), || {
86            // todo use context
87            (self.eval)(style)
88        })
89    }
90
91    pub fn map<U>(self, f: impl Fn(T) -> U + 'static) -> ContextValue<U>
92    where
93        T: 'static,
94    {
95        let eval = self.eval;
96        ContextValue::new(move |style| f(eval(style)))
97    }
98}
99
100pub trait StylePropValue: Clone + PartialEq + Debug {
101    fn debug_view(&self) -> Option<Box<dyn View>> {
102        None
103    }
104
105    fn interpolate(&self, _other: &Self, _value: f64) -> Option<Self> {
106        None
107    }
108
109    /// Compute a content-based hash for this value.
110    ///
111    /// This hash is used for style caching - identical values should produce
112    /// identical hashes. The default implementation uses the Debug representation,
113    /// which works for most types but allocates. Types should override this
114    /// for better performance.
115    fn content_hash(&self) -> u64 {
116        use std::hash::{Hash, Hasher};
117        let mut hasher = rustc_hash::FxHasher::default();
118        let debug_str = format!("{:?}", self);
119        debug_str.hash(&mut hasher);
120        hasher.finish()
121    }
122}
123
124/// Hash a type that implements `Hash` using FxHasher.
125#[inline]
126fn hash_value<T: std::hash::Hash>(val: &T) -> u64 {
127    use std::hash::Hasher;
128    let mut h = rustc_hash::FxHasher::default();
129    val.hash(&mut h);
130    h.finish()
131}
132
133/// Hash an f64 by its bit representation.
134#[inline]
135fn hash_f64(val: f64) -> u64 {
136    use std::hash::{Hash, Hasher};
137    let mut h = rustc_hash::FxHasher::default();
138    val.to_bits().hash(&mut h);
139    h.finish()
140}
141
142/// Hash an f32 by its bit representation.
143#[inline]
144fn hash_f32(val: f32) -> u64 {
145    use std::hash::{Hash, Hasher};
146    let mut h = rustc_hash::FxHasher::default();
147    val.to_bits().hash(&mut h);
148    h.finish()
149}
150
151impl StylePropValue for i32 {
152    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
153        Some((*self as f64 + (*other as f64 - *self as f64) * value).round() as i32)
154    }
155    fn content_hash(&self) -> u64 {
156        hash_value(self)
157    }
158}
159impl StylePropValue for bool {
160    fn content_hash(&self) -> u64 {
161        hash_value(self)
162    }
163}
164impl StylePropValue for f32 {
165    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
166        Some(*self * (1.0 - value as f32) + *other * value as f32)
167    }
168    fn content_hash(&self) -> u64 {
169        hash_f32(*self)
170    }
171}
172impl StylePropValue for u16 {
173    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
174        Some((*self as f64 + (*other as f64 - *self as f64) * value).round() as u16)
175    }
176    fn content_hash(&self) -> u64 {
177        hash_value(self)
178    }
179}
180impl StylePropValue for usize {
181    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
182        Some((*self as f64 + (*other as f64 - *self as f64) * value).round() as usize)
183    }
184    fn content_hash(&self) -> u64 {
185        hash_value(self)
186    }
187}
188impl StylePropValue for f64 {
189    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
190        Some(*self * (1.0 - value) + *other * value)
191    }
192    fn content_hash(&self) -> u64 {
193        hash_f64(*self)
194    }
195}
196// Taffy enums — use discriminant-based hashing (no Hash derive available).
197// For simple enums with no data fields, discriminant alone is a perfect hash.
198// For enums with data, we combine discriminant with field hashes.
199
200impl StylePropValue for Overflow {
201    fn content_hash(&self) -> u64 {
202        hash_value(&std::mem::discriminant(self))
203    }
204}
205impl StylePropValue for Display {
206    fn content_hash(&self) -> u64 {
207        hash_value(&std::mem::discriminant(self))
208    }
209}
210impl StylePropValue for Position {
211    fn content_hash(&self) -> u64 {
212        hash_value(&std::mem::discriminant(self))
213    }
214}
215impl StylePropValue for FlexDirection {
216    fn content_hash(&self) -> u64 {
217        hash_value(&std::mem::discriminant(self))
218    }
219}
220impl StylePropValue for FlexWrap {
221    fn content_hash(&self) -> u64 {
222        hash_value(&std::mem::discriminant(self))
223    }
224}
225impl StylePropValue for AlignItems {
226    fn content_hash(&self) -> u64 {
227        hash_value(&std::mem::discriminant(self))
228    }
229}
230impl StylePropValue for BoxSizing {
231    fn content_hash(&self) -> u64 {
232        hash_value(&std::mem::discriminant(self))
233    }
234}
235impl StylePropValue for AlignContent {
236    fn content_hash(&self) -> u64 {
237        hash_value(&std::mem::discriminant(self))
238    }
239}
240impl StylePropValue for GridTemplateComponent<String> {}
241impl StylePropValue for MinTrackSizingFunction {}
242impl StylePropValue for MaxTrackSizingFunction {}
243impl<T: StylePropValue, M: StylePropValue> StylePropValue for MinMax<T, M> {
244    fn content_hash(&self) -> u64 {
245        hash_value(&(self.min.content_hash(), self.max.content_hash()))
246    }
247}
248impl<T: StylePropValue> StylePropValue for Line<T> {
249    fn content_hash(&self) -> u64 {
250        hash_value(&(self.start.content_hash(), self.end.content_hash()))
251    }
252}
253impl StylePropValue for taffy::GridAutoFlow {
254    fn content_hash(&self) -> u64 {
255        hash_value(&std::mem::discriminant(self))
256    }
257}
258impl StylePropValue for GridPlacement {}
259
260/// How the content of a replaced element, such as an img, should be resized to fit its container.
261/// Corresponds to the CSS `object-fit` property.
262/// See <https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit>.
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
264pub enum ObjectFit {
265    /// The replaced content is sized to fill the element's content box.
266    /// The entire object will completely fill the box.
267    /// If the object's aspect ratio does not match the aspect ratio of its box,
268    /// then the object will be stretched to fit.
269    #[default]
270    Fill,
271    /// The replaced content is scaled to maintain its aspect ratio while fitting
272    /// within the element's content box. The entire object is made to fill the box,
273    /// while preserving its aspect ratio, so the object will be "letterboxed"
274    /// if its aspect ratio does not match the aspect ratio of the box.
275    Contain,
276    /// The content is sized to maintain its aspect ratio while filling the element's
277    /// entire content box. If the object's aspect ratio does not match the aspect
278    /// ratio of its box, then the object will be clipped to fit.
279    Cover,
280    /// The content is sized as if none or contain were specified, whichever would
281    /// result in a smaller concrete object size.
282    ScaleDown,
283    /// The replaced content is not resized.
284    None,
285}
286
287/// Where the content of a replaced element should be positioned inside its container.
288/// Corresponds to common CSS `object-position` keyword combinations.
289#[derive(Debug, Clone, Copy, PartialEq, Default)]
290pub enum ObjectPosition {
291    /// Align content to the top-left corner.
292    TopLeft,
293    /// Align content to the top edge and center horizontally.
294    Top,
295    /// Align content to the top-right corner.
296    TopRight,
297    /// Align content to the left edge and center vertically.
298    Left,
299    /// Center content both horizontally and vertically.
300    #[default]
301    Center,
302    /// Align content to the right edge and center vertically.
303    Right,
304    /// Align content to the bottom-left corner.
305    BottomLeft,
306    /// Align content to the bottom edge and center horizontally.
307    Bottom,
308    /// Align content to the bottom-right corner.
309    BottomRight,
310    /// Position content using explicit horizontal and vertical offsets.
311    ///
312    /// Percentage values are resolved against the remaining free space on each axis,
313    /// matching CSS object-position behavior.
314    Custom(crate::style::Length, crate::style::Length),
315}
316
317impl StylePropValue for ObjectFit {
318    fn content_hash(&self) -> u64 {
319        hash_value(self)
320    }
321    fn debug_view(&self) -> Option<Box<dyn View>> {
322        use peniko::kurbo::RoundedRect;
323
324        let object_fit = *self;
325        let container_color = RwSignal::new(palette::css::GRAY);
326        let image_color = RwSignal::new(palette::css::BLUE);
327
328        // Visual preview showing how an image with 2:1 aspect ratio fits in a square container
329        let preview = canvas(move |cx, size| {
330            let width = size.width;
331            let height = size.height;
332            let padding = 4.0;
333            let container_size = width.min(height) - padding * 2.0;
334
335            // Draw container box (square)
336            let container_x = (width - container_size) / 2.0;
337            let container_y = (height - container_size) / 2.0;
338            let container_rect = RoundedRect::from_rect(
339                kurbo::Rect::new(
340                    container_x,
341                    container_y,
342                    container_x + container_size,
343                    container_y + container_size,
344                ),
345                2.0,
346            );
347            cx.stroke(
348                &container_rect,
349                container_color.get(),
350                &Stroke {
351                    width: 1.5,
352                    ..Default::default()
353                },
354            );
355
356            // Simulate an image with 2:1 aspect ratio (wider than tall)
357            let image_aspect = 2.0;
358            let (img_width, img_height) = match object_fit {
359                ObjectFit::Fill => {
360                    // Stretch to fill container
361                    (container_size, container_size)
362                }
363                ObjectFit::Contain => {
364                    // Fit inside while maintaining aspect ratio
365                    // Image is 2:1, container is 1:1, so width is the constraint
366                    let w = container_size;
367                    let h = w / image_aspect;
368                    (w, h)
369                }
370                ObjectFit::Cover => {
371                    // Cover entire container while maintaining aspect ratio
372                    // Height is the constraint
373                    let h = container_size;
374                    let w = h * image_aspect;
375                    (w, h)
376                }
377                ObjectFit::ScaleDown => {
378                    // Like contain but don't scale up
379                    // Assume natural image size is smaller than container
380                    let natural_w = container_size * 0.6;
381                    let natural_h = natural_w / image_aspect;
382                    (natural_w, natural_h)
383                }
384                ObjectFit::None => {
385                    // Natural size (simulated as 60% of container)
386                    let natural_w = container_size * 0.6;
387                    let natural_h = natural_w / image_aspect;
388                    (natural_w, natural_h)
389                }
390            };
391
392            // Center the image in the container
393            let img_x = container_x + (container_size - img_width) / 2.0;
394            let img_y = container_y + (container_size - img_height) / 2.0;
395
396            // Clip to container bounds for Cover mode
397            if matches!(object_fit, ObjectFit::Cover) {
398                // Draw the image rect (it will extend beyond container)
399                let img_rect = RoundedRect::from_rect(
400                    kurbo::Rect::new(img_x, img_y, img_x + img_width, img_y + img_height),
401                    2.0,
402                );
403                // Show it as semi-transparent to indicate it's clipped
404                let clipped_color = image_color.get().with_alpha(0.7);
405                cx.fill(&img_rect, clipped_color, 0.0);
406            } else {
407                // Draw the image rect normally
408                let img_rect = RoundedRect::from_rect(
409                    kurbo::Rect::new(img_x, img_y, img_x + img_width, img_y + img_height),
410                    2.0,
411                );
412                cx.fill(&img_rect, image_color.get(), 0.0);
413            }
414        })
415        .style(|s| s.width(70.0).height(70.0))
416        .container()
417        .style(move |s| {
418            s.padding(4.0)
419                .border(1.)
420                .border_radius(5.0)
421                .with_theme(move |s, t| s.border_color(t.border()))
422        });
423
424        let label_text = match object_fit {
425            ObjectFit::Fill => "Fill",
426            ObjectFit::Contain => "Contain",
427            ObjectFit::Cover => "Cover",
428            ObjectFit::ScaleDown => "ScaleDown",
429            ObjectFit::None => "None",
430        };
431
432        let tooltip_view = move || {
433            let description = match object_fit {
434                ObjectFit::Fill => "Stretches content to fill the box.\nMay distort aspect ratio.",
435                ObjectFit::Contain => {
436                    "Scales content to fit inside the box.\nPreserves aspect ratio (letterboxed)."
437                }
438                ObjectFit::Cover => {
439                    "Scales content to cover the box.\nPreserves aspect ratio (may clip)."
440                }
441                ObjectFit::ScaleDown => {
442                    "Like 'contain' but won't scale up.\nNever larger than natural size."
443                }
444                ObjectFit::None => {
445                    "Content keeps its natural size.\nMay overflow or be smaller than box."
446                }
447            };
448
449            Stack::vertical((
450                Label::new(label_text).style(|s| s.font_bold()),
451                Label::new(description).style(|s| s.with_theme(|s, t| s.color(t.text_muted()))),
452            ))
453            .style(|s| s.gap(8.0).padding(12.0).max_width(220.0))
454        };
455
456        Some(
457            preview
458                .tooltip(tooltip_view)
459                .style(|s| s.items_center())
460                .into_any(),
461        )
462    }
463}
464
465impl StylePropValue for ObjectPosition {
466    fn content_hash(&self) -> u64 {
467        hash_value(&std::mem::discriminant(self))
468    }
469    fn debug_view(&self) -> Option<Box<dyn View>> {
470        use peniko::kurbo::{Circle, RoundedRect};
471
472        let object_position = *self;
473        let container_color = RwSignal::new(palette::css::GRAY);
474        let image_color = RwSignal::new(palette::css::BLUE);
475        let marker_color = RwSignal::new(palette::css::RED);
476
477        let preview = canvas(move |cx, size| {
478            let width = size.width;
479            let height = size.height;
480            let padding = 6.0;
481            let container_size = width.min(height) - padding * 2.0;
482            let container_x = (width - container_size) / 2.0;
483            let container_y = (height - container_size) / 2.0;
484            let container_rect = RoundedRect::from_rect(
485                kurbo::Rect::new(
486                    container_x,
487                    container_y,
488                    container_x + container_size,
489                    container_y + container_size,
490                ),
491                2.0,
492            );
493            cx.stroke(
494                &container_rect,
495                container_color.get(),
496                &Stroke {
497                    width: 1.5,
498                    ..Default::default()
499                },
500            );
501
502            let image_w = container_size * 0.55;
503            let image_h = container_size * 0.35;
504            let free_x = container_size - image_w;
505            let free_y = container_size - image_h;
506            let font_size_cx = crate::style::FontSizeCx::new(16.0, 16.0);
507
508            let (offset_x, offset_y) = match object_position {
509                ObjectPosition::TopLeft => (0.0, 0.0),
510                ObjectPosition::Top => (free_x * 0.5, 0.0),
511                ObjectPosition::TopRight => (free_x, 0.0),
512                ObjectPosition::Left => (0.0, free_y * 0.5),
513                ObjectPosition::Center => (free_x * 0.5, free_y * 0.5),
514                ObjectPosition::Right => (free_x, free_y * 0.5),
515                ObjectPosition::BottomLeft => (0.0, free_y),
516                ObjectPosition::Bottom => (free_x * 0.5, free_y),
517                ObjectPosition::BottomRight => (free_x, free_y),
518                ObjectPosition::Custom(x, y) => (
519                    x.resolve(free_x, &font_size_cx),
520                    y.resolve(free_y, &font_size_cx),
521                ),
522            };
523
524            let img_x = container_x + offset_x;
525            let img_y = container_y + offset_y;
526            let img_rect = RoundedRect::from_rect(
527                kurbo::Rect::new(img_x, img_y, img_x + image_w, img_y + image_h),
528                2.0,
529            );
530            cx.fill(&img_rect, image_color.get(), 0.0);
531
532            let marker = Circle::new(
533                kurbo::Point::new(img_x + image_w / 2.0, img_y + image_h / 2.0),
534                2.5,
535            );
536            cx.fill(&marker, marker_color.get(), 0.0);
537        })
538        .style(|s| s.width(70.0).height(70.0))
539        .container()
540        .style(move |s| {
541            s.padding(4.0)
542                .border(1.)
543                .border_radius(5.0)
544                .with_theme(move |s, t| s.border_color(t.border()))
545        });
546
547        let (label_text, description) = match object_position {
548            ObjectPosition::TopLeft => ("TopLeft", "Anchors content to the top-left corner."),
549            ObjectPosition::Top => (
550                "Top",
551                "Anchors content to the top edge, centered horizontally.",
552            ),
553            ObjectPosition::TopRight => ("TopRight", "Anchors content to the top-right corner."),
554            ObjectPosition::Left => (
555                "Left",
556                "Anchors content to the left edge, centered vertically.",
557            ),
558            ObjectPosition::Center => ("Center", "Centers content on both axes."),
559            ObjectPosition::Right => (
560                "Right",
561                "Anchors content to the right edge, centered vertically.",
562            ),
563            ObjectPosition::BottomLeft => {
564                ("BottomLeft", "Anchors content to the bottom-left corner.")
565            }
566            ObjectPosition::Bottom => (
567                "Bottom",
568                "Anchors content to the bottom edge, centered horizontally.",
569            ),
570            ObjectPosition::BottomRight => {
571                ("BottomRight", "Anchors content to the bottom-right corner.")
572            }
573            ObjectPosition::Custom(x, y) => {
574                let label = format!("Custom({x:?}, {y:?})");
575                let description = "Uses explicit horizontal and vertical offsets. Percentages resolve against remaining free space.";
576                return Some(
577                    preview
578                        .tooltip(move || {
579                            Stack::vertical((
580                                Label::new(label.clone()).style(|s| s.font_bold()),
581                                Label::new(description)
582                                    .style(|s| s.with_theme(|s, t| s.color(t.text_muted()))),
583                            ))
584                            .style(|s| s.gap(8.0).padding(12.0).max_width(240.0))
585                        })
586                        .style(|s| s.items_center())
587                        .into_any(),
588                );
589            }
590        };
591
592        Some(
593            preview
594                .tooltip(move || {
595                    Stack::vertical((
596                        Label::new(label_text).style(|s| s.font_bold()),
597                        Label::new(description)
598                            .style(|s| s.with_theme(|s, t| s.color(t.text_muted()))),
599                    ))
600                    .style(|s| s.gap(8.0).padding(12.0).max_width(240.0))
601                })
602                .style(|s| s.items_center())
603                .into_any(),
604        )
605    }
606}
607
608impl<A: smallvec::Array> StylePropValue for SmallVec<A>
609where
610    <A as smallvec::Array>::Item: StylePropValue,
611{
612    fn debug_view(&self) -> Option<Box<dyn View>> {
613        if self.is_empty() {
614            return Some(
615                Label::new("smallvec\n[]")
616                    .style(|s| s.with_theme(|s, t| s.color(t.text_muted())))
617                    .into_any(),
618            );
619        }
620
621        let count = self.len();
622        let is_spilled = self.spilled();
623
624        // Create a preview that shows count and whether it has spilled to heap
625        let preview = Label::derived(move || {
626            if is_spilled {
627                format!("smallvec\n[{}] (heap)", count)
628            } else {
629                format!("smallvec\n[{}] (inline)", count)
630            }
631        })
632        .style(|s| {
633            s.padding(2.0)
634                .padding_horiz(6.0)
635                .items_center()
636                .justify_center()
637                .text_align(parley::Alignment::Center)
638                .border(1.)
639                .border_radius(5.0)
640                .margin_left(6.0)
641                .with_theme(|s, t| s.color(t.text()).border_color(t.border()))
642                .with::<FontSize>(|s, fs| s.font_size(fs.def(|fs| fs * 0.85)))
643        });
644
645        // Clone items for the tooltip view
646        let items = self.clone();
647
648        let tooltip_view = move || {
649            Stack::vertical_from_iter(items.iter().enumerate().map(|(i, item)| {
650                let index_label = Label::new(format!("[{}]", i))
651                    .style(|s| s.with_theme(|s, t| s.color(t.text_muted())));
652
653                let item_view = item.debug_view().unwrap_or_else(|| {
654                    Label::new(format!("{:?}", item))
655                        .style(|s| s.flex_grow(1.0))
656                        .into_any()
657                });
658
659                Stack::new((index_label, item_view))
660                    .style(|s| s.items_center().gap(8.0).padding(4.0))
661            }))
662            .style(|s| s.gap(4.0))
663        };
664
665        // Return the tooltip view wrapped in the preview
666        Some(
667            Stack::new((preview, tooltip_view()))
668                .style(|s| s.gap(8.0))
669                .into_any(),
670        )
671    }
672
673    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
674        self.iter().zip(other.iter()).try_fold(
675            SmallVec::with_capacity(self.len()),
676            |mut acc, (v1, v2)| {
677                if let Some(interpolated) = v1.interpolate(v2, value) {
678                    acc.push(interpolated);
679                    Some(acc)
680                } else {
681                    None
682                }
683            },
684        )
685    }
686
687    fn content_hash(&self) -> u64 {
688        use std::hash::{Hash, Hasher};
689        let mut h = rustc_hash::FxHasher::default();
690        self.len().hash(&mut h);
691        for item in self.iter() {
692            item.content_hash().hash(&mut h);
693        }
694        h.finish()
695    }
696}
697impl StylePropValue for String {
698    fn content_hash(&self) -> u64 {
699        hash_value(self)
700    }
701}
702impl StylePropValue for FontWeight {
703    fn content_hash(&self) -> u64 {
704        hash_f32(self.value())
705    }
706    fn debug_view(&self) -> Option<Box<dyn View>> {
707        let clone = *self;
708        Some(
709            format!("{clone:?}")
710                .style(move |s| s.font_weight(clone))
711                .into_any(),
712        )
713    }
714    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
715        self.value()
716            .interpolate(&other.value(), value)
717            .map(FontWeight::new)
718    }
719}
720impl StylePropValue for crate::text::FontStyle {
721    fn content_hash(&self) -> u64 {
722        hash_value(&std::mem::discriminant(self))
723    }
724    fn debug_view(&self) -> Option<Box<dyn View>> {
725        let clone = *self;
726        Some(
727            format!("{clone:?}")
728                .style(move |s| s.font_style(clone))
729                .into_any(),
730        )
731    }
732}
733impl StylePropValue for crate::text::Alignment {
734    fn content_hash(&self) -> u64 {
735        hash_value(&std::mem::discriminant(self))
736    }
737}
738impl StylePropValue for LineHeightValue {
739    fn content_hash(&self) -> u64 {
740        use std::hash::{Hash, Hasher};
741        let mut h = rustc_hash::FxHasher::default();
742        std::mem::discriminant(self).hash(&mut h);
743        match self {
744            LineHeightValue::Normal(v) => v.to_bits().hash(&mut h),
745            LineHeightValue::Pt(v) => v.to_bits().hash(&mut h),
746        }
747        h.finish()
748    }
749    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
750        match (self, other) {
751            (LineHeightValue::Normal(v1), LineHeightValue::Normal(v2)) => {
752                v1.interpolate(v2, value).map(LineHeightValue::Normal)
753            }
754            (LineHeightValue::Pt(v1), LineHeightValue::Pt(v2)) => {
755                v1.interpolate(v2, value).map(LineHeightValue::Pt)
756            }
757            _ => None,
758        }
759    }
760}
761impl StylePropValue for Size<LengthPercentage> {}
762
763impl<T: StylePropValue> StylePropValue for Option<T> {
764    fn debug_view(&self) -> Option<Box<dyn View>> {
765        self.as_ref().and_then(|v| v.debug_view())
766    }
767
768    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
769        self.as_ref().and_then(|this| {
770            other
771                .as_ref()
772                .and_then(|other| this.interpolate(other, value).map(Some))
773        })
774    }
775
776    fn content_hash(&self) -> u64 {
777        use std::hash::{Hash, Hasher};
778        let mut h = rustc_hash::FxHasher::default();
779        match self {
780            Some(v) => {
781                1u8.hash(&mut h);
782                v.content_hash().hash(&mut h);
783            }
784            None => 0u8.hash(&mut h),
785        }
786        h.finish()
787    }
788}
789impl<T: StylePropValue + 'static> StylePropValue for Vec<T> {
790    fn debug_view(&self) -> Option<Box<dyn View>> {
791        if self.is_empty() {
792            return Some(
793                Label::new("[]")
794                    .style(|s| s.with_theme(|s, t| s.color(t.text_muted())))
795                    .into_any(),
796            );
797        }
798
799        let count = self.len();
800        let _preview = Label::derived(move || format!("[{}]", count)).style(|s| {
801            s.padding(2.0)
802                .padding_horiz(6.0)
803                .border(1.)
804                .border_radius(5.0)
805                .margin_left(6.0)
806                .with_theme(|s, t| s.color(t.text()).border_color(t.border()))
807                .with::<FontSize>(|s, fs| s.font_size(fs.def(|fs| fs * 0.85)))
808        });
809
810        let items = self.clone();
811        let tooltip_view = move || {
812            Stack::vertical_from_iter(items.iter().enumerate().map(|(i, item)| {
813                let index_label = Label::new(format!("[{}]", i))
814                    .style(|s| s.with_theme(|s, t| s.color(t.text_muted())));
815
816                let item_view = item.debug_view().unwrap_or_else(|| {
817                    Label::new(format!("{:?}", item))
818                        .style(|s| s.flex_grow(1.0))
819                        .into_any()
820                });
821
822                Stack::new((index_label, item_view))
823                    .style(|s| s.items_center().gap(8.0).padding(4.0))
824            }))
825            .style(|s| s.gap(4.0))
826        };
827
828        Some(
829            // preview
830            tooltip_view().into_any(),
831        )
832    }
833
834    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
835        self.iter().zip(other.iter()).try_fold(
836            Vec::with_capacity(self.len()),
837            |mut acc, (v1, v2)| {
838                if let Some(interpolated) = v1.interpolate(v2, value) {
839                    acc.push(interpolated);
840                    Some(acc)
841                } else {
842                    None
843                }
844            },
845        )
846    }
847
848    fn content_hash(&self) -> u64 {
849        use std::hash::{Hash, Hasher};
850        let mut h = rustc_hash::FxHasher::default();
851        self.len().hash(&mut h);
852        for item in self {
853            item.content_hash().hash(&mut h);
854        }
855        h.finish()
856    }
857}
858impl StylePropValue for Pt {
859    fn debug_view(&self) -> Option<Box<dyn View>> {
860        Some(Label::new(format!("{} pt", self.0)).into_any())
861    }
862    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
863        self.0.interpolate(&other.0, value).map(Pt)
864    }
865    fn content_hash(&self) -> u64 {
866        hash_f64(self.0)
867    }
868}
869#[allow(deprecated)]
870impl StylePropValue for super::unit::Px {
871    fn debug_view(&self) -> Option<Box<dyn View>> {
872        Pt(self.0).debug_view()
873    }
874
875    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
876        self.0.interpolate(&other.0, value).map(super::unit::Px)
877    }
878    fn content_hash(&self) -> u64 {
879        hash_f64(self.0)
880    }
881}
882impl StylePropValue for Pct {
883    fn debug_view(&self) -> Option<Box<dyn View>> {
884        Some(Label::new(format!("{}%", self.0)).into_any())
885    }
886    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
887        self.0.interpolate(&other.0, value).map(Pct)
888    }
889    fn content_hash(&self) -> u64 {
890        hash_f64(self.0)
891    }
892}
893impl StylePropValue for LengthAuto {
894    fn debug_view(&self) -> Option<Box<dyn View>> {
895        let label = match self {
896            Self::Pt(v) => format!("{v} pt"),
897            Self::Pct(v) => format!("{v}%"),
898            Self::Em(v) => format!("{v} em"),
899            Self::Lh(v) => format!("{v} lh"),
900            Self::Auto => "auto".to_string(),
901        };
902        Some(Label::new(label).into_any())
903    }
904    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
905        match (self, other) {
906            (Self::Pt(v1), Self::Pt(v2)) => Some(Self::Pt(v1 + (v2 - v1) * value)),
907            (Self::Pct(v1), Self::Pct(v2)) => Some(Self::Pct(v1 + (v2 - v1) * value)),
908            (Self::Em(v1), Self::Em(v2)) => Some(Self::Em(v1 + (v2 - v1) * value)),
909            (Self::Lh(v1), Self::Lh(v2)) => Some(Self::Lh(v1 + (v2 - v1) * value)),
910            (Self::Auto, Self::Auto) => Some(Self::Auto),
911            // TODO: Figure out some way to get in the relevant layout information in order to interpolate between pixels and percent
912            _ => None,
913        }
914    }
915    fn content_hash(&self) -> u64 {
916        use std::hash::{Hash, Hasher};
917        let mut h = rustc_hash::FxHasher::default();
918        std::mem::discriminant(self).hash(&mut h);
919        match self {
920            Self::Pt(v) | Self::Pct(v) | Self::Em(v) | Self::Lh(v) => v.to_bits().hash(&mut h),
921            Self::Auto => {}
922        }
923        h.finish()
924    }
925}
926#[allow(deprecated)]
927impl StylePropValue for super::unit::PxPctAuto {
928    fn debug_view(&self) -> Option<Box<dyn View>> {
929        LengthAuto::from(*self).debug_view()
930    }
931
932    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
933        match (self, other) {
934            (Self::Px(v1), Self::Px(v2)) => Some(Self::Px(v1 + (v2 - v1) * value)),
935            (Self::Pct(v1), Self::Pct(v2)) => Some(Self::Pct(v1 + (v2 - v1) * value)),
936            (Self::Auto, Self::Auto) => Some(Self::Auto),
937            _ => None,
938        }
939    }
940    fn content_hash(&self) -> u64 {
941        use std::hash::{Hash, Hasher};
942        let mut h = rustc_hash::FxHasher::default();
943        std::mem::discriminant(self).hash(&mut h);
944        match self {
945            Self::Px(v) | Self::Pct(v) => v.to_bits().hash(&mut h),
946            Self::Auto => {}
947        }
948        h.finish()
949    }
950}
951impl StylePropValue for Length {
952    fn debug_view(&self) -> Option<Box<dyn View>> {
953        let label = match self {
954            Self::Pt(v) => format!("{v} pt"),
955            Self::Pct(v) => format!("{v}%"),
956            Self::Em(v) => format!("{v} em"),
957            Self::Lh(v) => format!("{v} lh"),
958        };
959        Some(Label::new(label).into_any())
960    }
961
962    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
963        match (self, other) {
964            (Self::Pt(v1), Self::Pt(v2)) => Some(Self::Pt(v1 + (v2 - v1) * value)),
965            (Self::Pct(v1), Self::Pct(v2)) => Some(Self::Pct(v1 + (v2 - v1) * value)),
966            (Self::Em(v1), Self::Em(v2)) => Some(Self::Em(v1 + (v2 - v1) * value)),
967            (Self::Lh(v1), Self::Lh(v2)) => Some(Self::Lh(v1 + (v2 - v1) * value)),
968            // TODO: Figure out some way to get in the relevant layout information in order to interpolate between pixels and percent
969            _ => None,
970        }
971    }
972    fn content_hash(&self) -> u64 {
973        use std::hash::{Hash, Hasher};
974        let mut h = rustc_hash::FxHasher::default();
975        std::mem::discriminant(self).hash(&mut h);
976        match self {
977            Self::Pt(v) | Self::Pct(v) | Self::Em(v) | Self::Lh(v) => v.to_bits().hash(&mut h),
978        }
979        h.finish()
980    }
981}
982#[allow(deprecated)]
983impl StylePropValue for super::unit::PxPct {
984    fn debug_view(&self) -> Option<Box<dyn View>> {
985        Length::from(*self).debug_view()
986    }
987
988    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
989        match (self, other) {
990            (Self::Px(v1), Self::Px(v2)) => Some(Self::Px(v1 + (v2 - v1) * value)),
991            (Self::Pct(v1), Self::Pct(v2)) => Some(Self::Pct(v1 + (v2 - v1) * value)),
992            _ => None,
993        }
994    }
995    fn content_hash(&self) -> u64 {
996        use std::hash::{Hash, Hasher};
997        let mut h = rustc_hash::FxHasher::default();
998        std::mem::discriminant(self).hash(&mut h);
999        match self {
1000            Self::Px(v) | Self::Pct(v) => v.to_bits().hash(&mut h),
1001        }
1002        h.finish()
1003    }
1004}
1005
1006pub(crate) fn views(views: impl ViewTuple) -> Vec<AnyView> {
1007    views.into_views()
1008}
1009
1010impl StylePropValue for Color {
1011    fn content_hash(&self) -> u64 {
1012        use std::hash::{Hash, Hasher};
1013        let mut h = rustc_hash::FxHasher::default();
1014        for c in self.components {
1015            c.to_bits().hash(&mut h);
1016        }
1017        h.finish()
1018    }
1019    fn debug_view(&self) -> Option<Box<dyn View>> {
1020        let color = *self;
1021        let swatch = ()
1022            .style(move |s| {
1023                s.background(color)
1024                    .width(22.0)
1025                    .height(14.0)
1026                    .border(1.)
1027                    .border_color(palette::css::WHITE.with_alpha(0.5))
1028                    .border_radius(5.0)
1029            })
1030            .container()
1031            .style(|s| {
1032                s.border(1.)
1033                    .border_color(palette::css::BLACK.with_alpha(0.5))
1034                    .border_radius(5.0)
1035            });
1036
1037        let tooltip_view = move || {
1038            // Convert to RGBA8 for standard representations
1039            let c = color.to_rgba8();
1040            let (r, g, b, a) = (c.r, c.g, c.b, c.a);
1041
1042            // Hex representation
1043            let hex = if a == 255 {
1044                format!("#{:02X}{:02X}{:02X}", r, g, b)
1045            } else {
1046                format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a)
1047            };
1048
1049            // RGBA string
1050            let rgba_str = format!("rgba({}, {}, {}, {:.3})", r, g, b, a as f32 / 255.0);
1051
1052            // Alpha percentage
1053            let alpha_str = format!(
1054                "{:.1}% ({:.3})",
1055                (a as f32 / 255.0) * 100.0,
1056                a as f32 / 255.0
1057            );
1058
1059            let components = color.components;
1060            let color_space_str = format!("{:?}", color.cs);
1061
1062            let hex = views((
1063                "Hex:".style(|s| s.font_bold().min_width(80.0).justify_end()),
1064                Label::derived(move || hex.clone()),
1065            ));
1066            let rgba = views((
1067                "RGBA:".style(|s| s.font_bold().min_width(80.0).justify_end()),
1068                Label::derived(move || rgba_str.clone()),
1069            ));
1070            let components = views((
1071                "Components:".style(|s| s.font_bold().min_width(80.0).justify_end()),
1072                (
1073                    Label::derived(move || format!("[0]: {:.3}", components[0])),
1074                    Label::derived(move || format!("[1]: {:.3}", components[1])),
1075                    Label::derived(move || format!("[2]: {:.3}", components[2])),
1076                    Label::derived(move || format!("[3]: {:.3}", components[3])),
1077                )
1078                    .v_stack()
1079                    .style(|s| s.gap(2.0)),
1080            ));
1081            let color_space = views((
1082                "Color Space:".style(|s| s.font_bold().min_width(80.0).justify_end()),
1083                Label::derived(move || color_space_str.clone()),
1084            ));
1085            let alpha = views((
1086                "Alpha:".style(|s| s.font_bold().min_width(80.0).justify_end()),
1087                Label::derived(move || alpha_str.clone()),
1088            ));
1089            (hex, rgba, components, color_space, alpha)
1090                .flatten()
1091                .style(|s| {
1092                    s.grid()
1093                        .grid_template_columns([auto(), fr(1.)])
1094                        .justify_center()
1095                        .items_center()
1096                        .row_gap(20)
1097                        .col_gap(10)
1098                        .padding(30)
1099                })
1100        };
1101
1102        Some(
1103            swatch
1104                .tooltip(tooltip_view)
1105                .style(|s| s.items_center())
1106                .into_any(),
1107        )
1108    }
1109
1110    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
1111        Some(self.lerp(*other, value as f32, HueDirection::default()))
1112    }
1113}
1114
1115impl StylePropValue for Gradient {
1116    fn debug_view(&self) -> Option<Box<dyn View>> {
1117        let box_width = 22.;
1118        let box_height = 14.;
1119        let mut grad = self.clone();
1120        grad.kind = match grad.kind {
1121            GradientKind::Linear(LinearGradientPosition { start, end }) => {
1122                let dx = end.x - start.x;
1123                let dy = end.y - start.y;
1124
1125                let scale_x = box_width / dx.abs();
1126                let scale_y = box_height / dy.abs();
1127                let scale = scale_x.min(scale_y);
1128
1129                let new_dx = dx * scale;
1130                let new_dy = dy * scale;
1131
1132                let new_start = Point {
1133                    x: if dx > 0.0 { 0.0 } else { box_width },
1134                    y: if dy > 0.0 { 0.0 } else { box_height },
1135                };
1136
1137                let new_end = Point {
1138                    x: new_start.x + new_dx,
1139                    y: new_start.y + new_dy,
1140                };
1141
1142                GradientKind::Linear(LinearGradientPosition {
1143                    start: new_start,
1144                    end: new_end,
1145                })
1146            }
1147            _ => grad.kind,
1148        };
1149        let color = ().style(move |s| {
1150            s.background(grad.clone())
1151                .width(box_width)
1152                .height(box_height)
1153                .border(1.)
1154                .border_color(palette::css::WHITE.with_alpha(0.5))
1155                .border_radius(5.0)
1156        });
1157        let color = color.container().style(|s| {
1158            s.border(1.)
1159                .border_color(palette::css::BLACK.with_alpha(0.5))
1160                .border_radius(5.0)
1161                .margin_left(6.0)
1162        });
1163        Some(
1164            Stack::new((Label::new(format!("{self:?}")), color))
1165                .style(|s| s.items_center())
1166                .into_any(),
1167        )
1168    }
1169
1170    fn interpolate(&self, _other: &Self, _value: f64) -> Option<Self> {
1171        None
1172    }
1173
1174    fn content_hash(&self) -> u64 {
1175        use std::hash::{Hash, Hasher};
1176        let mut h = rustc_hash::FxHasher::default();
1177        std::mem::discriminant(&self.kind).hash(&mut h);
1178        for stop in self.stops.iter() {
1179            stop.offset.to_bits().hash(&mut h);
1180            for c in stop.color.components {
1181                c.to_bits().hash(&mut h);
1182            }
1183        }
1184        h.finish()
1185    }
1186}
1187
1188// this is a convenience wrapper so border/outline setters can accept numeric widths.
1189#[derive(Clone, Debug, Default)]
1190pub struct StrokeWrap(pub Stroke);
1191impl StrokeWrap {
1192    pub fn new(width: f64) -> Self {
1193        Self(Stroke::new(width))
1194    }
1195}
1196
1197impl From<Stroke> for StrokeWrap {
1198    fn from(value: Stroke) -> Self {
1199        Self(value)
1200    }
1201}
1202impl From<f32> for StrokeWrap {
1203    fn from(value: f32) -> Self {
1204        Self(Stroke::new(value.into()))
1205    }
1206}
1207impl From<f64> for StrokeWrap {
1208    fn from(value: f64) -> Self {
1209        Self(Stroke::new(value))
1210    }
1211}
1212impl From<i32> for StrokeWrap {
1213    fn from(value: i32) -> Self {
1214        Self(Stroke::new(value.into()))
1215    }
1216}
1217
1218impl StylePropValue for Stroke {
1219    fn content_hash(&self) -> u64 {
1220        hash_f64(self.width)
1221    }
1222    fn debug_view(&self) -> Option<Box<dyn View>> {
1223        let stroke = self.clone();
1224        let clone = stroke.clone();
1225
1226        let color = RwSignal::new(palette::css::RED);
1227
1228        // Visual preview of the stroke
1229        let preview = canvas(move |cx, size| {
1230            cx.stroke(
1231                &kurbo::Line::new(
1232                    Point::new(0., size.height / 2.),
1233                    Point::new(size.width, size.height / 2.),
1234                ),
1235                color.get(),
1236                &clone,
1237            );
1238        })
1239        .style(move |s| s.width(80.0).height(20.0))
1240        .container()
1241        .style(move |s| {
1242            s.with_theme(move |s, t| s.border_color(t.border()))
1243                .defer::<Theme>(move |t| color.set(t.primary()))
1244                .padding(4.0)
1245        });
1246
1247        let tooltip_view = move || {
1248            let stroke = stroke.clone();
1249
1250            let width_row = views((
1251                "Width:".style(|s| s.font_bold().min_width(100.0).justify_end()),
1252                Label::derived(move || format!("{:.1}px", stroke.width)),
1253            ));
1254
1255            let join_row = views((
1256                "Join:".style(|s| s.font_bold().min_width(100.0).justify_end()),
1257                Label::derived(move || format!("{:?}", stroke.join)),
1258            ));
1259
1260            let miter_row = views((
1261                "Miter Limit:".style(|s| s.font_bold().min_width(100.0).justify_end()),
1262                Label::derived(move || format!("{:.2}", stroke.miter_limit)),
1263            ));
1264
1265            let start_cap_row = views((
1266                "Start Cap:".style(|s| s.font_bold().min_width(100.0).justify_end()),
1267                Label::derived(move || format!("{:?}", stroke.start_cap)),
1268            ));
1269
1270            let end_cap_row = views((
1271                "End Cap:".style(|s| s.font_bold().min_width(100.0).justify_end()),
1272                Label::derived(move || format!("{:?}", stroke.end_cap)),
1273            ));
1274
1275            let pattern_clone = stroke.dash_pattern.clone();
1276
1277            let dash_pattern_row = views((
1278                "Dash Pattern:".style(|s| s.font_bold().min_width(100.0).justify_end()),
1279                Label::derived(move || {
1280                    if pattern_clone.is_empty() {
1281                        "Solid".to_string()
1282                    } else {
1283                        format!("{:?}", pattern_clone.as_slice())
1284                    }
1285                }),
1286            ));
1287
1288            let dash_offset_row = if !stroke.dash_pattern.is_empty() {
1289                Some(views((
1290                    "Dash Offset:".style(|s| s.font_bold().min_width(100.0).justify_end()),
1291                    Label::derived(move || format!("{:.1}", stroke.dash_offset)),
1292                )))
1293            } else {
1294                None
1295            };
1296
1297            let mut rows = vec![
1298                width_row.into_any(),
1299                join_row.into_any(),
1300                miter_row.into_any(),
1301                start_cap_row.into_any(),
1302                end_cap_row.into_any(),
1303                dash_pattern_row.into_any(),
1304            ];
1305
1306            if let Some(offset_row) = dash_offset_row {
1307                rows.push(offset_row.into_any());
1308            }
1309
1310            Stack::vertical_from_iter(rows).style(|s| {
1311                s.grid()
1312                    .grid_template_columns([auto(), fr(1.)])
1313                    .justify_center()
1314                    .items_center()
1315                    .row_gap(12)
1316                    .col_gap(10)
1317                    .padding(20)
1318            })
1319        };
1320
1321        Some(
1322            preview
1323                .tooltip(tooltip_view)
1324                .style(|s| s.items_center())
1325                .into_any(),
1326        )
1327    }
1328}
1329impl StylePropValue for Brush {
1330    fn content_hash(&self) -> u64 {
1331        use std::hash::{Hash, Hasher};
1332        let mut h = rustc_hash::FxHasher::default();
1333        std::mem::discriminant(self).hash(&mut h);
1334        match self {
1335            Brush::Solid(c) => {
1336                for comp in c.components {
1337                    comp.to_bits().hash(&mut h);
1338                }
1339            }
1340            Brush::Gradient(g) => g.content_hash().hash(&mut h),
1341            Brush::Image(_) => {}
1342        }
1343        h.finish()
1344    }
1345    fn debug_view(&self) -> Option<Box<dyn View>> {
1346        match self {
1347            Brush::Solid(color) => color.debug_view(),
1348            Brush::Gradient(grad) => grad.debug_view(),
1349            Brush::Image(_) => None,
1350        }
1351    }
1352
1353    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
1354        match (self, other) {
1355            (Brush::Solid(color), Brush::Solid(other)) => Some(Self::Solid(color.lerp(
1356                *other,
1357                value as f32,
1358                HueDirection::default(),
1359            ))),
1360            (Brush::Gradient(gradient), Brush::Solid(solid)) => {
1361                let interpolated_stops: Vec<ColorStop> = gradient
1362                    .stops
1363                    .iter()
1364                    .map(|stop| {
1365                        let interpolated_color = stop.color.to_alpha_color().lerp(
1366                            *solid,
1367                            value as f32,
1368                            HueDirection::default(),
1369                        );
1370                        ColorStop::from((stop.offset, interpolated_color))
1371                    })
1372                    .collect();
1373                Some(Brush::Gradient(Gradient {
1374                    kind: gradient.kind,
1375                    extend: gradient.extend,
1376                    interpolation_cs: gradient.interpolation_cs,
1377                    hue_direction: gradient.hue_direction,
1378                    stops: ColorStops::from(&*interpolated_stops),
1379                    interpolation_alpha_space: InterpolationAlphaSpace::Premultiplied,
1380                }))
1381            }
1382            (Brush::Solid(solid), Brush::Gradient(gradient)) => {
1383                let interpolated_stops: Vec<ColorStop> = gradient
1384                    .stops
1385                    .iter()
1386                    .map(|stop| {
1387                        let interpolated_color = solid.lerp(
1388                            stop.color.to_alpha_color(),
1389                            value as f32,
1390                            HueDirection::default(),
1391                        );
1392                        ColorStop::from((stop.offset, interpolated_color))
1393                    })
1394                    .collect();
1395                Some(Brush::Gradient(Gradient {
1396                    kind: gradient.kind,
1397                    extend: gradient.extend,
1398                    interpolation_cs: gradient.interpolation_cs,
1399                    hue_direction: gradient.hue_direction,
1400                    stops: ColorStops::from(&*interpolated_stops),
1401                    interpolation_alpha_space: InterpolationAlphaSpace::Premultiplied,
1402                }))
1403            }
1404
1405            (Brush::Gradient(gradient1), Brush::Gradient(gradient2)) => {
1406                gradient1.interpolate(gradient2, value).map(Brush::Gradient)
1407            }
1408            _ => None,
1409        }
1410    }
1411}
1412impl StylePropValue for Duration {
1413    fn debug_view(&self) -> Option<Box<dyn View>> {
1414        None
1415    }
1416
1417    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
1418        self.as_secs_f64()
1419            .interpolate(&other.as_secs_f64(), value)
1420            .map(Duration::from_secs_f64)
1421    }
1422
1423    fn content_hash(&self) -> u64 {
1424        hash_value(self)
1425    }
1426}
1427
1428impl StylePropValue for super::Angle {
1429    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
1430        Some(self.lerp(other, value))
1431    }
1432    fn content_hash(&self) -> u64 {
1433        use std::hash::{Hash, Hasher};
1434        let mut h = rustc_hash::FxHasher::default();
1435        std::mem::discriminant(self).hash(&mut h);
1436        match self {
1437            super::Angle::Deg(v) | super::Angle::Rad(v) => v.to_bits().hash(&mut h),
1438        }
1439        h.finish()
1440    }
1441}
1442
1443impl StylePropValue for super::AnchorAbout {
1444    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
1445        Some(Self {
1446            x: self.x + (other.x - self.x) * value,
1447            y: self.y + (other.y - self.y) * value,
1448        })
1449    }
1450    fn content_hash(&self) -> u64 {
1451        use std::hash::{Hash, Hasher};
1452        let mut h = rustc_hash::FxHasher::default();
1453        self.x.to_bits().hash(&mut h);
1454        self.y.to_bits().hash(&mut h);
1455        h.finish()
1456    }
1457}
1458
1459impl StylePropValue for kurbo::Rect {
1460    fn content_hash(&self) -> u64 {
1461        use std::hash::{Hash, Hasher};
1462        let mut h = rustc_hash::FxHasher::default();
1463        self.x0.to_bits().hash(&mut h);
1464        self.y0.to_bits().hash(&mut h);
1465        self.x1.to_bits().hash(&mut h);
1466        self.y1.to_bits().hash(&mut h);
1467        h.finish()
1468    }
1469    fn debug_view(&self) -> Option<Box<dyn View>> {
1470        let r = *self;
1471
1472        let w = r.x1 - r.x0;
1473        let h = r.y1 - r.y0;
1474
1475        let coords = [
1476            format!("x0: {:.2}", r.x0),
1477            format!("y0: {:.2}", r.y0),
1478            format!("x1: {:.2}", r.x1),
1479            format!("y1: {:.2}", r.y1),
1480        ]
1481        .v_stack();
1482
1483        let wh = [format!("w: {:.2}", w), format!("h: {:.2}", h)].h_stack();
1484
1485        let preview = Empty::new().style(move |s| {
1486            let max = w.abs().max(h.abs()).max(1.0);
1487            let scale = 60.0 / max;
1488
1489            s.width(w.abs() * scale)
1490                .height(h.abs() * scale)
1491                .border(1.0)
1492                .with_theme(|s, t| {
1493                    s.border_color(t.border())
1494                        .background(t.primary_muted())
1495                        .border_radius(t.border_radius())
1496                })
1497        });
1498
1499        Some(
1500            (
1501                "Rect",
1502                preview,
1503                coords.style(|s| s.gap(2)),
1504                wh.style(|s| s.gap(8)),
1505            )
1506                .v_stack()
1507                .into_any(),
1508        )
1509    }
1510
1511    fn interpolate(&self, other: &Self, value: f64) -> Option<Self> {
1512        let lerp = |a: f64, b: f64| a + (b - a) * value;
1513
1514        Some(Self {
1515            x0: lerp(self.x0, other.x0),
1516            y0: lerp(self.y0, other.y0),
1517            x1: lerp(self.x1, other.x1),
1518            y1: lerp(self.y1, other.y1),
1519        })
1520    }
1521}
1522
1523impl StylePropValue for Affine {
1524    fn debug_view(&self) -> Option<Box<dyn View>> {
1525        let affine = *self;
1526        let coeffs = affine.as_coeffs();
1527
1528        // Decompose to show meaningful transform components
1529        let (scale, rotation) = affine.svd();
1530        let translation = affine.translation();
1531
1532        // Create a visual preview showing the transform effect
1533        let preview = canvas(move |cx, size| {
1534            let center = Point::new(size.width / 2., size.height / 2.);
1535            let box_size = 20.0;
1536
1537            // Draw original position (dashed outline)
1538            let original_rect =
1539                kurbo::Rect::from_center_size(center, kurbo::Size::new(box_size, box_size));
1540            cx.stroke(
1541                &original_rect,
1542                palette::css::GRAY.with_alpha(0.5),
1543                &kurbo::Stroke::new(1.0).with_dashes(0., [3., 3.]),
1544            );
1545
1546            // Draw transformed position
1547            let transform_offset =
1548                Affine::translate((center.x - box_size / 2., center.y - box_size / 2.));
1549            let display_transform = transform_offset * affine * transform_offset.inverse();
1550
1551            let transformed_rect = kurbo::Rect::new(0., 0., box_size, box_size);
1552            cx.fill(
1553                &display_transform.transform_rect_bbox(transformed_rect),
1554                palette::css::BLUE.with_alpha(0.7),
1555                0.,
1556            );
1557            cx.stroke(
1558                &(display_transform.transform_rect_bbox(transformed_rect)),
1559                palette::css::BLUE,
1560                &kurbo::Stroke::new(2.0),
1561            );
1562
1563            // Draw origin point
1564            let origin_marker = kurbo::Circle::new(display_transform * Point::ZERO, 3.0);
1565            cx.fill(&origin_marker, palette::css::RED, 0.);
1566        })
1567        .style(|s| s.width(80.0).height(60.0))
1568        .container()
1569        .style(|s| {
1570            s.padding(4.0)
1571                .border(1.)
1572                .border_radius(5.0)
1573                .with_theme(|s, t| s.border_color(t.border()))
1574        });
1575
1576        let tooltip_view = move || {
1577            // Matrix coefficients in a grid
1578            let matrix_label = Label::new("Matrix:").style(|s| s.font_bold().margin_bottom(8.0));
1579
1580            let matrix_grid = (
1581                views((
1582                    Label::new(format!("{:.3}", coeffs[0])),
1583                    Label::new(format!("{:.3}", coeffs[2])),
1584                    Label::new(format!("{:.3}", coeffs[4])),
1585                )),
1586                views((
1587                    Label::new(format!("{:.3}", coeffs[1])),
1588                    Label::new(format!("{:.3}", coeffs[3])),
1589                    Label::new(format!("{:.3}", coeffs[5])),
1590                )),
1591                views((Label::new("0"), Label::new("0"), Label::new("1"))),
1592            )
1593                .v_stack()
1594                .style(|s| {
1595                    s.gap(4.0)
1596                        .padding(8.0)
1597                        .border(1.)
1598                        .border_radius(4.0)
1599                        .with_theme(|s, t| {
1600                            s.background(t.def(|t| t.primary().with_alpha(0.5)))
1601                                .border_color(t.border())
1602                        })
1603                });
1604
1605            // Decomposed components
1606            let components_label = Label::new("Components:")
1607                .style(|s| s.font_bold().margin_top(16.0).margin_bottom(8.0));
1608
1609            let translate_row = views((
1610                "Translate:".style(|s| s.font_bold().min_width(100.0).justify_end()),
1611                Label::derived(move || format!("({:.2}, {:.2})", translation.x, translation.y)),
1612            ));
1613
1614            let rotate_row = views((
1615                "Rotate:".style(|s| s.font_bold().min_width(100.0).justify_end()),
1616                Label::derived(move || format!("{:.1}°", rotation.to_degrees())),
1617            ));
1618
1619            let scale_row = views((
1620                "Scale:".style(|s| s.font_bold().min_width(100.0).justify_end()),
1621                Label::derived(move || format!("({:.2}, {:.2})", scale.x, scale.y)),
1622            ));
1623
1624            // Check for special properties
1625            let is_identity = affine == Affine::IDENTITY;
1626            let determinant = coeffs[0] * coeffs[3] - coeffs[1] * coeffs[2];
1627            let has_reflection = determinant < 0.0;
1628
1629            let properties = if is_identity {
1630                Some(
1631                    Label::new("Identity (no transform)")
1632                        .style(|s| s.with_theme(|s, t| s.color(t.text_muted()))),
1633                )
1634            } else if has_reflection {
1635                Some(
1636                    Label::new("⚠ Contains reflection")
1637                        .style(|s| s.with_theme(|s, t| s.color(t.warning()))),
1638                )
1639            } else {
1640                None
1641            };
1642
1643            let components_grid = (translate_row, rotate_row, scale_row).flatten().style(|s| {
1644                s.grid()
1645                    .grid_template_columns([auto(), fr(1.)])
1646                    .justify_center()
1647                    .items_center()
1648                    .row_gap(8)
1649                    .col_gap(10)
1650            });
1651
1652            let mut content = vec![
1653                matrix_label.into_any(),
1654                matrix_grid.into_any(),
1655                components_label.into_any(),
1656                components_grid.into_any(),
1657            ];
1658
1659            if let Some(props) = properties {
1660                content.push(props.into_any());
1661            }
1662
1663            Stack::vertical_from_iter(content).style(|s| s.padding(20))
1664        };
1665
1666        Some(
1667            preview
1668                .tooltip(tooltip_view)
1669                .style(|s| s.items_center())
1670                .into_any(),
1671        )
1672    }
1673
1674    fn interpolate(&self, other: &Self, t: f64) -> Option<Self> {
1675        Some(self.lerp(other, t))
1676    }
1677
1678    fn content_hash(&self) -> u64 {
1679        use std::hash::{Hash, Hasher};
1680        let mut hasher = rustc_hash::FxHasher::default();
1681
1682        let coeffs = self.as_coeffs();
1683        for coeff in coeffs {
1684            coeff.to_bits().hash(&mut hasher);
1685        }
1686
1687        hasher.finish()
1688    }
1689}
1690
1691pub trait AffineLerp {
1692    fn svd(self) -> (Vec2, f64);
1693
1694    /// Linearly interpolate between two affine transforms.
1695    ///
1696    /// This implements the CSS Transforms interpolation algorithm:
1697    /// - Decompose both transforms into translation, rotation, and scale components
1698    /// - Interpolate each component separately
1699    /// - Recompose the result
1700    ///
1701    /// `t` should be in the range [0.0, 1.0] where:
1702    /// - t = 0.0 returns `self`
1703    /// - t = 1.0 returns `other`
1704    /// - t = 0.5 returns the midpoint
1705    fn lerp(&self, other: &Affine, t: f64) -> Affine;
1706}
1707
1708impl AffineLerp for Affine {
1709    fn svd(self) -> (Vec2, f64) {
1710        let [a, b, c, d, _, _] = self.as_coeffs();
1711        let a2 = a * a;
1712        let b2 = b * b;
1713        let c2 = c * c;
1714        let d2 = d * d;
1715        let ab = a * b;
1716        let cd = c * d;
1717        let angle = 0.5 * (2.0 * (ab + cd)).atan2(a2 - b2 + c2 - d2);
1718        let s1 = a2 + b2 + c2 + d2;
1719        let s2 = ((a2 - b2 + c2 - d2).powi(2) + 4.0 * (ab + cd).powi(2)).sqrt();
1720        (
1721            Vec2 {
1722                x: (0.5 * (s1 + s2)).sqrt(),
1723                y: (0.5 * (s1 - s2)).sqrt(),
1724            },
1725            angle,
1726        )
1727    }
1728
1729    fn lerp(&self, other: &Affine, t: f64) -> Affine {
1730        // Extract translations
1731        let trans_a = self.translation();
1732        let trans_b = other.translation();
1733
1734        // Remove translations to get the linear parts
1735        let linear_a = self.with_translation(Vec2::ZERO);
1736        let linear_b = other.with_translation(Vec2::ZERO);
1737
1738        // Decompose into scale and rotation using SVD
1739        let (scale_a, rotation_a) = linear_a.svd();
1740        let (scale_b, rotation_b) = linear_b.svd();
1741
1742        // Interpolate translation
1743        let trans = Vec2 {
1744            x: trans_a.x + (trans_b.x - trans_a.x) * t,
1745            y: trans_a.y + (trans_b.y - trans_a.y) * t,
1746        };
1747
1748        // Interpolate scale
1749        let scale = Vec2 {
1750            x: scale_a.x + (scale_b.x - scale_a.x) * t,
1751            y: scale_a.y + (scale_b.y - scale_a.y) * t,
1752        };
1753
1754        // Interpolate rotation (taking the shorter path)
1755        let mut angle_diff = rotation_b - rotation_a;
1756        // Normalize to [-π, π] to take the shorter rotation path
1757        while angle_diff > std::f64::consts::PI {
1758            angle_diff -= 2.0 * std::f64::consts::PI;
1759        }
1760        while angle_diff < -std::f64::consts::PI {
1761            angle_diff += 2.0 * std::f64::consts::PI;
1762        }
1763        let rotation = rotation_a + angle_diff * t;
1764
1765        // Recompose: rotate -> scale -> translate
1766        Affine::rotate(rotation)
1767            .then_scale_non_uniform(scale.x, scale.y)
1768            .then_translate(trans)
1769    }
1770}
1771
1772#[cfg(test)]
1773mod affine_lerp_tests {
1774    use super::*;
1775
1776    #[test]
1777    fn test_lerp_identity() {
1778        let a = Affine::IDENTITY;
1779        let b = Affine::translate(Vec2::new(100.0, 50.0));
1780
1781        let result = a.lerp(&b, 0.0);
1782        assert_eq!(result.as_coeffs(), a.as_coeffs());
1783
1784        let result = a.lerp(&b, 1.0);
1785        assert_eq!(result.as_coeffs(), b.as_coeffs());
1786    }
1787
1788    #[test]
1789    fn test_lerp_translation() {
1790        let a = Affine::translate(Vec2::new(0.0, 0.0));
1791        let b = Affine::translate(Vec2::new(100.0, 50.0));
1792
1793        let result = a.lerp(&b, 0.5);
1794        let trans = result.translation();
1795        assert!((trans.x - 50.0).abs() < 1e-10);
1796        assert!((trans.y - 25.0).abs() < 1e-10);
1797    }
1798
1799    #[test]
1800    fn test_lerp_rotation() {
1801        let a = Affine::rotate(0.0);
1802        let b = Affine::rotate(std::f64::consts::PI / 2.0);
1803
1804        let result = a.lerp(&b, 0.5);
1805        // Should be rotated by π/4
1806        let point = result * Point::new(1.0, 0.0);
1807        let expected_angle = std::f64::consts::PI / 4.0;
1808        assert!((point.x - expected_angle.cos()).abs() < 1e-10);
1809        assert!((point.y - expected_angle.sin()).abs() < 1e-10);
1810    }
1811
1812    #[test]
1813    fn test_lerp_scale() {
1814        let a = Affine::scale(1.0);
1815        let b = Affine::scale(2.0);
1816
1817        let result = a.lerp(&b, 0.5);
1818        let point = result * Point::new(1.0, 1.0);
1819        assert!((point.x - 1.5).abs() < 1e-10);
1820        assert!((point.y - 1.5).abs() < 1e-10);
1821    }
1822}
1823
1824/// Internal storage for style property values in the style map.
1825///
1826/// Unlike `StyleValue<T>` which is used in the public API, `StyleMapValue<T>`
1827/// is the internal representation stored in the style hashmap.
1828#[derive(Debug, Clone, PartialEq, Eq)]
1829pub enum StyleMapValue<T> {
1830    /// Value inserted by animation interpolation
1831    Animated(T),
1832    /// Value set directly
1833    Val(T),
1834    /// Value resolved from inherited context when the property is read.
1835    Context(ContextValue<T>),
1836    /// Use the default value for the style, typically from the underlying `ComputedStyle`
1837    Unset,
1838}
1839
1840/// The value for a [`Style`] property in the public API.
1841///
1842/// This represents the result of reading a style property, with additional
1843/// states like `Base` that indicate inheritance from parent styles.
1844#[derive(Debug, Clone, PartialEq, Eq, Default)]
1845pub enum StyleValue<T> {
1846    /// Value resolved from inherited context when the property is read.
1847    Context(ContextValue<T>),
1848    /// Value inserted by animation interpolation
1849    Animated(T),
1850    /// Value set directly
1851    Val(T),
1852    /// Use the default value for the style, typically from the underlying `ComputedStyle`.
1853    Unset,
1854    /// Use whatever the base style is. For an overriding style like hover, this uses the base
1855    /// style. For the base style, this is equivalent to `Unset`.
1856    #[default]
1857    Base,
1858}
1859
1860impl<T: 'static> StyleValue<T> {
1861    pub fn map<U>(self, f: impl Fn(T) -> U + 'static) -> StyleValue<U> {
1862        match self {
1863            Self::Context(x) => StyleValue::Context(x.map(f)),
1864            Self::Val(x) => StyleValue::Val(f(x)),
1865            Self::Animated(x) => StyleValue::Animated(f(x)),
1866            Self::Unset => StyleValue::Unset,
1867            Self::Base => StyleValue::Base,
1868        }
1869    }
1870
1871    pub fn unwrap_or(self, default: T) -> T {
1872        match self {
1873            Self::Context(_) => default,
1874            Self::Val(x) => x,
1875            Self::Animated(x) => x,
1876            Self::Unset => default,
1877            Self::Base => default,
1878        }
1879    }
1880
1881    pub fn unwrap_or_else(self, f: impl FnOnce() -> T) -> T {
1882        match self {
1883            Self::Context(_) => f(),
1884            Self::Val(x) => x,
1885            Self::Animated(x) => x,
1886            Self::Unset => f(),
1887            Self::Base => f(),
1888        }
1889    }
1890
1891    pub fn as_mut(&mut self) -> Option<&mut T> {
1892        match self {
1893            Self::Context(_) => None,
1894            Self::Val(x) => Some(x),
1895            Self::Animated(x) => Some(x),
1896            Self::Unset => None,
1897            Self::Base => None,
1898        }
1899    }
1900}
1901
1902impl<T> From<T> for StyleValue<T> {
1903    fn from(x: T) -> Self {
1904        Self::Val(x)
1905    }
1906}
1907
1908impl<T> From<ContextValue<T>> for StyleValue<T> {
1909    fn from(x: ContextValue<T>) -> Self {
1910        Self::Context(x)
1911    }
1912}
1913
1914fn short_style_name(name: &str) -> String {
1915    name.strip_prefix("floem::style::")
1916        .unwrap_or(name)
1917        .to_string()
1918}
1919
1920struct StyleDebugRow {
1921    render: Rc<dyn Fn(bool) -> AnyView>,
1922    is_empty: bool,
1923}
1924
1925fn effective_inherited_debug_groups(
1926    style: &Style,
1927    parent_groups: &HashSet<StyleKey>,
1928) -> HashSet<StyleKey> {
1929    let mut groups = parent_groups.clone();
1930    for key in style.map.keys() {
1931        if let StyleKeyInfo::DebugGroup(info) = key.info {
1932            if style.debug_group_enabled(*key) {
1933                if info.inherited {
1934                    groups.insert(*key);
1935                }
1936            } else {
1937                groups.remove(key);
1938            }
1939        }
1940    }
1941    groups
1942}
1943
1944fn style_debug_active_groups(
1945    style: &Style,
1946    inherited_groups: &HashSet<StyleKey>,
1947) -> Vec<&'static StyleDebugGroupInfo> {
1948    let mut groups = style
1949        .map
1950        .keys()
1951        .filter_map(|key| match key.info {
1952            StyleKeyInfo::DebugGroup(info) if style.debug_group_enabled(*key) => Some(info),
1953            _ => None,
1954        })
1955        .collect::<Vec<_>>();
1956
1957    for key in inherited_groups {
1958        if let StyleKeyInfo::DebugGroup(info) = key.info
1959            && !style.map.contains_key(key)
1960        {
1961            groups.push(info);
1962        }
1963    }
1964
1965    groups.sort_unstable_by_key(|info| short_style_name((info.name)()));
1966    groups.dedup_by_key(|info| (info.name)());
1967    groups
1968}
1969
1970fn style_debug_is_empty(style: &Style, inherited_groups: &HashSet<StyleKey>) -> bool {
1971    let mut hidden_props = HashSet::new();
1972
1973    for info in style_debug_active_groups(style, inherited_groups) {
1974        let members = (info.member_props)();
1975        let present = members
1976            .iter()
1977            .copied()
1978            .filter(|key| style.map.contains_key(key) && !hidden_props.contains(key))
1979            .collect::<Vec<_>>();
1980        if !present.is_empty() {
1981            hidden_props.extend(present);
1982        }
1983    }
1984
1985    if style
1986        .map
1987        .iter()
1988        .any(|(key, _)| matches!(key.info, StyleKeyInfo::Prop(..)) && !hidden_props.contains(key))
1989    {
1990        return false;
1991    }
1992
1993    if style.map.iter().any(|(key, value)| match key.info {
1994        StyleKeyInfo::Selector(..) | StyleKeyInfo::Class(..) => {
1995            value.downcast_ref::<Style>().is_some_and(|nested| {
1996                !style_debug_is_empty(
1997                    nested,
1998                    &effective_inherited_debug_groups(nested, inherited_groups),
1999                )
2000            })
2001        }
2002        _ => false,
2003    }) {
2004        return false;
2005    }
2006
2007    for value in style.map.values() {
2008        if let Some(rules) = value.downcast_ref::<StructuralSelectors>()
2009            && rules.0.iter().any(|(_, nested)| {
2010                !style_debug_is_empty(
2011                    nested,
2012                    &effective_inherited_debug_groups(nested, inherited_groups),
2013                )
2014            })
2015        {
2016            return false;
2017        }
2018        if let Some(rules) = value.downcast_ref::<ResponsiveSelectors>()
2019            && rules.0.iter().any(|(_, nested)| {
2020                !style_debug_is_empty(
2021                    nested,
2022                    &effective_inherited_debug_groups(nested, inherited_groups),
2023                )
2024            })
2025        {
2026            return false;
2027        }
2028    }
2029
2030    true
2031}
2032
2033fn debug_name_cell(name: String, is_direct: bool, indent: usize) -> AnyView {
2034    let indent = (indent as f64) * 16.0;
2035    let name = if is_direct {
2036        Label::new(name).into_any()
2037    } else {
2038        Stack::new((
2039            "Inherited".style(|s| {
2040                s.margin_right(5.0)
2041                    .border(1.)
2042                    .border_radius(5.0)
2043                    .with_theme(|s, t| s.color(t.text_muted()).border_color(t.border()))
2044                    .padding_horiz(4.0)
2045                    .with::<FontSize>(|s, fs| s.font_size(fs.def(|fs| fs * 0.8)))
2046            }),
2047            Label::new(name),
2048        ))
2049        .style(|s| s.items_center().gap(6.0))
2050        .into_any()
2051    };
2052
2053    name.container()
2054        .style(move |s| {
2055            s.padding_left(indent)
2056                .min_width(170.)
2057                .padding_right(5.0)
2058                .flex_direction(FlexDirection::RowReverse)
2059        })
2060        .into_any()
2061}
2062
2063fn style_debug_prop_row(
2064    style: &Style,
2065    prop: StylePropRef,
2066    value: &Rc<dyn std::any::Any>,
2067    is_direct: bool,
2068    indent: usize,
2069) -> StyleDebugRow {
2070    let style = style.clone();
2071    let value = value.clone();
2072    let name = short_style_name(&format!("{:?}", prop.key));
2073    StyleDebugRow {
2074        render: Rc::new(move |_| {
2075            let mut value_view = (prop.info().debug_view)(&*value)
2076                .unwrap_or_else(|| Label::new((prop.info().debug_any)(&*value)).into_any());
2077
2078            if let Some(transition) = style
2079                .map
2080                .get(&prop.info().transition_key)
2081                .and_then(|v| v.downcast_ref::<Transition>())
2082            {
2083                value_view = Stack::vertical((
2084                    value_view,
2085                    Stack::new((
2086                        "Transition".style(|s| {
2087                            s.margin_top(4.0)
2088                                .margin_right(5.0)
2089                                .border(1.)
2090                                .border_radius(5.0)
2091                                .padding_horiz(4.0)
2092                                .with_theme(|s, t| s.color(t.text_muted()).border_color(t.border()))
2093                                .with::<FontSize>(|s, fs| s.font_size(fs.def(|fs| fs * 0.8)))
2094                        }),
2095                        transition.debug_view(),
2096                    ))
2097                    .style(|s| s.items_center().gap(6.0)),
2098                ))
2099                .into_any();
2100            }
2101
2102            Stack::new((debug_name_cell(name.clone(), is_direct, indent), value_view))
2103                .style(|s| s.items_center().width_full().padding_vert(4.0).gap(8.0))
2104                .into_any()
2105        }),
2106        is_empty: false,
2107    }
2108}
2109
2110fn style_debug_group_row<V>(
2111    name: String,
2112    value_view: V,
2113    is_direct: bool,
2114    indent: usize,
2115) -> StyleDebugRow
2116where
2117    V: Fn() -> AnyView + 'static,
2118{
2119    StyleDebugRow {
2120        render: Rc::new(move |_| {
2121            Stack::new((
2122                debug_name_cell(name.clone(), is_direct, indent),
2123                value_view(),
2124            ))
2125            .style(|s| s.items_center().width_full().padding_vert(4.0).gap(8.0))
2126            .into_any()
2127        }),
2128        is_empty: false,
2129    }
2130}
2131
2132fn style_debug_section(title: String, child: StyleDebugRow, indent: usize) -> StyleDebugRow {
2133    let expanded = RwSignal::new(false);
2134    let title_text = title.clone();
2135    let child_is_empty = child.is_empty;
2136    let chevron = move || {
2137        if expanded.get() {
2138            svg(
2139                r#"<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M4.427 6.427l3.396 3.396a.25.25 0 00.354 0l3.396-3.396A.25.25 0 0011.396 6H4.604a.25.25 0 00-.177.427z"/></svg>"#,
2140            )
2141        } else {
2142            svg(
2143                r#"<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M6.427 4.427l3.396 3.396a.25.25 0 010 .354l-3.396 3.396A.25.25 0 016 11.396V4.604a.25.25 0 01.427-.177z"/></svg>"#,
2144            )
2145        }
2146        .style(|s| s.size_full().with_theme(|s, t| s.color(t.text())))
2147    };
2148
2149    StyleDebugRow {
2150        render: Rc::new(move |row_is_base| {
2151            let child_render = child.render.clone();
2152            Stack::vertical((
2153                Stack::new((
2154                    dyn_view(chevron)
2155                        .class(ButtonClass)
2156                        .style(|s| s.size(16.0, 16.0).padding(0.)),
2157                    Label::new(title_text.clone()).style(|s| {
2158                        s.font_bold()
2159                            .cursor(CursorStyle::Pointer)
2160                            .with_theme(|s, t| s.color(t.primary()))
2161                    }),
2162                    Label::new("empty")
2163                        .style(|s| {
2164                            s.padding_horiz(6.0)
2165                                .border(1.)
2166                                .border_radius(999.0)
2167                                .with_theme(|s, t| s.color(t.text_muted()).border_color(t.border()))
2168                                .with::<FontSize>(|s, fs| s.font_size(fs.def(|fs| fs * 0.75)))
2169                        })
2170                        .style(move |s| s.apply_if(!child_is_empty, |s| s.hide())),
2171                ))
2172                .style(move |s| {
2173                    s.items_center()
2174                        .gap(6.0)
2175                        .padding_left((indent as f64) * 16.0)
2176                        .cursor(super::CursorStyle::Pointer)
2177                })
2178                .on_event_stop(crate::event::listener::Click, move |_cx, _event| {
2179                    expanded.update(|value| *value = !*value)
2180                }),
2181                dyn_view(move || {
2182                    if expanded.get() {
2183                        child_render(!row_is_base)
2184                            .style(|s| s.padding_left(12.0))
2185                            .into_any()
2186                    } else {
2187                        Empty::new().into_any()
2188                    }
2189                })
2190                .into_any(),
2191            ))
2192            .style(|s| s.gap(6.0).width_full().padding_vert(4.0))
2193            .into_any()
2194        }),
2195        is_empty: false,
2196    }
2197}
2198
2199fn style_debug_sections(
2200    title: &str,
2201    children: Vec<StyleDebugRow>,
2202    indent: usize,
2203) -> Option<StyleDebugRow> {
2204    if children.is_empty() {
2205        return None;
2206    }
2207
2208    Some(style_debug_section(
2209        title.to_string(),
2210        StyleDebugRow {
2211            render: Rc::new(move |start_with_base| style_debug_rows(&children, start_with_base)),
2212            is_empty: false,
2213        },
2214        indent,
2215    ))
2216}
2217
2218fn style_debug_style_section(
2219    title: String,
2220    style: &Style,
2221    inherited_groups: &HashSet<StyleKey>,
2222    indent: usize,
2223) -> StyleDebugRow {
2224    let nested_inherited = effective_inherited_debug_groups(style, inherited_groups);
2225    style_debug_section(
2226        title,
2227        style_debug_body(style, None, &nested_inherited, indent + 1),
2228        indent,
2229    )
2230}
2231
2232fn style_debug_prop_rows(
2233    style: &Style,
2234    direct_keys: Option<&HashSet<StyleKey>>,
2235    inherited_groups: &HashSet<StyleKey>,
2236    indent: usize,
2237) -> Vec<StyleDebugRow> {
2238    let mut rows: Vec<StyleDebugRow> = Vec::new();
2239    let mut hidden_props = HashSet::new();
2240
2241    for info in style_debug_active_groups(style, inherited_groups) {
2242        let members = (info.member_props)();
2243        let present = members
2244            .iter()
2245            .copied()
2246            .filter(|key| style.map.contains_key(key) && !hidden_props.contains(key))
2247            .collect::<Vec<_>>();
2248        if present.is_empty() {
2249            continue;
2250        }
2251        hidden_props.extend(present);
2252        if (info.debug_view)(style).is_some() {
2253            let info = info.clone();
2254            let style = style.clone();
2255            rows.push(style_debug_group_row(
2256                short_style_name((info.name)()),
2257                move || {
2258                    (info.debug_view)(&style)
2259                        .unwrap_or_else(|| Label::new("empty").into_any())
2260                        .into_any()
2261                },
2262                true,
2263                indent,
2264            ));
2265        }
2266    }
2267
2268    let mut props = style
2269        .map
2270        .iter()
2271        .filter_map(|(key, value)| match key.info {
2272            StyleKeyInfo::Prop(..) if !hidden_props.contains(key) => {
2273                Some((StylePropRef { key: *key }, value))
2274            }
2275            _ => None,
2276        })
2277        .collect::<Vec<_>>();
2278    props.sort_unstable_by_key(|(prop, _)| short_style_name(&format!("{:?}", prop.key)));
2279
2280    for (prop, value) in props {
2281        let is_direct = direct_keys
2282            .as_ref()
2283            .is_none_or(|keys| keys.contains(&prop.key));
2284        rows.push(style_debug_prop_row(style, prop, value, is_direct, indent));
2285    }
2286
2287    rows
2288}
2289
2290fn style_debug_selector_rows(
2291    style: &Style,
2292    inherited_groups: &HashSet<StyleKey>,
2293    indent: usize,
2294) -> Vec<StyleDebugRow> {
2295    let mut selector_rows: Vec<StyleDebugRow> = Vec::new();
2296    let mut selectors = style
2297        .map
2298        .iter()
2299        .filter_map(|(key, value)| match key.info {
2300            StyleKeyInfo::Selector(selector) => Some((selector.debug_string(), value)),
2301            _ => None,
2302        })
2303        .collect::<Vec<_>>();
2304    selectors.sort_unstable_by(|a, b| a.0.cmp(&b.0));
2305    for (name, value) in selectors {
2306        if let Some(nested_style) = value.downcast_ref::<Style>() {
2307            selector_rows.push(style_debug_style_section(
2308                name,
2309                nested_style,
2310                inherited_groups,
2311                indent,
2312            ));
2313        }
2314    }
2315
2316    for value in style.map.values() {
2317        if let Some(rules) = value.downcast_ref::<StructuralSelectors>() {
2318            for (selector, nested_style) in &rules.0 {
2319                selector_rows.push(style_debug_style_section(
2320                    format!("Structural: {selector:?}"),
2321                    nested_style,
2322                    inherited_groups,
2323                    indent,
2324                ));
2325            }
2326        }
2327        if let Some(rules) = value.downcast_ref::<ResponsiveSelectors>() {
2328            for (selector, nested_style) in &rules.0 {
2329                selector_rows.push(style_debug_style_section(
2330                    format!("Responsive: {selector:?}"),
2331                    nested_style,
2332                    inherited_groups,
2333                    indent,
2334                ));
2335            }
2336        }
2337    }
2338
2339    selector_rows
2340}
2341
2342fn style_debug_class_rows(
2343    style: &Style,
2344    inherited_groups: &HashSet<StyleKey>,
2345    indent: usize,
2346) -> Vec<StyleDebugRow> {
2347    let mut class_rows: Vec<StyleDebugRow> = Vec::new();
2348    let mut classes = style
2349        .map
2350        .iter()
2351        .filter_map(|(key, value)| match key.info {
2352            StyleKeyInfo::Class(info) => Some((short_style_name((info.name)()), value)),
2353            _ => None,
2354        })
2355        .collect::<Vec<_>>();
2356    classes.sort_unstable_by(|a, b| a.0.cmp(&b.0));
2357    for (name, value) in classes {
2358        if let Some(nested_style) = value.downcast_ref::<Style>() {
2359            class_rows.push(style_debug_style_section(
2360                name,
2361                nested_style,
2362                inherited_groups,
2363                indent,
2364            ));
2365        }
2366    }
2367    class_rows
2368}
2369
2370fn style_debug_body(
2371    style: &Style,
2372    direct_keys: Option<&HashSet<StyleKey>>,
2373    inherited_groups: &HashSet<StyleKey>,
2374    indent: usize,
2375) -> StyleDebugRow {
2376    let style = style.clone();
2377    let inherited_groups = inherited_groups.clone();
2378    let is_empty = style_debug_is_empty(&style, &inherited_groups);
2379    let direct_keys = direct_keys.cloned();
2380    StyleDebugRow {
2381        render: Rc::new(move |start_with_base| {
2382            let mut rows =
2383                style_debug_prop_rows(&style, direct_keys.as_ref(), &inherited_groups, indent);
2384            if let Some(selectors_section) = style_debug_sections(
2385                "Selectors",
2386                style_debug_selector_rows(&style, &inherited_groups, indent),
2387                indent,
2388            ) {
2389                rows.push(selectors_section);
2390            }
2391            if let Some(classes_section) = style_debug_sections(
2392                "Classes",
2393                style_debug_class_rows(&style, &inherited_groups, indent),
2394                indent,
2395            ) {
2396                rows.push(classes_section);
2397            }
2398
2399            if rows.is_empty() {
2400                return Label::new("empty")
2401                    .style(|s| s.with_theme(|s, t| s.color(t.text_muted())))
2402                    .into_any();
2403            }
2404
2405            style_debug_rows(&rows, start_with_base)
2406        }),
2407        is_empty,
2408    }
2409}
2410
2411fn style_debug_rows(rows: &[StyleDebugRow], start_with_base: bool) -> AnyView {
2412    Stack::vertical_from_iter(rows.iter().enumerate().map(|(idx, row)| {
2413        let is_base = if start_with_base {
2414            idx.is_multiple_of(2)
2415        } else {
2416            !idx.is_multiple_of(2)
2417        };
2418        (row.render)(is_base).style(move |s| {
2419            s.width_full().padding_horiz(4.0).with_theme(move |s, t| {
2420                s.apply_if(is_base, |s| s.background(t.bg_base()))
2421                    .apply_if(!is_base, |s| s.background(t.bg_elevated()))
2422            })
2423        })
2424    }))
2425    .style(|s| s.gap(4.0).width_full())
2426    .into_any()
2427}
2428
2429impl Style {
2430    pub fn debug_view(&self, direct_style: Option<&Style>) -> Box<dyn View> {
2431        let direct_keys =
2432            direct_style.map(|style| style.map.keys().copied().collect::<HashSet<_>>());
2433        let style = self.clone();
2434        let inherited_groups = effective_inherited_debug_groups(&style, &HashSet::new());
2435        let selected_tab = RwSignal::new(0);
2436        let tab_item = move |name, index| {
2437            Label::new(name)
2438                .class(TabSelectorClass)
2439                .action(move || selected_tab.set(index))
2440                .style(move |s| s.set_selected(selected_tab.get() == index))
2441        };
2442        let tabs = (
2443            tab_item("View Style", 0),
2444            tab_item("Selectors", 1),
2445            tab_item("Classes", 2),
2446        )
2447            .h_stack()
2448            .style(|s| s.with_theme(|s, t| s.background(t.bg_base())));
2449        let direct_keys_for_body = direct_keys.clone();
2450        let style_for_body = style.clone();
2451        let style_for_selectors = style.clone();
2452        let style_for_classes = style.clone();
2453        Stack::vertical((
2454            tabs,
2455            tab(
2456                move || Some(selected_tab.get()),
2457                move || [0, 1, 2],
2458                |it| *it,
2459                move |it| match it {
2460                    0 => {
2461                        let rows = style_debug_prop_rows(
2462                            &style_for_body,
2463                            direct_keys_for_body.as_ref(),
2464                            &inherited_groups,
2465                            0,
2466                        );
2467                        if rows.is_empty() {
2468                            Label::new("empty")
2469                                .style(|s| s.with_theme(|s, t| s.color(t.text_muted())))
2470                                .into_any()
2471                        } else {
2472                            style_debug_rows(&rows, true)
2473                        }
2474                    }
2475                    1 => {
2476                        let rows =
2477                            style_debug_selector_rows(&style_for_selectors, &inherited_groups, 0);
2478                        if rows.is_empty() {
2479                            Label::new("empty")
2480                                .style(|s| s.with_theme(|s, t| s.color(t.text_muted())))
2481                                .into_any()
2482                        } else {
2483                            style_debug_rows(&rows, true)
2484                        }
2485                    }
2486                    2 => {
2487                        let rows = style_debug_class_rows(&style_for_classes, &inherited_groups, 0);
2488                        if rows.is_empty() {
2489                            Label::new("empty")
2490                                .style(|s| s.with_theme(|s, t| s.color(t.text_muted())))
2491                                .into_any()
2492                        } else {
2493                            style_debug_rows(&rows, true)
2494                        }
2495                    }
2496                    _ => Label::new("empty").into_any(),
2497                },
2498            ),
2499        ))
2500        .style(|s| s.width_full().gap(6.0))
2501        .into_any()
2502    }
2503}