Skip to main content

cranpose_core/
movable_content.rs

1use std::{hash::Hash, rc::Rc};
2
3use crate::{
4    Composer, Key, composer_context, current_runtime_handle, hash_key, remember,
5    with_current_composer,
6};
7
8/// Composes `content` under an identity that is `key` alone, the same at
9/// every call site, so the subtree can be emitted from any parent and keep
10/// its remembered values, its running effects and its nodes.
11///
12/// Move content by emitting it under a different parent, in the same pass
13/// or a later one: when the old parent stops emitting it, the subtree is
14/// retained under `key`; when a parent emits it, the retained subtree is
15/// taken back. A site that asks for content still attached elsewhere
16/// composes nothing and is recomposed once the content is retained, so the
17/// order in which the two parents recompose does not matter.
18///
19/// Content is shown at most once. Two live sites with the same key leave
20/// the later one empty. Retained content is kept
21/// until a parent takes it back or [`forget_movable`] releases it, so an
22/// item the app closes for good must be forgotten or its state stays in
23/// memory. A subtree may cross into and out of a `SubcomposeLayout` slot,
24/// whose content lives in a slot table of its own: the table that held it
25/// gives up its anchors and the one taking it over issues new ones.
26#[track_caller]
27pub fn movable<K: Hash>(key: K, content: impl FnOnce()) {
28    let id = hash_key(&key);
29    with_current_composer(|composer| composer.with_movable_group(id, |_| content()));
30}
31
32/// Content written once that keeps its identity wherever it is shown.
33///
34/// [`movable`] names content by a key, which means writing the content out
35/// at every parent that might show it and trusting the keys to match. When
36/// the parents are alternatives -- a pane docked in a stack or pulled out
37/// into a window of its own, a tab in a strip or torn into its own window --
38/// that is the same body written twice, and two bodies that must be kept in
39/// step by hand are two bodies that drift.
40///
41/// This hands back the content as a value instead. Write the body once,
42/// remember it, and call [`MovableContent::show`] from whichever parent is
43/// showing it this pass:
44///
45/// ```ignore
46/// let pane = rememberMovableContentOf(move || PaneBody(state));
47/// if docked {
48///     Box(Modifier::empty().offset(0.0, y), BoxSpec::default(), || pane.show());
49/// } else {
50///     Box(Modifier::empty().window(config), BoxSpec::default(), || pane.show());
51/// }
52/// ```
53///
54/// The identity comes from the `remember`, so two of these at one call site
55/// -- one per row of a list -- are two pieces of content, and the same one
56/// survives recomposition. Everything [`movable`] says about ordering,
57/// showing content at most once, and releasing it still holds; release this
58/// one with [`MovableContent::forget`].
59///
60/// Content that has to be recognised from somewhere this value cannot reach
61/// -- another composable that shows the same pane in another arrangement --
62/// wants [`movableContentOf`] instead, which takes the identity as a key.
63///
64/// Mirrors Jetpack Compose's `movableContentOf`.
65#[expect(non_snake_case, reason = "names follow Compose's casing")]
66#[track_caller]
67pub fn rememberMovableContentOf(content: impl Fn() + 'static) -> MovableContent {
68    let runtime = with_current_composer(Composer::runtime_handle);
69    let id = remember(|| runtime.next_movable_content_id()).with(|id| *id);
70    MovableContent {
71        id,
72        content: Rc::new(content),
73    }
74}
75
76/// Movable content whose identity is `key`, the same at every call site.
77///
78/// The keyed twin of [`rememberMovableContentOf`], and the same identity
79/// [`movable`] uses: content shown by this value and content shown by
80/// `movable` under the same key is one piece of content. Reach for this when
81/// the parents that can show it are in different composables, so no single
82/// value can be handed to all of them.
83#[expect(non_snake_case, reason = "names follow Compose's casing")]
84pub fn movableContentOf<K: Hash>(key: K, content: impl Fn() + 'static) -> MovableContent {
85    MovableContent {
86        id: hash_key(&key),
87        content: Rc::new(content),
88    }
89}
90
91/// Movable content held as a value. See [`rememberMovableContentOf`].
92#[derive(Clone)]
93pub struct MovableContent {
94    id: Key,
95    content: Rc<dyn Fn()>,
96}
97
98impl MovableContent {
99    /// Shows the content here. Calling this from a different parent than
100    /// last pass moves the content, with its remembered values, its running
101    /// effects and its nodes.
102    pub fn show(&self) {
103        let content = Rc::clone(&self.content);
104        let id = self.id;
105        with_current_composer(|composer| composer.with_movable_group(id, |_| content()));
106    }
107
108    /// Releases the content's state once no parent is showing it, for
109    /// content the application has closed for good. See [`forget_movable`].
110    pub fn forget(&self) {
111        forget_movable_id(self.id);
112    }
113}
114
115impl std::fmt::Debug for MovableContent {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        f.debug_struct("MovableContent")
118            .field("id", &self.id)
119            .finish()
120    }
121}
122
123impl PartialEq for MovableContent {
124    fn eq(&self, other: &Self) -> bool {
125        self.id == other.id && Rc::ptr_eq(&self.content, &other.content)
126    }
127}
128
129/// Releases the state of [`movable`] content with identity `key` that no
130/// parent is showing. Call it when the item is closed for good, from an
131/// event handler or inside composition. Content a parent is still showing
132/// is unaffected. Needs a runtime: an active composition or one created on
133/// this thread.
134pub fn forget_movable<K: Hash>(key: K) {
135    let id = hash_key(&key);
136    forget_movable_id(id);
137}
138
139fn forget_movable_id(id: Key) {
140    let runtime = composer_context::try_with_composer(Composer::runtime_handle)
141        .or_else(current_runtime_handle);
142    match runtime {
143        Some(runtime) => runtime.forget_movable(id),
144        None => log::error!("forget_movable called without an active runtime"),
145    }
146}