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