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::{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 effects: Cell<TransitionEffects>,
101 animation: Cell<AnimationType>,
102 pub(crate) leaving: Cell<bool>,
104 coordinates: Rc<Cell<WindowCoordinates>>,
107}
108
109impl EntryMotion {
110 pub(crate) fn progress(&self) -> f32 {
111 self.progress.borrow().state().value()
112 }
113
114 fn layer(&self) -> GraphicsLayer {
115 self.effects.get().layer(self.progress(), self.size.get())
116 }
117
118 fn animate_to(&self, target: f32) {
121 let mut progress = self.progress.borrow_mut();
122 let settled = if progress.is_running() {
123 progress.target()
124 } else {
125 progress.state().get_non_reactive()
126 };
127 if (settled - target).abs() > f32::EPSILON {
128 progress.animateTo(target, self.animation.get());
129 }
130 }
131
132 fn snap_to(&self, target: f32) {
133 self.progress.borrow_mut().snapTo(target);
134 }
135
136 fn at_rest(&self) -> bool {
139 let progress = self.progress.borrow();
140 !progress.is_running() && progress.state().get_non_reactive() >= 1.0 - GONE
141 }
142
143 fn gone(&self) -> bool {
144 let progress = self.progress.borrow();
145 !progress.is_running() && progress.state().get_non_reactive() <= GONE
146 }
147}
148
149struct Entry<T> {
150 id: u64,
151 value: T,
152 motion: Rc<EntryMotion>,
153}
154
155type ContentFn<T> = std::boxed::Box<dyn FnMut(T)>;
156
157struct ContentState<T> {
158 runtime: RuntimeHandle,
159 entries: RefCell<Vec<Entry<T>>>,
161 next_id: Cell<u64>,
162 last_target: RefCell<Option<T>>,
163 content: RefCell<Option<ContentFn<T>>>,
164 size_transform: Cell<Option<SizeTransform>>,
165 size: RefCell<Option<Animatable<Size>>>,
166 coordinates: Rc<Cell<WindowCoordinates>>,
168 last_size: Cell<Size>,
171 node: Cell<Option<NodeId>>,
172 removed: MutableState<u64>,
175 watcher: RefCell<Option<FrameCallbackRegistration>>,
176}
177
178impl<T: Clone + PartialEq + 'static> ContentState<T> {
179 fn new(runtime: RuntimeHandle) -> Self {
180 Self {
181 removed: MutableState::with_runtime(0, runtime.clone()),
182 runtime,
183 entries: RefCell::new(Vec::new()),
184 next_id: Cell::new(0),
185 last_target: RefCell::new(None),
186 content: RefCell::new(None),
187 size_transform: Cell::new(None),
188 size: RefCell::new(None),
189 last_size: Cell::new(Size::ZERO),
190 coordinates: Rc::default(),
191 node: Cell::new(None),
192 watcher: RefCell::new(None),
193 }
194 }
195
196 fn push(&self, value: T, progress: f32, effects: TransitionEffects, animation: AnimationType) {
197 let id = self.next_id.get();
198 self.next_id.set(id.wrapping_add(1));
199 self.entries.borrow_mut().push(Entry {
200 id,
201 value,
202 motion: Rc::new(EntryMotion {
203 progress: RefCell::new(Animatable::new(progress, self.runtime.clone())),
204 size: Rc::new(Cell::new(Size::ZERO)),
205 effects: Cell::new(effects),
206 animation: Cell::new(animation),
207 leaving: Cell::new(false),
208 coordinates: Rc::default(),
209 }),
210 });
211 }
212
213 fn sync(
217 &self,
218 target: &T,
219 seek: Option<&ContentSeek<T>>,
220 transition: &dyn Fn(&T, &T) -> ContentTransform,
221 ) {
222 let previous = self.last_target.replace(Some(target.clone()));
223 let change = previous
224 .filter(|previous| previous != target)
225 .map(|previous| transition(&previous, target));
226 if let Some(change) = change {
227 self.size_transform.set(change.size_transform);
228 }
229 let first = self.entries.borrow().is_empty();
230 if !self
231 .entries
232 .borrow()
233 .iter()
234 .any(|entry| entry.value == *target)
235 {
236 let enter = change.map_or_else(EnterTransition::none, |change| change.enter);
237 let progress = if first { 1.0 } else { 0.0 };
238 self.push(target.clone(), progress, enter.effects(), enter.animation());
239 }
240 let seek = seek.filter(|seek| seek.toward != *target);
241 let seeking = seek.map(|seek| (seek, transition(target, &seek.toward)));
242 if let Some((seek, _)) = seeking
243 && !self
244 .entries
245 .borrow()
246 .iter()
247 .any(|entry| entry.value == seek.toward)
248 {
249 self.push(
250 seek.toward.clone(),
251 0.0,
252 TransitionEffects::default(),
253 AnimationType::default(),
254 );
255 }
256 let mut entries = self.entries.borrow_mut();
257 for entry in entries.iter_mut() {
258 let motion = &entry.motion;
259 if entry.value == *target {
260 entry.motion.leaving.set(seeking.is_some());
265 match seeking {
266 Some((seek, seek_change)) => {
267 motion.effects.set(seek_change.exit.effects());
268 motion.snap_to(1.0 - seek.fraction.clamp(0.0, 1.0));
269 }
270 None => motion.animate_to(1.0),
271 }
272 } else if let Some((seek, seek_change)) =
273 seeking.filter(|(seek, _)| seek.toward == entry.value)
274 {
275 entry.motion.leaving.set(false);
276 motion.effects.set(seek_change.enter.effects());
277 motion.animation.set(seek_change.enter.animation());
278 motion.snap_to(seek.fraction.clamp(0.0, 1.0));
279 } else {
280 if !entry.motion.leaving.replace(true)
281 && let Some(change) = change.filter(|_| motion.at_rest())
282 {
283 motion.effects.set(change.exit.effects());
284 motion.animation.set(change.exit.animation());
285 }
286 motion.animate_to(0.0);
287 }
288 }
289 if let Some(index) = entries.iter().position(|entry| entry.value == *target) {
291 let current = entries.remove(index);
292 entries.push(current);
293 }
294 }
295
296 fn animating(&self) -> bool {
297 self.entries
298 .borrow()
299 .iter()
300 .any(|entry| entry.motion.leaving.get())
301 || self
302 .size
303 .borrow()
304 .as_ref()
305 .is_some_and(Animatable::is_running)
306 }
307}
308
309fn watch<T: Clone + PartialEq + 'static>(state: &Rc<ContentState<T>>) {
312 if state.watcher.borrow().is_some() || !state.animating() {
313 return;
314 }
315 let weak = Rc::downgrade(state);
316 let registration = state.runtime.frame_clock().with_frame_nanos(move |_| {
317 let Some(state) = weak.upgrade() else {
318 return;
319 };
320 state.watcher.borrow_mut().take();
321 let removed = {
322 let mut entries = state.entries.borrow_mut();
323 let before = entries.len();
324 let current = entries.last().map(|entry| entry.id);
327 entries.retain(|entry| {
328 Some(entry.id) == current || !(entry.motion.leaving.get() && entry.motion.gone())
329 });
330 before != entries.len()
331 };
332 if removed {
333 state.removed.update(|count| *count = count.wrapping_add(1));
334 }
335 if state
336 .size
337 .borrow()
338 .as_ref()
339 .is_some_and(Animatable::is_running)
340 && let Some(node) = state.node.get()
341 {
342 crate::schedule_measure_repass(node);
343 crate::request_render_invalidation();
344 }
345 watch(&state);
346 });
347 *state.watcher.borrow_mut() = Some(registration);
348}
349
350struct StateHandle<T>(Rc<ContentState<T>>);
351
352impl<T> Clone for StateHandle<T> {
353 fn clone(&self) -> Self {
354 Self(Rc::clone(&self.0))
355 }
356}
357
358impl<T> PartialEq for StateHandle<T> {
359 fn eq(&self, other: &Self) -> bool {
360 Rc::ptr_eq(&self.0, &other.0)
361 }
362}
363
364#[composable(no_skip)]
376pub fn AnimatedContent<T, F>(
377 target_state: T,
378 transition_spec: impl Fn(&T, &T) -> ContentTransform + 'static,
379 content: F,
380) where
381 T: Clone + PartialEq + 'static,
382 F: FnMut(T) + 'static,
383{
384 SeekableAnimatedContent(target_state, None, transition_spec, content);
385}
386
387#[composable(no_skip)]
393pub fn SeekableAnimatedContent<T, F>(
394 target_state: T,
395 seek: Option<ContentSeek<T>>,
396 transition_spec: impl Fn(&T, &T) -> ContentTransform + 'static,
397 content: F,
398) where
399 T: Clone + PartialEq + 'static,
400 F: FnMut(T) + 'static,
401{
402 let runtime = with_current_composer(|composer| composer.runtime_handle());
403 let state =
404 remember(|| StateHandle(Rc::new(ContentState::<T>::new(runtime)))).with(StateHandle::clone);
405 *state.0.content.borrow_mut() = Some(std::boxed::Box::new(content));
406 state.0.sync(&target_state, seek.as_ref(), &transition_spec);
407 watch(&state.0);
408 state.0.removed.get();
410 let clip = state
411 .0
412 .size_transform
413 .get()
414 .is_some_and(|transform| transform.clip);
415 let modifier = if clip {
416 Modifier::empty().clip_to_bounds()
417 } else {
418 Modifier::empty()
419 };
420 let shared = in_shared_transition();
423 let modifier = if shared {
424 modifier.report_window_coordinates(Rc::clone(&state.0.coordinates))
425 } else {
426 modifier
427 };
428 let policy = ContentMeasurePolicy(state.clone());
429 let shown = state.clone();
430 let node = Layout(modifier, policy, move || {
431 let state = &shown;
432 let entries: Vec<(u64, T, Rc<EntryMotion>)> = state
433 .0
434 .entries
435 .borrow()
436 .iter()
437 .map(|entry| (entry.id, entry.value.clone(), Rc::clone(&entry.motion)))
438 .collect();
439 for (id, value, motion) in entries {
440 let state = state.clone();
441 with_key(&id, move || {
442 let layer = Rc::clone(&motion);
443 let entry = Modifier::empty().report_size(Rc::clone(&motion.size));
444 let entry = if shared {
445 entry.report_window_coordinates(Rc::clone(&motion.coordinates))
446 } else {
447 entry
448 };
449 let shared_entry = shared.then(|| SharedEntry {
450 motion: Rc::clone(&motion),
451 entry: Rc::clone(&motion.coordinates),
452 container: Rc::clone(&state.0.coordinates),
453 });
454 Box(
455 entry.graphics_layer(move || layer.layer()),
456 BoxSpec::new(),
457 move || {
458 let compose = || {
459 if let Some(content) = state.0.content.borrow_mut().as_mut() {
460 content(value.clone());
461 }
462 };
463 match shared_entry.clone() {
464 Some(entry) => ProvideSharedEntry(entry, compose),
465 None => compose(),
466 }
467 },
468 );
469 });
470 }
471 });
472 state.0.node.set(Some(node));
473}
474
475struct ContentMeasurePolicy<T>(StateHandle<T>);
479
480impl<T> Clone for ContentMeasurePolicy<T> {
481 fn clone(&self) -> Self {
482 Self(self.0.clone())
483 }
484}
485
486impl<T> PartialEq for ContentMeasurePolicy<T> {
487 fn eq(&self, other: &Self) -> bool {
488 self.0 == other.0
489 }
490}
491
492impl<T: Clone + PartialEq + 'static> MeasurePolicy for ContentMeasurePolicy<T> {
493 fn measure(
494 &self,
495 _scope: &dyn MeasureScope,
496 measurables: &[Box<dyn Measurable>],
497 constraints: Constraints,
498 ) -> MeasureResult {
499 let state = &self.0.0;
500 let loose = Constraints {
501 min_width: 0.0,
502 min_height: 0.0,
503 ..constraints
504 };
505 let mut largest = Size::ZERO;
506 let mut current = Size::ZERO;
507 let mut placements = Vec::with_capacity(measurables.len());
508 for measurable in measurables {
509 let placeable = measurable.measure(loose);
510 let size = Size::new(placeable.width(), placeable.height());
511 largest = Size::new(
512 largest.width.max(size.width),
513 largest.height.max(size.height),
514 );
515 current = size;
517 placeable.place(0.0, 0.0);
518 placements.push(Placement::new(placeable.node_id(), 0.0, 0.0, 0));
519 }
520 let size = match state.size_transform.get() {
521 Some(transform) => {
522 let mut animated = state.size.borrow_mut();
523 let animated = animated.get_or_insert_with(|| {
524 Animatable::new(state.last_size.get(), state.runtime.clone())
525 });
526 if animated.target() != current {
527 animated.animateTo(current, transform.animation);
528 }
529 animated.state().value()
530 }
531 None => {
532 state.size.borrow_mut().take();
533 largest
534 }
535 };
536 let size = Size::new(
537 size.width
538 .clamp(constraints.min_width, constraints.max_width),
539 size.height
540 .clamp(constraints.min_height, constraints.max_height),
541 );
542 state.last_size.set(size);
543 MeasureResult::new(size, placements)
544 }
545
546 fn min_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
547 measurables
548 .iter()
549 .map(|measurable| measurable.min_intrinsic_width(height))
550 .fold(0.0, f32::max)
551 }
552
553 fn max_intrinsic_width(&self, measurables: &[Box<dyn Measurable>], height: f32) -> f32 {
554 measurables
555 .iter()
556 .map(|measurable| measurable.max_intrinsic_width(height))
557 .fold(0.0, f32::max)
558 }
559
560 fn min_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
561 measurables
562 .iter()
563 .map(|measurable| measurable.min_intrinsic_height(width))
564 .fold(0.0, f32::max)
565 }
566
567 fn max_intrinsic_height(&self, measurables: &[Box<dyn Measurable>], width: f32) -> f32 {
568 measurables
569 .iter()
570 .map(|measurable| measurable.max_intrinsic_height(width))
571 .fold(0.0, f32::max)
572 }
573}