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}