Skip to main content

sequoia_view/
modifiers.rs

1use std::cell::Cell;
2use std::rc::Rc;
3use std::time::{Duration, Instant};
4
5use sequoia_animation::{Easing, animate};
6use sequoia_core::{Color, Edges, Length, Vec2};
7use sequoia_layout::{Indents, LayoutStyle, sanitize_indent};
8use sequoia_reactive::Signal;
9use sequoia_style::ShadowSpec;
10
11use crate::any::AnyView;
12use crate::filter_view::{EffectNode, FilterExt, Filtered};
13use crate::live::{AnimSpec, Live, Prop, SpringSpec};
14use crate::live_view::{LiveBackground, LiveOffset, LiveOpacity};
15use crate::presence::Presence;
16use crate::runtime::{ElementId, Runtime};
17use crate::view::{PaintCtx, View};
18use crate::views::Margined;
19
20#[derive(Clone, Debug)]
21pub struct Motion {
22    opacity: Option<(f32, f32)>,
23    offset: Option<(Vec2, Vec2)>,
24    spec: AnimSpec,
25}
26
27pub fn fade() -> Motion {
28    Motion {
29        opacity: Some((0.0, 1.0)),
30        offset: None,
31        spec: AnimSpec::SMOOTH,
32    }
33}
34
35pub fn slide_y(px: f32) -> Motion {
36    Motion {
37        opacity: None,
38        offset: Some((Vec2::new(0.0, px), Vec2::ZERO)),
39        spec: AnimSpec::SMOOTH,
40    }
41}
42
43impl Motion {
44    pub fn slide_y(mut self, px: f32) -> Self {
45        self.offset = Some((Vec2::new(0.0, px), Vec2::ZERO));
46        self
47    }
48
49    pub fn slide(mut self, dx: f32, dy: f32) -> Self {
50        self.offset = Some((Vec2::new(dx, dy), Vec2::ZERO));
51        self
52    }
53
54    pub fn fade_from(mut self, from: f32) -> Self {
55        self.opacity = Some((from.clamp(0.0, 1.0), 1.0));
56        self
57    }
58
59    pub fn duration(mut self, d: Duration) -> Self {
60        self.spec.duration_secs = d.as_secs_f32().max(0.0);
61        self
62    }
63
64    pub fn easing(mut self, easing: Easing) -> Self {
65        self.spec.easing = easing;
66        self
67    }
68
69    pub fn delay(mut self, d: Duration) -> Self {
70        self.spec.delay_secs = d.as_secs_f32().max(0.0);
71        self
72    }
73
74    pub fn preset(mut self, spec: impl Into<AnimSpec>) -> Self {
75        self.spec = spec.into();
76        self
77    }
78
79    pub fn sample_at(&self, elapsed_secs: f32) -> (Option<f32>, Option<Vec2>) {
80        let op = self
81            .opacity
82            .map(|(a, b)| self.spec.sample(a, b, elapsed_secs));
83        let off = self.offset.map(|(a, b)| {
84            Vec2::new(
85                self.spec.sample(a.x, b.x, elapsed_secs),
86                self.spec.sample(a.y, b.y, elapsed_secs),
87            )
88        });
89        (op, off)
90    }
91
92    pub fn is_running(&self, elapsed_secs: f32) -> bool {
93        (self.opacity.is_some() || self.offset.is_some()) && self.spec.is_running(elapsed_secs)
94    }
95
96    pub(crate) fn into_animate(self) -> sequoia_animation::Animate {
97        let mut b = animate()
98            .duration(Duration::from_secs_f32(self.spec.duration_secs))
99            .delay(Duration::from_secs_f32(self.spec.delay_secs))
100            .ease(self.spec.easing);
101        if let Some((a, c)) = self.opacity {
102            b = b.opacity(a..c);
103        }
104        if let Some((a, c)) = self.offset {
105            b = b.translate(a, c);
106        }
107        b.build()
108    }
109}
110
111impl From<AnimSpec> for Motion {
112    fn from(spec: AnimSpec) -> Self {
113        Motion {
114            opacity: Some((0.0, 1.0)),
115            offset: None,
116            spec,
117        }
118    }
119}
120
121impl From<Duration> for Motion {
122    fn from(d: Duration) -> Self {
123        Motion::from(AnimSpec::from(d))
124    }
125}
126
127#[derive(Clone)]
128pub struct When {
129    presence: Presence,
130}
131
132pub fn when<V: View + 'static>(present: Signal<bool>, child: V) -> When {
133    When {
134        // Motion is opt-in: `when` alone only mounts and unmounts the child,
135        // and the animation is enabled only by `.enter(...)` / `.exit(...)`.
136        presence: Presence::new(present).child(child).durations(0.0, 0.0),
137    }
138}
139
140impl When {
141    pub fn enter(mut self, motion: impl Into<Motion>) -> Self {
142        self.presence = self.presence.enter(motion.into().into_animate());
143        self
144    }
145
146    pub fn exit(mut self, motion: impl Into<Motion>) -> Self {
147        self.presence = self.presence.exit(motion.into().into_animate());
148        self
149    }
150
151    pub fn on_exit_complete(mut self, f: impl Fn() + 'static) -> Self {
152        self.presence = self.presence.on_exit_complete(f);
153        self
154    }
155
156    pub fn is_mounted(&self) -> bool {
157        self.presence.is_mounted()
158    }
159}
160
161impl View for When {
162    fn body(&self) -> impl View + 'static {
163        crate::views::Unit
164    }
165    fn reuse_state(&self, old: &Self) {
166        self.presence.reuse_state(&old.presence);
167    }
168    fn keyed_children(&self) -> Option<Vec<(u64, AnyView)>> {
169        self.presence.keyed_children()
170    }
171    fn layout_style(&self) -> LayoutStyle {
172        self.presence.layout_style()
173    }
174    fn paint_opacity_ctx(&self, rt: &Runtime, el: ElementId) -> Option<f32> {
175        self.presence.paint_opacity_ctx(rt, el)
176    }
177    fn paint_offset_ctx(&self, rt: &Runtime, el: ElementId) -> Vec2 {
178        self.presence.paint_offset_ctx(rt, el)
179    }
180    fn paint_scale_ctx(&self, rt: &Runtime, el: ElementId) -> f32 {
181        self.presence.paint_scale_ctx(rt, el)
182    }
183    fn paint_self(&self, ctx: &mut PaintCtx) {
184        self.presence.paint_self(ctx);
185    }
186}
187
188#[derive(Clone)]
189pub struct Clip {
190    child: AnyView,
191    radius: f32,
192}
193
194impl Clip {
195    pub fn new<V: View + 'static>(child: V, radius: f32) -> Self {
196        Self {
197            child: AnyView::new(child),
198            radius: radius.max(0.0),
199        }
200    }
201}
202
203impl View for Clip {
204    fn body(&self) -> impl View + 'static {
205        crate::views::Unit
206    }
207    fn children_override(&self) -> Option<Vec<AnyView>> {
208        Some(vec![self.child.clone()])
209    }
210    fn layout_style(&self) -> LayoutStyle {
211        LayoutStyle {
212            width: Length::Fit,
213            height: Length::Fit,
214            ..LayoutStyle::default()
215        }
216    }
217    fn clip_self(&self) -> bool {
218        true
219    }
220    fn clip_radius(&self) -> f32 {
221        self.radius
222    }
223}
224
225#[derive(Clone)]
226pub struct Scaled {
227    child: AnyView,
228    live: Live<f32>,
229}
230
231impl Scaled {
232    pub fn new<V: View + 'static>(child: V, live: impl Into<Live<f32>>) -> Self {
233        Self {
234            child: AnyView::new(child),
235            live: live.into(),
236        }
237    }
238
239    pub fn animate(mut self, spec: impl Into<AnimSpec>) -> Self {
240        self.live.animate_mut(spec);
241        self
242    }
243
244    pub fn spring(mut self, spec: SpringSpec) -> Self {
245        self.live.spring_mut(spec);
246        self
247    }
248
249    pub fn target_untracked(&self) -> f32 {
250        self.live.target_untracked()
251    }
252}
253
254impl View for Scaled {
255    fn body(&self) -> impl View + 'static {
256        crate::views::Unit
257    }
258    fn children_override(&self) -> Option<Vec<AnyView>> {
259        let _ = self.live.target();
260        Some(vec![self.child.clone()])
261    }
262    fn layout_style(&self) -> LayoutStyle {
263        LayoutStyle {
264            width: Length::Fit,
265            height: Length::Fit,
266            ..LayoutStyle::default()
267        }
268    }
269    fn paint_scale_ctx(&self, rt: &Runtime, el: ElementId) -> f32 {
270        rt.resolve_live_f32(el, Prop::SCALE, &self.live)
271    }
272    fn paint_self(&self, ctx: &mut PaintCtx) {
273        let target = self.live.target_untracked();
274        let (_, running) = ctx.runtime.live_f32(
275            ctx.element,
276            Prop::SCALE,
277            target,
278            self.live.tween_spec().as_ref(),
279            self.live.spring_spec().as_ref(),
280        );
281        if running {
282            ctx.request_frame();
283        }
284    }
285}
286
287#[derive(Clone)]
288pub struct Rotated {
289    child: AnyView,
290    live: Live<f32>,
291}
292
293impl Rotated {
294    pub fn new<V: View + 'static>(child: V, live: impl Into<Live<f32>>) -> Self {
295        Self {
296            child: AnyView::new(child),
297            live: live.into(),
298        }
299    }
300
301    pub fn animate(mut self, spec: impl Into<AnimSpec>) -> Self {
302        self.live.animate_mut(spec);
303        self
304    }
305
306    pub fn spring(mut self, spec: SpringSpec) -> Self {
307        self.live.spring_mut(spec);
308        self
309    }
310
311    pub fn target_untracked(&self) -> f32 {
312        self.live.target_untracked()
313    }
314}
315
316impl View for Rotated {
317    fn body(&self) -> impl View + 'static {
318        crate::views::Unit
319    }
320    fn children_override(&self) -> Option<Vec<AnyView>> {
321        let _ = self.live.target();
322        Some(vec![self.child.clone()])
323    }
324    fn layout_style(&self) -> LayoutStyle {
325        LayoutStyle {
326            width: Length::Fit,
327            height: Length::Fit,
328            ..LayoutStyle::default()
329        }
330    }
331    fn paint_rotation_ctx(&self, rt: &Runtime, el: ElementId) -> f32 {
332        rt.resolve_live_f32(el, Prop::ROTATION, &self.live)
333    }
334    fn paint_self(&self, ctx: &mut PaintCtx) {
335        let target = self.live.target_untracked();
336        let (_, running) = ctx.runtime.live_f32(
337            ctx.element,
338            Prop::ROTATION,
339            target,
340            self.live.tween_spec().as_ref(),
341            self.live.spring_spec().as_ref(),
342        );
343        if running {
344            ctx.request_frame();
345        }
346    }
347}
348
349#[derive(Clone)]
350pub struct Enter {
351    child: AnyView,
352    motion: Motion,
353    start: Rc<Cell<Option<f32>>>,
354    wall_start: Rc<Cell<Option<Instant>>>,
355}
356
357impl Enter {
358    pub fn new<V: View + 'static>(child: V, motion: impl Into<Motion>) -> Self {
359        Self {
360            child: AnyView::new(child),
361            motion: motion.into(),
362            start: Rc::new(Cell::new(None)),
363            wall_start: Rc::new(Cell::new(None)),
364        }
365    }
366
367    fn elapsed_ctx(&self, rt: &Runtime) -> f32 {
368        if rt.reduced_motion() {
369            return f32::MAX;
370        }
371        let now = rt.elapsed_secs();
372        let start = self.start.get().unwrap_or(now);
373        self.start.set(Some(start));
374        (now - start).max(0.0)
375    }
376
377    fn elapsed_wall(&self) -> f32 {
378        let now = Instant::now();
379        let start = self.wall_start.get().unwrap_or(now);
380        self.wall_start.set(Some(start));
381        now.saturating_duration_since(start).as_secs_f32()
382    }
383}
384
385impl View for Enter {
386    fn body(&self) -> impl View + 'static {
387        crate::views::Unit
388    }
389    fn children_override(&self) -> Option<Vec<AnyView>> {
390        Some(vec![self.child.clone()])
391    }
392    fn layout_style(&self) -> LayoutStyle {
393        LayoutStyle {
394            width: Length::Fit,
395            height: Length::Fit,
396            ..LayoutStyle::default()
397        }
398    }
399    fn paint_opacity(&self) -> Option<f32> {
400        self.motion.sample_at(self.elapsed_wall()).0
401    }
402    fn paint_offset(&self) -> Vec2 {
403        self.motion
404            .sample_at(self.elapsed_wall())
405            .1
406            .unwrap_or(Vec2::ZERO)
407    }
408    fn paint_opacity_ctx(&self, rt: &Runtime, _el: ElementId) -> Option<f32> {
409        self.motion.sample_at(self.elapsed_ctx(rt)).0
410    }
411    fn paint_offset_ctx(&self, rt: &Runtime, _el: ElementId) -> Vec2 {
412        self.motion
413            .sample_at(self.elapsed_ctx(rt))
414            .1
415            .unwrap_or(Vec2::ZERO)
416    }
417    fn paint_self(&self, ctx: &mut PaintCtx) {
418        if !ctx.runtime().reduced_motion()
419            && self.motion.is_running(self.elapsed_ctx(ctx.runtime()))
420        {
421            ctx.request_frame();
422        }
423    }
424}
425
426#[derive(Clone, Default)]
427pub struct StatePatch {
428    background: Option<Live<Color>>,
429    opacity: Option<Live<f32>>,
430    scale: Option<Live<f32>>,
431    offset: Option<Live<Vec2>>,
432    rotation: Option<Live<f32>>,
433    spec: Option<AnimSpec>,
434    spring: Option<SpringSpec>,
435}
436
437impl StatePatch {
438    pub fn new() -> Self {
439        Self::default()
440    }
441
442    pub fn background(mut self, color: impl Into<Live<Color>>) -> Self {
443        self.background = Some(color.into());
444        self
445    }
446
447    pub fn opacity(mut self, opacity: impl Into<Live<f32>>) -> Self {
448        self.opacity = Some(opacity.into());
449        self
450    }
451
452    pub fn scale(mut self, scale: impl Into<Live<f32>>) -> Self {
453        self.scale = Some(scale.into());
454        self
455    }
456
457    pub fn offset(mut self, offset: impl Into<Live<Vec2>>) -> Self {
458        self.offset = Some(offset.into());
459        self
460    }
461
462    pub fn translate(mut self, dx: f32, dy: f32) -> Self {
463        self.offset = Some(Live::fixed(Vec2::new(dx, dy)));
464        self
465    }
466
467    pub fn rotate(mut self, degrees: impl Into<Live<f32>>) -> Self {
468        self.rotation = Some(degrees.into());
469        self
470    }
471
472    pub fn animate(mut self, spec: impl Into<AnimSpec>) -> Self {
473        self.spec = Some(spec.into());
474        self.spring = None;
475        self
476    }
477
478    pub fn spring(mut self, spec: SpringSpec) -> Self {
479        self.spring = Some(spec);
480        self.spec = None;
481        self
482    }
483}
484
485#[derive(Clone)]
486pub struct States {
487    child: AnyView,
488    hover: Option<StatePatch>,
489    pressed: Option<StatePatch>,
490    focused: Option<StatePatch>,
491    selected: Option<StatePatch>,
492    disabled: Option<StatePatch>,
493    disabled_when: Option<Live<bool>>,
494    selected_when: Option<Live<bool>>,
495    radius: f32,
496}
497
498impl States {
499    pub fn new<V: View + 'static>(child: V) -> Self {
500        Self {
501            child: AnyView::new(child),
502            hover: None,
503            pressed: None,
504            focused: None,
505            selected: None,
506            disabled: None,
507            disabled_when: None,
508            selected_when: None,
509            radius: 0.0,
510        }
511    }
512
513    pub fn hover(mut self, build: impl FnOnce(StatePatch) -> StatePatch) -> Self {
514        self.hover = Some(build(StatePatch::new()));
515        self
516    }
517
518    pub fn hover_patch(mut self, patch: StatePatch) -> Self {
519        self.hover = Some(patch);
520        self
521    }
522
523    pub fn pressed(mut self, build: impl FnOnce(StatePatch) -> StatePatch) -> Self {
524        self.pressed = Some(build(StatePatch::new()));
525        self
526    }
527
528    pub fn pressed_patch(mut self, patch: StatePatch) -> Self {
529        self.pressed = Some(patch);
530        self
531    }
532
533    pub fn focused(mut self, build: impl FnOnce(StatePatch) -> StatePatch) -> Self {
534        self.focused = Some(build(StatePatch::new()));
535        self
536    }
537
538    pub fn focused_patch(mut self, patch: StatePatch) -> Self {
539        self.focused = Some(patch);
540        self
541    }
542
543    pub fn selected(mut self, build: impl FnOnce(StatePatch) -> StatePatch) -> Self {
544        self.selected = Some(build(StatePatch::new()));
545        self
546    }
547
548    pub fn selected_patch(mut self, patch: StatePatch) -> Self {
549        self.selected = Some(patch);
550        self
551    }
552
553    pub fn disabled(mut self, build: impl FnOnce(StatePatch) -> StatePatch) -> Self {
554        self.disabled = Some(build(StatePatch::new()));
555        self
556    }
557
558    pub fn disabled_patch(mut self, patch: StatePatch) -> Self {
559        self.disabled = Some(patch);
560        self
561    }
562
563    pub fn disabled_when(mut self, cond: impl Into<Live<bool>>) -> Self {
564        self.disabled_when = Some(cond.into());
565        self
566    }
567
568    pub fn selected_when(mut self, cond: impl Into<Live<bool>>) -> Self {
569        self.selected_when = Some(cond.into());
570        self
571    }
572
573    pub fn radius(mut self, radius: f32) -> Self {
574        self.radius = radius.max(0.0);
575        self
576    }
577
578    fn touch(live: &Live<Color>) {
579        let _ = live.target();
580    }
581    fn touch_f32(live: &Live<f32>) {
582        let _ = live.target();
583    }
584    fn touch_bool(live: &Live<bool>) {
585        let _ = live.target();
586    }
587
588    fn touch_patch(p: &StatePatch) {
589        if let Some(v) = &p.background {
590            Self::touch(v);
591        }
592        if let Some(v) = &p.opacity {
593            Self::touch_f32(v);
594        }
595        if let Some(v) = &p.scale {
596            Self::touch_f32(v);
597        }
598        if let Some(v) = &p.offset {
599            let _ = v.target();
600        }
601        if let Some(v) = &p.rotation {
602            Self::touch_f32(v);
603        }
604    }
605
606    /// Patches that apply for the current visual state, highest priority first.
607    ///
608    /// Disabled is exclusive. Other states compose per property, so a hovered
609    /// and selected control can animate both its own hover background and its
610    /// selected accent without one clobbering the other.
611    fn layers(&self, hovered: bool, pressed: bool, focused: bool) -> Vec<&StatePatch> {
612        let disabled = self
613            .disabled_when
614            .as_ref()
615            .map(|c| c.target_untracked())
616            .unwrap_or(false);
617        if disabled && let Some(d) = self.disabled.as_ref() {
618            return vec![d];
619        }
620
621        let selected = self
622            .selected_when
623            .as_ref()
624            .map(|c| c.target_untracked())
625            .unwrap_or(false);
626
627        let mut out = Vec::with_capacity(4);
628        if pressed && let Some(p) = self.pressed.as_ref() {
629            out.push(p);
630        }
631        if hovered && let Some(p) = self.hover.as_ref() {
632            out.push(p);
633        }
634        if focused && let Some(p) = self.focused.as_ref() {
635            out.push(p);
636        }
637        if selected && let Some(p) = self.selected.as_ref() {
638            out.push(p);
639        }
640        out
641    }
642
643    fn pick<'a, T>(
644        layers: &[&'a StatePatch],
645        f: impl Fn(&'a StatePatch) -> Option<&'a Live<T>>,
646    ) -> Option<(&'a Live<T>, &'a StatePatch)> {
647        layers.iter().find_map(|p| f(p).map(|live| (live, *p)))
648    }
649
650    #[cfg(test)]
651    fn active_patch(&self, hovered: bool, pressed: bool, focused: bool) -> Option<&StatePatch> {
652        self.layers(hovered, pressed, focused).into_iter().next()
653    }
654
655    fn eff_live<T: Clone + PartialEq + 'static>(patch: &StatePatch, live: &Live<T>) -> Live<T> {
656        if live.tween_spec().is_some() || live.spring_spec().is_some() {
657            return live.clone();
658        }
659        let mut out = live.clone();
660        if let Some(s) = patch.spec {
661            out.animate_mut(s);
662        } else if let Some(s) = patch.spring {
663            out.spring_mut(s);
664        }
665        out
666    }
667
668    fn eff_f32(patch: &StatePatch, live: &Live<f32>) -> Live<f32> {
669        Self::eff_live(patch, live)
670    }
671
672    fn eff_color(patch: &StatePatch, live: &Live<Color>) -> Live<Color> {
673        if live.tween_spec().is_some() {
674            return live.clone();
675        }
676        let mut out = live.clone();
677        if let Some(s) = patch.spec {
678            out.animate_mut(s);
679        }
680        out
681    }
682}
683
684impl View for States {
685    fn body(&self) -> impl View + 'static {
686        crate::views::Unit
687    }
688    fn children_override(&self) -> Option<Vec<AnyView>> {
689        for p in [
690            &self.hover,
691            &self.pressed,
692            &self.focused,
693            &self.selected,
694            &self.disabled,
695        ]
696        .into_iter()
697        .flatten()
698        {
699            Self::touch_patch(p);
700        }
701        if let Some(c) = &self.disabled_when {
702            Self::touch_bool(c);
703        }
704        if let Some(c) = &self.selected_when {
705            Self::touch_bool(c);
706        }
707        Some(vec![self.child.clone()])
708    }
709    fn layout_style(&self) -> LayoutStyle {
710        LayoutStyle {
711            width: Length::Fit,
712            height: Length::Fit,
713            ..LayoutStyle::default()
714        }
715    }
716    fn has_visual_state(&self) -> bool {
717        true
718    }
719    fn paint_opacity_ctx(&self, rt: &Runtime, el: ElementId) -> Option<f32> {
720        let st = rt.visual_state(el);
721        let layers = self.layers(st.hovered, st.pressed, st.focused);
722        let (live, patch) = Self::pick(&layers, |p| p.opacity.as_ref())?;
723        let live = Self::eff_f32(patch, live);
724        Some(
725            rt.resolve_live_f32(el, Prop::OPACITY, &live)
726                .clamp(0.0, 1.0),
727        )
728    }
729
730    fn paint_scale_ctx(&self, rt: &Runtime, el: ElementId) -> f32 {
731        let st = rt.visual_state(el);
732        let layers = self.layers(st.hovered, st.pressed, st.focused);
733        let Some((live, patch)) = Self::pick(&layers, |p| p.scale.as_ref()) else {
734            return 1.0;
735        };
736        let live = Self::eff_f32(patch, live);
737        rt.resolve_live_f32(el, Prop::SCALE, &live)
738    }
739
740    fn paint_offset_ctx(&self, rt: &Runtime, el: ElementId) -> Vec2 {
741        let st = rt.visual_state(el);
742        let layers = self.layers(st.hovered, st.pressed, st.focused);
743        let Some((live, patch)) = Self::pick(&layers, |p| p.offset.as_ref()) else {
744            return Vec2::ZERO;
745        };
746        let live = Self::eff_live(patch, live);
747        rt.resolve_live_vec2(el, Prop::OFFSET, &live)
748    }
749
750    fn paint_rotation_ctx(&self, rt: &Runtime, el: ElementId) -> f32 {
751        let st = rt.visual_state(el);
752        let layers = self.layers(st.hovered, st.pressed, st.focused);
753        let Some((live, patch)) = Self::pick(&layers, |p| p.rotation.as_ref()) else {
754            return 0.0;
755        };
756        let live = Self::eff_f32(patch, live);
757        rt.resolve_live_f32(el, Prop::ROTATION, &live)
758    }
759
760    fn paint_self(&self, ctx: &mut PaintCtx) {
761        let st = ctx.state;
762        let layers = self.layers(st.hovered, st.pressed, st.focused);
763        if layers.is_empty() {
764            return;
765        }
766
767        let mut running = false;
768        if let Some((live, patch)) = Self::pick(&layers, |p| p.background.as_ref()) {
769            let live = Self::eff_color(patch, live);
770            let target = live.target_untracked();
771            let (color, r) = ctx.runtime.live_color(
772                ctx.element,
773                Prop::BACKGROUND,
774                target,
775                live.tween_spec().as_ref(),
776            );
777            running |= r;
778            if color.a > 0.0 {
779                ctx.quad(ctx.rect, color, self.radius);
780            }
781        }
782        if let Some((live, patch)) = Self::pick(&layers, |p| p.opacity.as_ref()) {
783            let live = Self::eff_f32(patch, live);
784            let target = live.target_untracked();
785            let (_, r) = ctx.runtime.live_f32(
786                ctx.element,
787                Prop::OPACITY,
788                target,
789                live.tween_spec().as_ref(),
790                live.spring_spec().as_ref(),
791            );
792            running |= r;
793        }
794        if let Some((live, patch)) = Self::pick(&layers, |p| p.scale.as_ref()) {
795            let live = Self::eff_f32(patch, live);
796            let target = live.target_untracked();
797            let (_, r) = ctx.runtime.live_f32(
798                ctx.element,
799                Prop::SCALE,
800                target,
801                live.tween_spec().as_ref(),
802                live.spring_spec().as_ref(),
803            );
804            running |= r;
805        }
806        if let Some((live, patch)) = Self::pick(&layers, |p| p.offset.as_ref()) {
807            let live = Self::eff_live(patch, live);
808            let target = live.target_untracked();
809            let (_, r) = ctx.runtime.live_vec2(
810                ctx.element,
811                Prop::OFFSET,
812                target,
813                live.tween_spec().as_ref(),
814                live.spring_spec().as_ref(),
815            );
816            running |= r;
817        }
818        if let Some((live, patch)) = Self::pick(&layers, |p| p.rotation.as_ref()) {
819            let live = Self::eff_f32(patch, live);
820            let target = live.target_untracked();
821            let (_, r) = ctx.runtime.live_f32(
822                ctx.element,
823                Prop::ROTATION,
824                target,
825                live.tween_spec().as_ref(),
826                live.spring_spec().as_ref(),
827            );
828            running |= r;
829        }
830        if running {
831            ctx.request_frame();
832        }
833    }
834}
835
836pub trait ViewModifiers: View + Sized + 'static {
837    fn opacity(self, opacity: impl Into<Live<f32>>) -> LiveOpacity {
838        LiveOpacity::new(self, opacity)
839    }
840
841    fn offset(self, dx: f32, dy: f32) -> LiveOffset {
842        LiveOffset::new(self, Live::fixed(Vec2::new(dx, dy)))
843    }
844
845    fn offset_by(self, offset: impl Into<Live<Vec2>>) -> LiveOffset {
846        LiveOffset::new(self, offset)
847    }
848
849    fn background(self, color: impl Into<Live<Color>>) -> LiveBackground {
850        LiveBackground::new(self, color)
851    }
852
853    fn bg(self, color: impl Into<Live<Color>>) -> LiveBackground {
854        LiveBackground::new(self, color)
855    }
856
857    fn blur(self, px: f32) -> Filtered {
858        self.blur_own(px)
859    }
860
861    /// Wraps the view in an [`EffectNode`] that paints `spec` behind it.
862    ///
863    /// This changes the builder type, so finish configuring the inner view
864    /// first: call `.child(...)`, `.padding(...)`, and other builder methods
865    /// **before** `.shadow(...)`. To add more builders after the effect, wrap
866    /// the already-built view explicitly with
867    /// `EffectNode::new(view, Effect::Shadow(spec))`, or set the shadow through
868    /// `BoxStyle::shadow` where the view supports a box style (for example
869    /// `Container::box_style`). [`EffectNode`] has no `.child()` method.
870    fn shadow(self, spec: ShadowSpec) -> EffectNode {
871        self.effect(sequoia_style::Effect::Shadow(spec))
872    }
873
874    fn clip(self) -> Clip {
875        Clip::new(self, 0.0)
876    }
877
878    fn clip_rounded(self, radius: f32) -> Clip {
879        Clip::new(self, radius)
880    }
881
882    fn scale(self, scale: impl Into<Live<f32>>) -> Scaled {
883        Scaled::new(self, scale)
884    }
885
886    fn rotate(self, degrees: impl Into<Live<f32>>) -> Rotated {
887        Rotated::new(self, degrees)
888    }
889
890    fn width_live(self, width: impl Into<Live<f32>>) -> crate::layout_live::AnimatedSize {
891        crate::layout_live::AnimatedSize::width(self, width)
892    }
893
894    fn height_live(self, height: impl Into<Live<f32>>) -> crate::layout_live::AnimatedSize {
895        crate::layout_live::AnimatedSize::height(self, height)
896    }
897
898    fn size_live(
899        self,
900        width: impl Into<Live<f32>>,
901        height: impl Into<Live<f32>>,
902    ) -> crate::layout_live::AnimatedSize {
903        crate::layout_live::AnimatedSize::size(self, width, height)
904    }
905
906    fn animate(self, spec: impl Into<AnimSpec>) -> Enter {
907        Enter::new(self, Motion::from(spec.into()))
908    }
909
910    fn enter(self, motion: impl Into<Motion>) -> Enter {
911        Enter::new(self, motion)
912    }
913
914    fn hover(self, build: impl FnOnce(StatePatch) -> StatePatch) -> States {
915        States::new(self).hover_patch(build(StatePatch::new()))
916    }
917
918    fn pressed(self, build: impl FnOnce(StatePatch) -> StatePatch) -> States {
919        States::new(self).pressed_patch(build(StatePatch::new()))
920    }
921
922    fn focused(self, build: impl FnOnce(StatePatch) -> StatePatch) -> States {
923        States::new(self).focused_patch(build(StatePatch::new()))
924    }
925
926    fn selected(self, build: impl FnOnce(StatePatch) -> StatePatch) -> States {
927        States::new(self).selected_patch(build(StatePatch::new()))
928    }
929
930    fn disabled(self, build: impl FnOnce(StatePatch) -> StatePatch) -> States {
931        States::new(self).disabled_patch(build(StatePatch::new()))
932    }
933
934    fn state(self, build: impl FnOnce(States) -> States) -> States {
935        build(States::new(self))
936    }
937
938    fn margin(self, margin: impl Into<Edges>) -> Margined {
939        Margined::new(self, margin)
940    }
941
942    fn margin_xy(self, x: f32, y: f32) -> Margined {
943        Margined::new(self, Edges::xy(x, y))
944    }
945
946    fn margin_x(self, x: f32) -> Margined {
947        Margined::new(self, Edges::horizontal(x))
948    }
949
950    fn margin_y(self, y: f32) -> Margined {
951        Margined::new(self, Edges::vertical(y))
952    }
953
954    fn margin_left(self, v: f32) -> Margined {
955        Margined::new(self, Edges::left(v))
956    }
957
958    fn margin_right(self, v: f32) -> Margined {
959        Margined::new(self, Edges::right(v))
960    }
961
962    fn margin_top(self, v: f32) -> Margined {
963        Margined::new(self, Edges::top(v))
964    }
965
966    fn margin_bottom(self, v: f32) -> Margined {
967        Margined::new(self, Edges::bottom(v))
968    }
969
970    fn indent_top(self, v: f32) -> Margined {
971        Margined::with_indents(
972            self,
973            Indents::only(None, Some(sanitize_indent(v)), None, None),
974        )
975    }
976
977    fn indent_bottom(self, v: f32) -> Margined {
978        Margined::with_indents(
979            self,
980            Indents::only(None, None, None, Some(sanitize_indent(v))),
981        )
982    }
983
984    fn indent_left(self, v: f32) -> Margined {
985        Margined::with_indents(
986            self,
987            Indents::only(Some(sanitize_indent(v)), None, None, None),
988        )
989    }
990
991    fn indent_right(self, v: f32) -> Margined {
992        Margined::with_indents(
993            self,
994            Indents::only(None, None, Some(sanitize_indent(v)), None),
995        )
996    }
997
998    fn indent(self, indents: Indents) -> Margined {
999        Margined::with_indents(self, indents)
1000    }
1001
1002    fn indent_all(self, v: f32) -> Margined {
1003        Margined::with_indents(self, Indents::all(v))
1004    }
1005
1006    fn indent_xy(self, x: f32, y: f32) -> Margined {
1007        Margined::with_indents(self, Indents::xy(x, y))
1008    }
1009
1010    fn indent_axes(self, horizontal: f32, vertical: f32) -> Margined {
1011        Margined::with_indents(self, Indents::axes(horizontal, vertical))
1012    }
1013}
1014
1015impl<T: View + 'static> ViewModifiers for T {}
1016
1017#[cfg(test)]
1018mod tests {
1019    use super::*;
1020    use crate::value::ValueExt;
1021
1022    #[test]
1023    fn motion_fade_and_slide_sample() {
1024        let m = fade().slide_y(12.0);
1025        let (op0, off0) = m.sample_at(0.0);
1026        assert_eq!(op0, Some(0.0));
1027        assert_eq!(off0, Some(Vec2::new(0.0, 12.0)));
1028        let (op1, off1) = m.sample_at(10.0);
1029        assert_eq!(op1, Some(1.0));
1030        assert_eq!(off1, Some(Vec2::ZERO));
1031        assert!(m.is_running(0.05));
1032        assert!(!m.is_running(10.0));
1033    }
1034
1035    #[test]
1036    fn motion_from_spec_and_duration() {
1037        let a = Motion::from(AnimSpec::SNAPPY);
1038        assert_eq!(a.sample_at(0.0).0, Some(0.0));
1039        let b = Motion::from(Duration::from_millis(200));
1040        assert_eq!(b.sample_at(10.0).0, Some(1.0));
1041    }
1042
1043    #[test]
1044    fn modifiers_chain_static_values() {
1045        let v = crate::views::Unit
1046            .opacity(0.5)
1047            .offset(3.0, -4.0)
1048            .background(Color::WHITE)
1049            .blur(2.0)
1050            .clip_rounded(6.0)
1051            .scale(1.03);
1052        assert_eq!(v.paint_opacity(), None);
1053        assert_eq!(v.target_untracked(), 1.03);
1054        let r = crate::views::Unit.rotate(15.0);
1055
1056        assert_eq!(r.paint_rotation(), 0.0);
1057        assert_eq!(r.target_untracked(), 15.0);
1058    }
1059
1060    #[test]
1061    fn post_hoc_animate_attaches_spec() {
1062        let v = crate::views::Unit.opacity(0.0).animate(AnimSpec::SNAPPY);
1063        let _ = v.children_override();
1064        let v = crate::views::Unit
1065            .scale(1.0)
1066            .spring(SpringSpec::BOUNCY)
1067            .offset(0.0, 8.0)
1068            .animate(AnimSpec::SMOOTH);
1069        let _ = v.children_override();
1070    }
1071
1072    #[test]
1073    fn reactive_value_flows_into_modifier() {
1074        let s = Signal::new(0.8f32);
1075        let v = crate::views::Unit.opacity(s.clone().animated(AnimSpec::FAST));
1076        let kids = v.children_override().expect("dziecko");
1077        assert_eq!(kids.len(), 1);
1078        s.set(0.2);
1079        let _ = v.children_override().expect("dziecko");
1080    }
1081
1082    #[test]
1083    fn states_accept_patch_and_builder() {
1084        let a = crate::views::Unit.state(|s| s.hover_patch(StatePatch::new().opacity(0.5)));
1085        let b = crate::views::Unit.hover(|h| {
1086            h.background(Color::WHITE)
1087                .opacity(0.9)
1088                .animate(AnimSpec::FAST)
1089        });
1090        let c = crate::views::Unit.state(|s| {
1091            s.hover(|h| h.opacity(1.0))
1092                .pressed(|p| p.scale(0.97).spring(SpringSpec::SNAPPY))
1093                .focused(|f| f.opacity(1.0))
1094                .disabled(|d| d.opacity(0.5))
1095        });
1096        assert!(a.hover.is_some());
1097        assert!(b.hover.as_ref().unwrap().background.is_some());
1098        assert!(c.pressed.as_ref().unwrap().scale.is_some());
1099
1100        assert!(c.active_patch(true, true, false).unwrap().scale.is_some());
1101        assert!(
1102            c.active_patch(true, false, false)
1103                .unwrap()
1104                .opacity
1105                .is_some()
1106        );
1107        assert!(c.active_patch(false, false, false).is_none());
1108    }
1109
1110    #[test]
1111    fn states_compose_layers_per_property() {
1112        let v = crate::views::Unit.state(|s| {
1113            s.hover(|h| h.opacity(0.8))
1114                .focused(|f| f.scale(1.05))
1115                .selected(|p| p.translate(4.0, 2.0).rotate(12.0))
1116                .selected_when(true)
1117        });
1118        let layers = v.layers(true, false, true);
1119        assert_eq!(layers.len(), 3, "hover, focus and selected all apply");
1120        assert!(layers[0].opacity.is_some(), "hover wins opacity");
1121        assert!(layers[1].scale.is_some(), "focus wins scale");
1122        assert!(layers[2].offset.is_some(), "selected supplies offset");
1123        assert!(layers[2].rotation.is_some(), "selected supplies rotation");
1124
1125        let (offset, _) = States::pick(&layers, |p| p.offset.as_ref()).expect("offset");
1126        assert_eq!(offset.target_untracked(), Vec2::new(4.0, 2.0));
1127        let (rotation, _) = States::pick(&layers, |p| p.rotation.as_ref()).expect("rotation");
1128        assert_eq!(rotation.target_untracked(), 12.0);
1129    }
1130
1131    #[test]
1132    fn when_builds_with_enter_exit() {
1133        let open = Signal::new(true);
1134        let w = when(open, crate::views::Unit)
1135            .enter(fade().slide_y(-8.0))
1136            .exit(fade().slide_y(-8.0));
1137        let _ = w.keyed_children();
1138    }
1139}