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