1use std::{
11 cell::{Cell, RefCell},
12 rc::Rc,
13};
14
15use cranpose_animation::{Animatable, AnimationType};
16use cranpose_core::{
17 MutableState, NodeId, RuntimeHandle, internal::FrameCallbackRegistration, remember,
18 with_current_composer, with_key,
19};
20use cranpose_ui_graphics::{Size, WindowCoordinates};
21use cranpose_ui_layout::{
22 Constraints, Measurable, MeasurePolicy, MeasureResult, MeasureScope, Placement,
23};
24
25use crate::{
26 composable,
27 modifier::{GraphicsLayer, Modifier},
28 widgets::{
29 animated_visibility::{ContainerFrame, EnterTransition, ExitTransition, TransitionEffects},
30 box_widget::{Box, BoxSpec},
31 layout::Layout,
32 shared_transition::{ProvideSharedEntry, SharedEntry, in_shared_transition},
33 },
34};
35
36const GONE: f32 = 0.001;
38
39#[derive(Clone, Copy, Debug, PartialEq)]
45pub struct ContentTransform {
46 pub enter: EnterTransition,
48 pub exit: ExitTransition,
50 pub size_transform: Option<SizeTransform>,
53}
54
55impl ContentTransform {
56 pub fn using(mut self, size_transform: SizeTransform) -> Self {
59 self.size_transform = Some(size_transform);
60 self
61 }
62}
63
64#[derive(Clone, Copy, Debug, PartialEq)]
67pub struct SizeTransform {
68 pub clip: bool,
70 pub animation: AnimationType,
72}
73
74impl Default for SizeTransform {
75 fn default() -> Self {
76 Self {
77 clip: true,
78 animation: AnimationType::default(),
79 }
80 }
81}
82
83#[derive(Clone, Debug, PartialEq)]
88pub struct ContentSeek<T> {
89 pub toward: T,
91 pub fraction: f32,
94}
95
96pub(crate) struct EntryMotion {
98 progress: RefCell<Animatable<f32>>,
99 size: Rc<Cell<Size>>,
100 container: Rc<Cell<ContainerFrame>>,
102 effects: Cell<TransitionEffects>,
103 animation: Cell<AnimationType>,
104 pub(crate) leaving: Cell<bool>,
106 coordinates: Rc<Cell<WindowCoordinates>>,
109}
110
111impl EntryMotion {
112 pub(crate) fn progress(&self) -> f32 {
113 self.progress.borrow().state().value()
114 }
115
116 fn layer(&self) -> GraphicsLayer {
117 self.effects
118 .get()
119 .layer(self.progress(), self.size.get(), self.container.get())
120 }
121
122 fn animate_to(&self, target: f32) {
125 let mut progress = self.progress.borrow_mut();
126 let settled = if progress.is_running() {
127 progress.target()
128 } else {
129 progress.state().get_non_reactive()
130 };
131 if (settled - target).abs() > f32::EPSILON {
132 progress.animateTo(target, self.animation.get());
133 }
134 }
135
136 fn snap_to(&self, target: f32) {
137 self.progress.borrow_mut().snapTo(target);
138 }
139
140 fn at_rest(&self) -> bool {
143 let progress = self.progress.borrow();
144 !progress.is_running() && progress.state().get_non_reactive() >= 1.0 - GONE
145 }
146
147 fn gone(&self) -> bool {
148 let progress = self.progress.borrow();
149 !progress.is_running() && progress.state().get_non_reactive() <= GONE
150 }
151}
152
153struct Entry<T> {
154 id: u64,
155 value: T,
156 motion: Rc<EntryMotion>,
157}
158
159type ContentFn<T> = std::boxed::Box<dyn FnMut(T)>;
160
161struct ContentState<T> {
162 runtime: RuntimeHandle,
163 entries: RefCell<Vec<Entry<T>>>,
165 next_id: Cell<u64>,
166 last_target: RefCell<Option<T>>,
167 content: RefCell<Option<ContentFn<T>>>,
168 size_transform: Cell<Option<SizeTransform>>,
169 size: RefCell<Option<Animatable<Size>>>,
170 coordinates: Rc<Cell<WindowCoordinates>>,
172 frame: Rc<Cell<ContainerFrame>>,
175 node: Cell<Option<NodeId>>,
176 removed: MutableState<u64>,
179 watcher: RefCell<Option<FrameCallbackRegistration>>,
180}
181
182impl<T: Clone + PartialEq + 'static> ContentState<T> {
183 fn new(runtime: RuntimeHandle) -> Self {
184 Self {
185 removed: MutableState::with_runtime(0, runtime.clone()),
186 runtime,
187 entries: RefCell::new(Vec::new()),
188 next_id: Cell::new(0),
189 last_target: RefCell::new(None),
190 content: RefCell::new(None),
191 size_transform: Cell::new(None),
192 size: RefCell::new(None),
193 frame: Rc::default(),
194 coordinates: Rc::default(),
195 node: Cell::new(None),
196 watcher: RefCell::new(None),
197 }
198 }
199
200 fn push(&self, value: T, progress: f32, effects: TransitionEffects, animation: AnimationType) {
201 let id = self.next_id.get();
202 self.next_id.set(id.wrapping_add(1));
203 self.entries.borrow_mut().push(Entry {
204 id,
205 value,
206 motion: Rc::new(EntryMotion {
207 progress: RefCell::new(Animatable::new(progress, self.runtime.clone())),
208 size: Rc::new(Cell::new(Size::ZERO)),
209 container: Rc::clone(&self.frame),
210 effects: Cell::new(effects),
211 animation: Cell::new(animation),
212 leaving: Cell::new(false),
213 coordinates: Rc::default(),
214 }),
215 });
216 }
217
218 fn show(&self, value: &T, same: &dyn Fn(&T, &T) -> bool) -> bool {
222 let mut entries = self.entries.borrow_mut();
223 let Some(entry) = entries.iter_mut().find(|entry| same(&entry.value, value)) else {
224 return false;
225 };
226 if entry.value != *value {
227 entry.value = value.clone();
228 }
229 true
230 }
231
232 fn sync(
236 &self,
237 target: &T,
238 seek: Option<&ContentSeek<T>>,
239 transition: &dyn Fn(&T, &T) -> ContentTransform,
240 same: &dyn Fn(&T, &T) -> bool,
241 ) {
242 let previous = self.last_target.replace(Some(target.clone()));
243 let change = previous
244 .filter(|previous| !same(previous, target))
245 .map(|previous| transition(&previous, target));
246 if let Some(change) = change {
247 self.size_transform.set(change.size_transform);
248 }
249 let first = self.entries.borrow().is_empty();
250 if !self.show(target, same) {
251 let enter = change.map_or_else(EnterTransition::none, |change| change.enter);
252 let progress = if first { 1.0 } else { 0.0 };
253 self.push(target.clone(), progress, enter.effects(), enter.animation());
254 }
255 let seek = seek.filter(|seek| !same(&seek.toward, target));
256 let seeking = seek.map(|seek| (seek, transition(target, &seek.toward)));
257 if let Some((seek, _)) = seeking
258 && !self.show(&seek.toward, same)
259 {
260 self.push(
261 seek.toward.clone(),
262 0.0,
263 TransitionEffects::default(),
264 AnimationType::default(),
265 );
266 }
267 let mut entries = self.entries.borrow_mut();
268 for entry in entries.iter_mut() {
269 let motion = &entry.motion;
270 if same(&entry.value, target) {
271 entry.motion.leaving.set(seeking.is_some());
276 match seeking {
277 Some((seek, seek_change)) => {
278 motion.effects.set(seek_change.exit.effects());
279 motion.snap_to(1.0 - seek.fraction.clamp(0.0, 1.0));
280 }
281 None => motion.animate_to(1.0),
282 }
283 } else if let Some((seek, seek_change)) =
284 seeking.filter(|(seek, _)| same(&seek.toward, &entry.value))
285 {
286 entry.motion.leaving.set(false);
287 motion.effects.set(seek_change.enter.effects());
288 motion.animation.set(seek_change.enter.animation());
289 motion.snap_to(seek.fraction.clamp(0.0, 1.0));
290 } else {
291 if !entry.motion.leaving.replace(true)
292 && let Some(change) = change.filter(|_| motion.at_rest())
293 {
294 motion.effects.set(change.exit.effects());
295 motion.animation.set(change.exit.animation());
296 }
297 motion.animate_to(0.0);
298 }
299 }
300 if let Some(index) = entries.iter().position(|entry| same(&entry.value, target)) {
302 let current = entries.remove(index);
303 entries.push(current);
304 }
305 }
306
307 fn note_direction(&self) {
310 let uses_direction = self
311 .entries
312 .borrow()
313 .iter()
314 .any(|entry| entry.motion.effects.get().uses_direction());
315 if uses_direction {
316 let frame = self.frame.get();
317 self.frame.set(ContainerFrame {
318 rtl: crate::layout_direction::layout_direction().is_rtl(),
319 ..frame
320 });
321 }
322 }
323
324 fn animating(&self) -> bool {
325 self.entries
326 .borrow()
327 .iter()
328 .any(|entry| entry.motion.leaving.get())
329 || self
330 .size
331 .borrow()
332 .as_ref()
333 .is_some_and(Animatable::is_running)
334 }
335}
336
337fn watch<T: Clone + PartialEq + 'static>(state: &Rc<ContentState<T>>) {
340 if state.watcher.borrow().is_some() || !state.animating() {
341 return;
342 }
343 let weak = Rc::downgrade(state);
344 let registration = state.runtime.frame_clock().with_frame_nanos(move |_| {
345 let Some(state) = weak.upgrade() else {
346 return;
347 };
348 state.watcher.borrow_mut().take();
349 let removed = {
350 let mut entries = state.entries.borrow_mut();
351 let before = entries.len();
352 let current = entries.last().map(|entry| entry.id);
355 entries.retain(|entry| {
356 Some(entry.id) == current || !(entry.motion.leaving.get() && entry.motion.gone())
357 });
358 before != entries.len()
359 };
360 if removed {
361 state.removed.update(|count| *count = count.wrapping_add(1));
362 }
363 if state
364 .size
365 .borrow()
366 .as_ref()
367 .is_some_and(Animatable::is_running)
368 && let Some(node) = state.node.get()
369 {
370 crate::schedule_measure_repass(node);
371 crate::request_render_invalidation();
372 }
373 watch(&state);
374 });
375 *state.watcher.borrow_mut() = Some(registration);
376}
377
378struct StateHandle<T>(Rc<ContentState<T>>);
379
380impl<T> Clone for StateHandle<T> {
381 fn clone(&self) -> Self {
382 Self(Rc::clone(&self.0))
383 }
384}
385
386impl<T> PartialEq for StateHandle<T> {
387 fn eq(&self, other: &Self) -> bool {
388 Rc::ptr_eq(&self.0, &other.0)
389 }
390}
391
392#[composable(no_skip)]
404pub fn AnimatedContent<T, F>(
405 target_state: T,
406 transition_spec: impl Fn(&T, &T) -> ContentTransform + 'static,
407 content: F,
408) where
409 T: Clone + PartialEq + 'static,
410 F: FnMut(T) + 'static,
411{
412 SeekableAnimatedContent(target_state, None, transition_spec, content);
413}
414
415#[composable(no_skip)]
419pub fn AnimatedContentWithKey<T, K, F>(
420 target_state: T,
421 content_key: impl Fn(&T) -> K + 'static,
422 transition_spec: impl Fn(&T, &T) -> ContentTransform + 'static,
423 content: F,
424) where
425 T: Clone + PartialEq + 'static,
426 K: PartialEq,
427 F: FnMut(T) + 'static,
428{
429 animated_content(
430 target_state,
431 None,
432 &transition_spec,
433 &|a, b| content_key(a) == content_key(b),
434 content,
435 );
436}
437
438#[composable(no_skip)]
444pub fn SeekableAnimatedContent<T, F>(
445 target_state: T,
446 seek: Option<ContentSeek<T>>,
447 transition_spec: impl Fn(&T, &T) -> ContentTransform + 'static,
448 content: F,
449) where
450 T: Clone + PartialEq + 'static,
451 F: FnMut(T) + 'static,
452{
453 animated_content(
454 target_state,
455 seek,
456 &transition_spec,
457 &|a, b| a == b,
458 content,
459 );
460}
461
462fn animated_content<T, F>(
465 target_state: T,
466 seek: Option<ContentSeek<T>>,
467 transition_spec: &dyn Fn(&T, &T) -> ContentTransform,
468 same: &dyn Fn(&T, &T) -> bool,
469 content: F,
470) where
471 T: Clone + PartialEq + 'static,
472 F: FnMut(T) + 'static,
473{
474 let runtime = with_current_composer(cranpose_core::Composer::runtime_handle);
475 let state =
476 remember(|| StateHandle(Rc::new(ContentState::<T>::new(runtime)))).with(StateHandle::clone);
477 *state.0.content.borrow_mut() = Some(std::boxed::Box::new(content));
478 state
479 .0
480 .sync(&target_state, seek.as_ref(), transition_spec, same);
481 state.0.note_direction();
482 watch(&state.0);
483 state.0.removed.get();
485 let clip = state
486 .0
487 .size_transform
488 .get()
489 .is_some_and(|transform| transform.clip);
490 let modifier = if clip {
491 Modifier::empty().clip_to_bounds()
492 } else {
493 Modifier::empty()
494 };
495 let shared = in_shared_transition();
498 let modifier = if shared {
499 modifier.report_window_coordinates(Rc::clone(&state.0.coordinates))
500 } else {
501 modifier
502 };
503 let policy = ContentMeasurePolicy(state.clone());
504 let shown = state.clone();
505 let node = Layout(modifier, policy, move || {
506 let state = &shown;
507 let entries: Vec<(u64, T, Rc<EntryMotion>)> = state
508 .0
509 .entries
510 .borrow()
511 .iter()
512 .map(|entry| (entry.id, entry.value.clone(), Rc::clone(&entry.motion)))
513 .collect();
514 for (id, value, motion) in entries {
515 let state = state.clone();
516 with_key(&id, move || {
517 let layer = Rc::clone(&motion);
518 let entry = Modifier::empty().report_size(Rc::clone(&motion.size));
519 let entry = if shared {
520 entry.report_window_coordinates(Rc::clone(&motion.coordinates))
521 } else {
522 entry
523 };
524 let shared_entry = shared.then(|| SharedEntry {
525 motion: Rc::clone(&motion),
526 entry: Rc::clone(&motion.coordinates),
527 container: Rc::clone(&state.0.coordinates),
528 });
529 Box(
530 entry.graphics_layer(move || layer.layer()),
531 BoxSpec::new(),
532 move || {
533 let compose = || {
534 if let Some(content) = state.0.content.borrow_mut().as_mut() {
535 content(value.clone());
536 }
537 };
538 match shared_entry.clone() {
539 Some(entry) => ProvideSharedEntry(entry, compose),
540 None => compose(),
541 }
542 },
543 );
544 });
545 }
546 });
547 state.0.node.set(Some(node));
548}
549
550struct ContentMeasurePolicy<T>(StateHandle<T>);
554
555impl<T> Clone for ContentMeasurePolicy<T> {
556 fn clone(&self) -> Self {
557 Self(self.0.clone())
558 }
559}
560
561impl<T> PartialEq for ContentMeasurePolicy<T> {
562 fn eq(&self, other: &Self) -> bool {
563 self.0 == other.0
564 }
565}
566
567impl<T: Clone + PartialEq + 'static> MeasurePolicy for ContentMeasurePolicy<T> {
568 fn measure(
569 &self,
570 _scope: &dyn MeasureScope,
571 measurables: &[Box<dyn Measurable>],
572 constraints: Constraints,
573 ) -> MeasureResult {
574 let state = &self.0.0;
575 let loose = Constraints {
576 min_width: 0.0,
577 min_height: 0.0,
578 ..constraints
579 };
580 let mut largest = Size::ZERO;
581 let mut current = Size::ZERO;
582 let mut placements = Vec::with_capacity(measurables.len());
583 for measurable in measurables {
584 let placeable = measurable.measure(loose);
585 let size = Size::new(placeable.width(), placeable.height());
586 largest = Size::new(
587 largest.width.max(size.width),
588 largest.height.max(size.height),
589 );
590 current = size;
592 placeable.place(0.0, 0.0);
593 placements.push(Placement::new(placeable.node_id(), 0.0, 0.0, 0));
594 }
595 let size = match state.size_transform.get() {
596 Some(transform) => {
597 let mut animated = state.size.borrow_mut();
598 let animated = animated.get_or_insert_with(|| {
599 Animatable::new(state.frame.get().size, state.runtime.clone())
600 });
601 if animated.target() != current {
602 animated.animateTo(current, transform.animation);
603 }
604 animated.state().value()
605 }
606 None => {
607 state.size.borrow_mut().take();
608 largest
609 }
610 };
611 let size = Size::new(
612 size.width
613 .clamp(constraints.min_width, constraints.max_width),
614 size.height
615 .clamp(constraints.min_height, constraints.max_height),
616 );
617 state.frame.set(ContainerFrame {
618 size,
619 ..state.frame.get()
620 });
621 MeasureResult::new(size, placements)
622 }
623
624 fn min_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
625 measurables
626 .iter()
627 .map(|measurable| measurable.min_intrinsic_width(height))
628 .fold(0.0, f32::max)
629 }
630
631 fn max_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
632 measurables
633 .iter()
634 .map(|measurable| measurable.max_intrinsic_width(height))
635 .fold(0.0, f32::max)
636 }
637
638 fn min_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
639 measurables
640 .iter()
641 .map(|measurable| measurable.min_intrinsic_height(width))
642 .fold(0.0, f32::max)
643 }
644
645 fn max_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
646 measurables
647 .iter()
648 .map(|measurable| measurable.max_intrinsic_height(width))
649 .fold(0.0, f32::max)
650 }
651}