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#[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#[doc(hidden)]
25#[derive(Clone, Copy, PartialEq, Eq)]
26pub enum ParameterChange {
27 Unchanged,
29 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#[derive(Clone, Copy)]
45enum Body {
46 Runs,
47 Skipped,
49 Paused,
51}
52
53impl ComposableGroup<'_> {
54 pub fn scope(&self) -> &RecomposeScope {
56 &self.scope
57 }
58
59 #[inline]
63 pub fn set_recompose_fn(&self, body: fn(&Composer)) {
64 self.scope.set_recompose_fn(body);
65 }
66
67 pub fn skip(&self) {
70 self.body.set(Body::Skipped);
71 }
72
73 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 #[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 #[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 #[inline(never)]
223 fn close_skipped_group_in_active_pass(&self, host: &Rc<SlotsHost>, scope: &RecomposeScope) {
224 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 #[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 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 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 #[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}