1use std::collections::HashMap;
9use std::ops::Range;
10
11use ravenrs_animation::{Animated, Transition};
12use ravenrs_core::{Border, Brush, Color, Effect, Point, Primitive, Rect, Shadow};
13use ravenrs_layout::{Axis, LayoutStyle};
14
15use crate::context::{BuildCx, NodeId, Paint, QuadPaint, WidgetId};
16use crate::{AnyView, View};
17
18#[derive(Clone, Copy, Debug)]
32#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
33pub struct Motion {
34 pub(crate) paint: Option<Transition>,
35 pub(crate) layout: Option<Transition>,
36 pub(crate) shape_only: bool,
37 pub(crate) underlay: bool,
38 pub(crate) shared: Option<(WidgetId, u64)>,
39 pub(crate) surface_exit: bool,
40 pub(crate) effect: (Effect, Transition),
41 pub(crate) enter: Option<(Effect, Transition)>,
42 pub(crate) exit: Option<(Effect, Transition)>,
43 pub(crate) key: Option<u64>,
44 pub(crate) anchored: bool,
45}
46
47impl Default for Motion {
48 fn default() -> Self {
49 Self::new()
50 }
51}
52
53#[cfg_attr(feature = "live", ravenrs_live::expose)]
54impl Motion {
55 pub const fn new() -> Self {
57 Self {
58 paint: None,
59 layout: None,
60 shape_only: false,
61 underlay: false,
62 shared: None,
63 surface_exit: false,
64 effect: (Effect::IDENTITY, Transition::Instant),
65 enter: None,
66 exit: None,
67 key: None,
68 anchored: false,
69 }
70 }
71
72 pub const fn paint(mut self, transition: Transition) -> Self {
76 self.paint = Some(transition);
77 self
78 }
79
80 pub const fn layout(mut self, transition: Transition) -> Self {
83 self.layout = Some(transition);
84 self
85 }
86
87 pub const fn shared_layout(mut self, scope: WidgetId, tag: u64, transition: Transition) -> Self {
92 self.shared = Some((scope, tag));
93 self.layout = Some(transition);
94 self
95 }
96
97 pub const fn surface_exit(mut self) -> Self {
102 self.surface_exit = true;
103 self
104 }
105
106 pub const fn shape_only(mut self) -> Self {
109 self.shape_only = true;
110 self
111 }
112
113 pub const fn underlay(mut self) -> Self {
117 self.underlay = true;
118 self
119 }
120
121 pub const fn effect(mut self, effect: Effect, transition: Transition) -> Self {
124 self.effect = (effect, transition);
125 self
126 }
127
128 pub const fn enter(mut self, from: Effect, transition: Transition) -> Self {
130 self.enter = Some((from, transition));
131 self
132 }
133
134 pub const fn exit(mut self, to: Effect, transition: Transition) -> Self {
139 self.exit = Some((to, transition));
140 self
141 }
142
143 pub const fn key(mut self, key: u64) -> Self {
146 self.key = Some(key);
147 self
148 }
149
150 pub const fn anchored(mut self) -> Self {
153 self.anchored = true;
154 self
155 }
156}
157
158#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
160pub(crate) enum MotionKey {
161 Node(WidgetId, u64),
162 Shared(WidgetId, u64),
163}
164
165#[derive(Clone, Copy, Debug)]
167pub(crate) struct MotionEntry {
168 pub node: NodeId,
169 pub key: MotionKey,
170 pub motion: Motion,
171}
172
173#[derive(Debug)]
174struct PaintTrack {
175 fill: Animated<Color>,
176 radius: Animated<f32>,
177 border_color: Animated<Color>,
178 border_width: Animated<f32>,
179 shadow_color: Animated<Color>,
180 shadow_shape: Animated<Rect>,
181}
182
183#[cfg_attr(feature = "live", ravenrs_live::expose)]
184impl PaintTrack {
185 fn new(q: &QuadPaint) -> Self {
186 let (border, shadow) = (border_of(q, None), shadow_of(q, None));
187 Self {
188 fill: Animated::new(solid(q.fill)),
189 radius: Animated::new(q.corner_radius),
190 border_color: Animated::new(border.color),
191 border_width: Animated::new(border.width),
192 shadow_color: Animated::new(shadow.color),
193 shadow_shape: Animated::new(shadow_rect(&shadow)),
194 }
195 }
196
197 fn set_target(&mut self, q: &QuadPaint, t: Transition, now: f64) {
198 let was_border = self.border_width.target();
199 let was_shadow = self.shadow_shape.target();
200 let (border, shadow) = (border_of(q, Some(was_border)), shadow_of(q, Some(was_shadow)));
201 self.fill.set_target(solid(q.fill), t, now);
202 self.radius.set_target(q.corner_radius, t, now);
203 self.border_color.set_target(border.color, t, now);
204 self.border_width.set_target(border.width, t, now);
205 self.shadow_color.set_target(shadow.color, t, now);
206 self.shadow_shape.set_target(shadow_rect(&shadow), t, now);
207 }
208
209 fn sample(&self, q: &QuadPaint, now: f64) -> QuadPaint {
210 let fill = match q.fill {
211 Brush::Solid(_) => Brush::Solid(self.fill.value(now)),
212 other => other,
213 };
214 let border_color = self.border_color.value(now);
215 let border = (border_color.a > 0.003)
216 .then(|| Border { width: self.border_width.value(now).max(0.0), color: border_color });
217 let shadow_color = self.shadow_color.value(now);
218 let shape = self.shadow_shape.value(now);
219 let shadow = (shadow_color.a > 0.003).then_some(Shadow {
220 offset: shape.origin,
221 blur: shape.width(),
222 color: shadow_color,
223 });
224 QuadPaint { fill, corner_radius: self.radius.value(now).max(0.0), border, shadow }
225 }
226
227 fn step(&mut self, now: f64) -> bool {
228 self.fill.step(now)
230 | self.radius.step(now)
231 | self.border_color.step(now)
232 | self.border_width.step(now)
233 | self.shadow_color.step(now)
234 | self.shadow_shape.step(now)
235 }
236}
237
238fn solid(brush: Brush) -> Color {
239 match brush {
240 Brush::Solid(c) => c,
241 Brush::LinearGradient(g) => g.to,
242 _ => Color::TRANSPARENT,
243 }
244}
245
246fn border_of(q: &QuadPaint, was_width: Option<f32>) -> Border {
248 q.border.unwrap_or(Border { width: was_width.unwrap_or(1.0), color: solid(q.fill).with_alpha(0.0) })
249}
250
251fn shadow_of(q: &QuadPaint, was: Option<Rect>) -> Shadow {
252 q.shadow.unwrap_or_else(|| {
253 let was = was.unwrap_or(Rect::ZERO);
254 Shadow { offset: was.origin, blur: was.width(), color: Color::TRANSPARENT }
255 })
256}
257
258fn shadow_rect(s: &Shadow) -> Rect {
259 Rect::new(s.offset.x, s.offset.y, s.blur, 0.0)
260}
261
262#[derive(Debug, Default)]
264struct Snapshot {
265 primitives: Vec<Primitive>,
266 nested: Vec<NestedLayer>,
267 clip: Rect,
268 pivot: Point,
269 flip: bool,
270 layer: GhostLayer,
271 order: u64,
273 valid: bool,
274}
275
276#[derive(Clone, Debug)]
279pub(crate) struct NestedLayer {
280 range: Range<usize>,
281 pivot: Point,
282 exit: (Effect, Transition),
283}
284
285#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
286pub(crate) enum GhostLayer {
287 #[default]
289 Main,
290 Overlay,
292 Top,
294}
295
296#[derive(Debug)]
297struct Track {
298 seen: u64,
299 effect: Animated<Effect>,
300 paint: Option<PaintTrack>,
301 text: Option<Animated<Color>>,
302 rect: Option<Animated<Rect>>,
303 owner: Option<WidgetId>,
305 exit: Option<(Effect, Transition)>,
306 surface_exit: bool,
307 snapshot: Snapshot,
308}
309
310#[derive(Debug)]
311struct Ghost {
312 key: Option<MotionKey>,
313 primitives: Vec<Primitive>,
314 clip: Rect,
315 pivot: Point,
316 layer: GhostLayer,
317 effect: Animated<Effect>,
318 nested: Vec<(Range<usize>, Point, Animated<Effect>)>,
319 surface_exit: bool,
320}
321
322#[derive(Debug)]
324pub(crate) struct Sampled {
325 pub quad: Option<QuadPaint>,
326 pub text: Option<Color>,
327 pub frame: Option<Rect>,
329 pub effect: Effect,
330}
331
332#[derive(Debug, Default)]
334pub(crate) struct MotionSystem {
335 tracks: HashMap<MotionKey, Track>,
336 ghosts: Vec<Ghost>,
337 captures: u64,
338 build: u64,
339 pub active: bool,
341 pub clock: f64,
343 pub enabled: bool,
346 pub reduced: bool,
347 capture_depth: u32,
349 nested: Vec<NestedLayer>,
350}
351
352#[cfg_attr(feature = "live", ravenrs_live::expose)]
353impl MotionSystem {
354 fn adapt(&self, effect: Effect, t: Transition) -> (Effect, Transition) {
356 if self.reduced {
357 (Effect { opacity: effect.opacity, ..Effect::IDENTITY }, t.without_bounce())
358 } else {
359 (effect, t)
360 }
361 }
362
363 pub fn sync(&mut self, entries: &[MotionEntry], paints: impl Fn(NodeId) -> NodePaint, snap: bool) {
366 self.build += 1;
367 let (now, build) = (self.clock, self.build);
368 let mut started = !self.tracks.is_empty() || self.build > 1;
369 started &= self.enabled;
370 for entry in entries {
371 let m = &entry.motion;
372 let (target, target_t) = self.adapt(m.effect.0, m.effect.1);
373 let track = match self.tracks.get_mut(&entry.key) {
374 Some(track) => {
375 let t = if self.enabled { target_t } else { Transition::Instant };
376 track.effect.set_target(target, t, now);
377 track
378 }
379 None => {
380 let revived = self.ghosts.iter().position(|g| g.key == Some(entry.key));
381 let (from, t) = match (revived, m.enter) {
382 (Some(i), _) => (self.ghosts.remove(i).effect.value(now), m.enter.map(|e| e.1)),
383 (None, Some((from, t))) if started => {
384 let (from, t) = self.adapt(from, t);
385 (from, Some(t))
386 }
387 _ => (target, None),
388 };
389 let mut effect = Animated::new(from);
390 effect.set_target(target, t.unwrap_or(target_t), now);
391 self.tracks.entry(entry.key).or_insert(Track {
392 seen: build,
393 effect,
394 paint: None,
395 text: None,
396 rect: None,
397 owner: None,
398 exit: None,
399 surface_exit: false,
400 snapshot: Snapshot::default(),
401 })
402 }
403 };
404 track.seen = build;
405 track.surface_exit = m.surface_exit;
406 track.exit = m.exit.map(|(e, t)| {
407 if self.reduced {
408 (Effect { opacity: e.opacity, ..Effect::IDENTITY }, t.without_bounce())
409 } else {
410 (e, t)
411 }
412 });
413 if let Some(t) = m.paint {
414 let t = if snap || !self.enabled {
415 Transition::Instant
416 } else if self.reduced {
417 t.without_bounce()
418 } else {
419 t
420 };
421 match paints(entry.node) {
422 NodePaint::Quad(q) => match &mut track.paint {
423 Some(p) => p.set_target(&q, t, now),
424 None => track.paint = Some(PaintTrack::new(&q)),
425 },
426 NodePaint::Text(c) => match &mut track.text {
427 Some(a) => a.set_target(c, t, now),
428 None => track.text = Some(Animated::new(c)),
429 },
430 NodePaint::Other => {}
431 }
432 }
433 }
434 let mut gone: Vec<(u64, MotionKey)> = self
436 .tracks
437 .iter()
438 .filter(|(_, t)| t.seen != build)
439 .map(|(k, t)| (t.snapshot.order, *k))
440 .collect();
441 gone.sort_unstable_by_key(|(order, _)| *order);
442 for (_, key) in gone {
443 let Some(track) = self.tracks.remove(&key) else { continue };
444 let Some((to, t)) = track.exit else { continue };
445 if !self.enabled || !track.snapshot.valid {
446 continue;
447 }
448 let from = track.effect.value(now);
449 let snapshot = track.snapshot;
450 let flip = snapshot.flip;
451 let mut effect = Animated::new(from);
452 effect.set_target(flipped(to, flip), t, now);
453 let nested = snapshot.nested.iter().map(|n| {
454 let mut e = Animated::new(Effect::IDENTITY);
455 let (to, t) = self.adapt(n.exit.0, n.exit.1);
456 e.set_target(to, t, now);
457 (n.range.clone(), n.pivot, e)
458 });
459 let nested = nested.collect();
460 self.ghosts.push(Ghost {
461 key: Some(key),
462 primitives: snapshot.primitives,
463 clip: snapshot.clip,
464 pivot: snapshot.pivot,
465 layer: snapshot.layer,
466 effect,
467 nested,
468 surface_exit: track.surface_exit,
469 });
470 }
471 }
472
473 pub fn sync_frame(&mut self, key: MotionKey, owner: WidgetId, frame: Rect, t: Transition, snap: bool) {
479 let t = if snap || !self.enabled || self.reduced { Transition::Instant } else { t };
480 let now = self.clock;
481 if let Some(track) = self.tracks.get_mut(&key) {
482 let shared = matches!(key, MotionKey::Shared(..));
483 let t = if shared && track.owner == Some(owner) { Transition::Instant } else { t };
484 track.owner = Some(owner);
485 match &mut track.rect {
486 Some(rect) => rect.set_target(frame, t, now),
487 None => track.rect = Some(Animated::new(frame)),
488 }
489 }
490 }
491
492 pub fn sample(&mut self, key: MotionKey, paint: &NodePaint, shift: Point) -> Sampled {
494 let now = self.clock;
495 let Some(track) = self.tracks.get_mut(&key) else {
496 return Sampled { quad: None, text: None, frame: None, effect: Effect::IDENTITY };
497 };
498 let mut moving = track.effect.step(now);
499 let quad = match (&mut track.paint, paint) {
500 (Some(p), NodePaint::Quad(q)) => {
501 moving |= p.step(now);
502 Some(p.sample(q, now))
503 }
504 _ => None,
505 };
506 let text = track.text.as_mut().map(|a| {
507 moving |= a.step(now);
508 a.value(now)
509 });
510 let frame = track.rect.as_mut().map(|a| {
511 moving |= a.step(now);
512 a.value(now).translate(shift)
513 });
514 self.active |= moving;
515 Sampled { quad, text, frame, effect: track.effect.value(now) }
516 }
517
518 pub fn begin_capture(&mut self) -> bool {
520 self.capture_depth += 1;
521 self.capture_depth == 1
522 }
523
524 #[allow(clippy::too_many_arguments)]
526 pub fn end_capture(
527 &mut self,
528 key: MotionKey,
529 primitives: &[Primitive],
530 range: Range<usize>,
531 clip: Rect,
532 pivot: Point,
533 flip: bool,
534 layer: GhostLayer,
535 ) {
536 self.capture_depth = self.capture_depth.saturating_sub(1);
537 let Some(track) = self.tracks.get_mut(&key) else { return };
538 let Some(exit) = track.exit else { return };
539 if self.capture_depth > 0 {
540 self.nested.push(NestedLayer { range, pivot, exit });
541 return;
542 }
543 let s = &mut track.snapshot;
544 s.primitives.clear();
545 s.primitives.extend_from_slice(primitives);
546 s.nested.clear();
547 s.nested.extend(
548 self.nested
549 .drain(..)
550 .map(|n| NestedLayer { range: n.range.start - range.start..n.range.end - range.start, ..n }),
551 );
552 s.clip = clip;
553 s.pivot = pivot;
554 s.flip = flip;
555 s.layer = layer;
556 self.captures += 1;
557 s.order = self.captures;
558 s.valid = true;
559 }
560
561 pub fn crossfade(&mut self, primitives: Vec<Primitive>, viewport: Rect, t: Transition) {
563 let mut effect = Animated::new(Effect::IDENTITY);
564 effect.set_target(Effect::hidden(), t, self.clock);
565 self.ghosts.push(Ghost {
566 key: None,
567 primitives,
568 clip: viewport,
569 pivot: viewport.center(),
570 layer: GhostLayer::Top,
571 effect,
572 nested: Vec::new(),
573 surface_exit: false,
574 });
575 }
576
577 pub fn paint_ghosts(&mut self, layer: GhostLayer, scene: &mut ravenrs_core::Scene) {
579 let now = self.clock;
580 let mut active = false;
581 self.ghosts.retain_mut(|g| {
582 if g.layer != layer {
583 return true;
584 }
585 let mut moving = g.effect.step(now);
586 for (_, _, e) in &mut g.nested {
587 moving |= e.step(now);
588 }
589 if !moving {
590 return false;
591 }
592 active = true;
593 scene.push_clip(g.clip);
594 let start = scene.len();
595 scene.extend_from_slice(&g.primitives);
596 for (range, pivot, e) in &g.nested {
597 let r = start + range.start..start + range.end;
598 scene.apply_effect(r, e.value(now), *pivot);
599 }
600 let effect = g.effect.value(now);
601 if g.surface_exit {
602 let panel = Effect { opacity: (effect.opacity * 2.0).min(1.0), ..effect };
603 let content = Effect { opacity: (effect.opacity * 2.0 - 1.0).max(0.0), ..effect };
604 let first = (start + 1).min(scene.len());
605 scene.apply_effect(start..first, panel, g.pivot);
606 scene.apply_effect(first..scene.len(), content, g.pivot);
607 } else {
608 scene.apply_effect(start..scene.len(), effect, g.pivot);
609 }
610 scene.pop_clip();
611 true
612 });
613 self.active |= active;
614 }
615}
616
617pub(crate) fn flipped(effect: Effect, flip: bool) -> Effect {
619 if flip { Effect { offset: Point::new(effect.offset.x, -effect.offset.y), ..effect } } else { effect }
620}
621
622#[derive(Clone, Copy, Debug)]
624pub(crate) enum NodePaint {
625 Quad(QuadPaint),
626 Text(Color),
627 Other,
628}
629
630#[cfg_attr(feature = "live", ravenrs_live::expose)]
631impl NodePaint {
632 pub(crate) fn of(paint: &Paint, text: Option<Color>) -> Self {
633 match (paint, text) {
634 (Paint::Quad(q), _) => Self::Quad(*q),
635 (_, Some(c)) => Self::Text(c),
636 _ => Self::Other,
637 }
638 }
639}
640
641#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
644pub struct Collapse {
645 open: bool,
646 content: AnyView,
647}
648
649impl std::fmt::Debug for Collapse {
650 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
651 f.debug_struct("Collapse").field("open", &self.open).finish_non_exhaustive()
652 }
653}
654
655#[cfg_attr(feature = "live", ravenrs_live::expose)]
656impl Collapse {
657 pub fn new(open: bool, content: impl View + 'static) -> Self {
659 Self { open, content: Box::new(content) }
660 }
661
662 pub fn boxed(open: bool, content: AnyView) -> Self {
664 Self { open, content }
665 }
666}
667
668impl View for Collapse {
669 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
670 collapse(cx, self.open, self.content.as_ref())
671 }
672}
673
674pub(crate) fn collapse(cx: &mut BuildCx<'_>, open: bool, content: &dyn View) -> NodeId {
675 struct Body<'a>(bool, &'a dyn View);
676 impl View for Body<'_> {
677 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
678 let transition = cx.theme().motion.base;
679 let target = if self.0 { 1.0_f32 } else { 0.0 };
680 let progress = cx.animated(target, transition).clamp(0.0, 1.0);
681 let at_rest = (progress - target).abs() < f32::EPSILON;
682 if at_rest && !self.0 {
683 return cx.empty();
684 }
685 let child = cx.child(self.1);
686 let natural = cx.measured_content().map_or(0.0, |s| s.height);
687 let style = LayoutStyle {
688 height: (!at_rest).then_some(natural * progress),
689 overflow_hidden: !at_rest,
690 align: ravenrs_layout::Align::Stretch,
691 ..LayoutStyle::stack(Axis::Vertical)
692 };
693 let node = cx.container(style, &[child], Paint::None);
694 cx.measure_content(node);
695 if !at_rest {
696 cx.clip(node);
697 }
698 node
699 }
700 }
701 cx.child(&Body(open, content))
702}
703
704pub(crate) const SURFACE_ENTER_SCALE: f32 = 0.96;
705pub(crate) const SURFACE_EXIT_SCALE: f32 = 0.98;
706
707pub(crate) fn popup(cx: &mut BuildCx<'_>, node: NodeId) {
713 let m = cx.theme().motion;
714 cx.motion(
715 node,
716 Motion::new()
717 .anchored()
718 .surface_exit()
719 .enter(Effect::scaled(SURFACE_ENTER_SCALE), m.fast)
720 .exit(Effect { opacity: 0.0, ..Effect::scaled(SURFACE_EXIT_SCALE) }, m.fast),
721 );
722}
723
724pub(crate) fn scrim(cx: &mut BuildCx<'_>, viewport: ravenrs_core::Size) -> NodeId {
727 let theme = *cx.theme();
728 let node = cx.container(
729 LayoutStyle {
730 absolute: Some((0.0, 0.0)),
731 width: Some(viewport.width),
732 height: Some(viewport.height),
733 ..LayoutStyle::leaf()
734 },
735 &[],
736 Paint::Quad(QuadPaint::new(theme.color(ravenrs_style::Semantic::Scrim))),
737 );
738 let m = theme.motion;
739 cx.motion(node, Motion::new().enter(Effect::hidden(), m.base).exit(Effect::hidden(), m.base));
740 node
741}
742
743pub(crate) fn control(cx: &mut BuildCx<'_>, node: NodeId) {
745 let t = cx.theme().motion.fast;
746 cx.motion(node, Motion::new().paint(t));
747}