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).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 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 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}