Skip to main content

cranpose_core/composer/
composable_group.rs

1use std::{cell::Cell, hash::Hash, rc::Rc};
2
3use super::{GroupScopeEntry, PendingMovable, SlotHostPassGuard};
4use crate::{
5    BodyRun, Composer, Key, RecomposeOptions, RecomposeScope, RetentionMode, SlotsHost,
6    explicit_group_key_seed, slot::GroupStart,
7};
8
9/// A group a composable call opened; [`ComposableGroup::close`] ends it
10/// after the call's body.
11#[doc(hidden)]
12pub struct ComposableGroup<'a> {
13    composer: &'a Composer,
14    host: Rc<SlotsHost>,
15    scope: RecomposeScope,
16    pass: Option<SlotHostPassGuard>,
17    runs_body: bool,
18    completed: bool,
19    body: Cell<Body>,
20}
21
22/// Whether a composable call's parameters changed since its body last ran,
23/// or its scope asks to recompose.
24#[doc(hidden)]
25#[derive(Clone, Copy, PartialEq, Eq)]
26pub enum ParameterChange {
27    /// Nothing the body reads as a parameter changed.
28    Unchanged,
29    /// A parameter changed or the scope asks to recompose.
30    Changed,
31}
32
33impl From<bool> for ParameterChange {
34    fn from(changed: bool) -> Self {
35        if changed {
36            Self::Changed
37        } else {
38            Self::Unchanged
39        }
40    }
41}
42
43// What a composable call does with its group's body.
44#[derive(Clone, Copy)]
45enum Body {
46    Runs,
47    // What the body composed last stays.
48    Skipped,
49    // The first body waits for a later frame, in pausable content.
50    Paused,
51}
52
53impl ComposableGroup<'_> {
54    /// The recompose scope of the group.
55    pub fn scope(&self) -> &RecomposeScope {
56        &self.scope
57    }
58
59    /// Registers `body` as the function that recomposes the group's scope,
60    /// as [`Composer::set_recompose_fn`] does for the current scope: a plain
61    /// function needs no box and no call shim.
62    #[inline]
63    pub fn set_recompose_fn(&self, body: fn(&Composer)) {
64        self.scope.set_recompose_fn(body);
65    }
66
67    /// Skips the group's body: what it composed last stays. The call
68    /// composes nothing more in the group before closing it.
69    pub fn skip(&self) {
70        self.body.set(Body::Skipped);
71    }
72
73    /// Registers `recompose` as the function that recomposes the group's scope,
74    /// as [`Self::set_recompose_fn`] does, and answers whether the call runs
75    /// its body: `change` says whether a parameter changed or the scope asks
76    /// to recompose. A body that does not run keeps what it composed
77    /// last, or, for a group that opened in pausable content past the
78    /// frame's budget, waits for a later frame. When the body does not run,
79    /// the call composes nothing more in the group before closing it.
80    /// Compose's `Composer.shouldExecute`.
81    pub fn should_execute(&self, change: ParameterChange, recompose: fn(&Composer)) -> bool {
82        self.scope.set_recompose_fn(recompose);
83        match self.body.get() {
84            Body::Paused => false,
85            _ if change == ParameterChange::Unchanged && self.scope.has_composed_once() => {
86                self.body.set(Body::Skipped);
87                false
88            }
89            _ => true,
90        }
91    }
92
93    // Closes a group whose first body waits for a later frame: its scope has
94    // not composed once and stays invalid until the body runs.
95    #[cold]
96    #[inline(never)]
97    fn close_paused(&self) {
98        self.scope.end_reads(BodyRun::Skipped);
99        self.composer
100            .close_group_in_active_pass(&self.host, &self.scope);
101        self.composer.hold_paused(&self.scope);
102    }
103
104    /// Ends the group after its body composed.
105    #[inline]
106    pub fn close(mut self) {
107        self.completed = true;
108    }
109}
110
111impl Drop for ComposableGroup<'_> {
112    fn drop(&mut self) {
113        match self.body.get() {
114            Body::Runs => {
115                if self.completed {
116                    self.scope.mark_composed_once();
117                }
118                self.scope
119                    .end_reads(BodyRun::from(self.completed & self.runs_body));
120                self.composer
121                    .close_group_in_active_pass(&self.host, &self.scope);
122            }
123            Body::Skipped => {
124                if self.completed {
125                    self.scope.mark_composed_once();
126                }
127                self.scope.end_reads(BodyRun::Skipped);
128                self.composer
129                    .close_skipped_group_in_active_pass(&self.host, &self.scope);
130            }
131            Body::Paused => self.close_paused(),
132        }
133        if let Some(mut pass) = self.pass.take() {
134            if self.completed
135                && let Err(err) = self.composer.finish_slot_host_pass(&pass.host)
136            {
137                log::error!("slot host pass finalization failed: {err}");
138            }
139            pass.close();
140        }
141    }
142}
143
144#[expect(clippy::multiple_inherent_impl, reason = "one file per concern")]
145impl Composer {
146    fn scope_for_started_group(
147        &self,
148        host: &SlotsHost,
149        group: crate::slot::ActiveGroupId,
150        scope: Option<RecomposeScope>,
151    ) -> RecomposeScope {
152        if let Some(scope) = scope {
153            return scope;
154        }
155        let scope = RecomposeScope::new(self.runtime_handle());
156        host.with_write_session(|slots| slots.set_group_scope(group, scope.clone()));
157        if self.core.shared_state.pausing.is_holding() {
158            self.hold_scope(&scope);
159        }
160        scope
161    }
162
163    #[inline(never)]
164    fn open_group_in_active_pass(
165        &self,
166        host: &Rc<SlotsHost>,
167        key: crate::slot::GroupKeySeed,
168    ) -> (RecomposeScope, bool) {
169        let options = self.core.pending_scope_options.take().unwrap_or_default();
170        let parent_node = self.current_parent_hint();
171        let (
172            GroupStart {
173                group,
174                anchor,
175                scope,
176                kind,
177            },
178            placeholder_for,
179        ) = self.start_group(host, key, parent_node);
180        let scope_ref = self.scope_for_started_group(host, group, scope);
181        if let Some(movable_key) = placeholder_for {
182            self.core.shared_state.record_pending_movable(
183                host,
184                PendingMovable {
185                    key: movable_key,
186                    placeholder: anchor,
187                    parent_scope: self
188                        .core
189                        .scope_stack
190                        .borrow()
191                        .last()
192                        .map(RecomposeScope::downgrade),
193                },
194            );
195        }
196        self.enter_group_scope(
197            &scope_ref,
198            GroupScopeEntry {
199                options,
200                start_kind: kind,
201                host,
202                group,
203                parent_hint: parent_node,
204            },
205        );
206        (scope_ref, placeholder_for.is_none())
207    }
208
209    #[inline(never)]
210    fn close_group_in_active_pass(&self, host: &Rc<SlotsHost>, scope: &RecomposeScope) {
211        let result = host.with_write_session(|slots| {
212            let result = slots.finish_group_body_changes();
213            slots.end_group();
214            result
215        });
216        self.close_finished_group(host, scope, result);
217        self.settle_closed_group(scope);
218    }
219
220    /// Closes a group whose body was skipped, in one slot session: its root
221    /// nodes stay attached where they were.
222    #[inline(never)]
223    fn close_skipped_group_in_active_pass(&self, host: &Rc<SlotsHost>, scope: &RecomposeScope) {
224        // Attaching a node reaches the parent stack and the applier, never
225        // the slot table the session holds.
226        host.with_write_session(|slots| slots.skip_and_end_group(|id| self.attach_to_parent(id)));
227        self.pop_closed_scope(scope);
228        self.settle_closed_group(scope);
229    }
230
231    fn settle_closed_group(&self, scope: &RecomposeScope) {
232        scope.mark_recomposed();
233        if let Err(err) = self.flush_pending_commands_if_large() {
234            log::error!("mid-composition command flush failed: {err}");
235        }
236    }
237
238    #[inline(never)]
239    fn open_group(&self, key: crate::slot::GroupKeySeed) -> ComposableGroup<'_> {
240        let host = self.active_slots_host();
241        let (host, pass) = if host.has_active_pass() {
242            (host, None)
243        } else {
244            let pass = self.begin_slot_host_pass(&host, crate::slot::SlotPassMode::Compose);
245            (self.active_slots_host(), Some(pass))
246        };
247        let (scope, runs_body) = self.open_group_in_active_pass(&host, key);
248        scope.begin_reads();
249        ComposableGroup {
250            composer: self,
251            host,
252            scope,
253            pass,
254            runs_body,
255            completed: false,
256            body: Cell::new(Body::Runs),
257        }
258    }
259
260    #[doc(hidden)]
261    pub fn __open_composable_group(&self, key: Key) -> ComposableGroup<'_> {
262        self.open_group(crate::slot::GroupKeySeed::unkeyed(key))
263    }
264
265    // Opens the group of a composable that returns nothing: in pausable
266    // content past the frame's budget, its first body waits.
267    #[doc(hidden)]
268    pub fn __open_pausable_group(&self, key: Key) -> ComposableGroup<'_> {
269        let group = self.open_group(crate::slot::GroupKeySeed::unkeyed(key));
270        if self.core.shared_state.pausing.is_holding() && self.pauses_first_body(&group.scope) {
271            group.body.set(Body::Paused);
272        }
273        group
274    }
275
276    pub(crate) fn with_group_seed<R>(
277        &self,
278        key: crate::slot::GroupKeySeed,
279        f: impl FnOnce(&Self) -> R,
280    ) -> R {
281        let group = self.open_group(key);
282        debug_assert!(group.runs_body, "only movable content waits for a body");
283        let result = f(self);
284        group.close();
285        result
286    }
287
288    pub(crate) fn with_movable_group(&self, id: Key, f: impl FnOnce(&Self)) {
289        let group = self.open_group(crate::slot::GroupKeySeed::movable(id));
290        if group.runs_body {
291            f(self);
292        }
293        group.close();
294    }
295
296    /// Runs `f` in a group identified by `key`. Slots and nodes that `f` creates belong to the
297    /// group.
298    pub fn with_group<R>(&self, key: Key, f: impl FnOnce(&Self) -> R) -> R {
299        self.with_group_seed(crate::slot::GroupKeySeed::unkeyed(key), f)
300    }
301
302    /// Runs `f` in a group identified by `key` that is retained when it leaves the composition,
303    /// with the recompose `options` applied to its scope. A retained group returns with its state
304    /// and nodes when `key` is composed again.
305    pub fn cranpose_with_reuse<R>(
306        &self,
307        key: Key,
308        mut options: RecomposeOptions,
309        f: impl FnOnce(&Self) -> R,
310    ) -> R {
311        options.retention = RetentionMode::RetainWhenInactive;
312        self.core.pending_scope_options.set(Some(options));
313        self.with_group(key, f)
314    }
315
316    /// Runs `f` in a group identified by the hash of `key`, so state and nodes follow the key when
317    /// siblings move. Compose's `key`.
318    #[track_caller]
319    pub fn with_key<K: Hash, R>(&self, key: &K, f: impl FnOnce(&Self) -> R) -> R {
320        let seed = explicit_group_key_seed(key, std::panic::Location::caller());
321        self.with_group_seed(seed, f)
322    }
323}