1use 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 (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 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#[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#[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#[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}
196impl 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
264pub enum ObjectFit {
265 #[default]
270 Fill,
271 Contain,
276 Cover,
280 ScaleDown,
283 None,
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Default)]
290pub enum ObjectPosition {
291 TopLeft,
293 Top,
295 TopRight,
297 Left,
299 #[default]
301 Center,
302 Right,
304 BottomLeft,
306 Bottom,
308 BottomRight,
310 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 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 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 let image_aspect = 2.0;
358 let (img_width, img_height) = match object_fit {
359 ObjectFit::Fill => {
360 (container_size, container_size)
362 }
363 ObjectFit::Contain => {
364 let w = container_size;
367 let h = w / image_aspect;
368 (w, h)
369 }
370 ObjectFit::Cover => {
371 let h = container_size;
374 let w = h * image_aspect;
375 (w, h)
376 }
377 ObjectFit::ScaleDown => {
378 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 let natural_w = container_size * 0.6;
387 let natural_h = natural_w / image_aspect;
388 (natural_w, natural_h)
389 }
390 };
391
392 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 if matches!(object_fit, ObjectFit::Cover) {
398 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 let clipped_color = image_color.get().with_alpha(0.7);
405 cx.fill(&img_rect, clipped_color, 0.0);
406 } else {
407 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 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 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 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 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 _ => 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 _ => 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 let c = color.to_rgba8();
1040 let (r, g, b, a) = (c.r, c.g, c.b, c.a);
1041
1042 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 let rgba_str = format!("rgba({}, {}, {}, {:.3})", r, g, b, a as f32 / 255.0);
1051
1052 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#[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 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 let (scale, rotation) = affine.svd();
1530 let translation = affine.translation();
1531
1532 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 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 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 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 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 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 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 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 let trans_a = self.translation();
1732 let trans_b = other.translation();
1733
1734 let linear_a = self.with_translation(Vec2::ZERO);
1736 let linear_b = other.with_translation(Vec2::ZERO);
1737
1738 let (scale_a, rotation_a) = linear_a.svd();
1740 let (scale_b, rotation_b) = linear_b.svd();
1741
1742 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 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 let mut angle_diff = rotation_b - rotation_a;
1756 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 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 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#[derive(Debug, Clone, PartialEq, Eq)]
1829pub enum StyleMapValue<T> {
1830 Animated(T),
1832 Val(T),
1834 Context(ContextValue<T>),
1836 Unset,
1838}
1839
1840#[derive(Debug, Clone, PartialEq, Eq, Default)]
1845pub enum StyleValue<T> {
1846 Context(ContextValue<T>),
1848 Animated(T),
1850 Val(T),
1852 Unset,
1854 #[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}