@react-native-gpui/screens 0.0.0-stage → 4.28.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/LICENSE +21 -0
- package/README.md +22 -2
- package/desktop/CMakeLists.txt +30 -0
- package/desktop/Cargo.lock +7781 -0
- package/desktop/Cargo.toml +54 -0
- package/desktop/build.rs +38 -0
- package/desktop/cpp/GpuiScreensModule.cpp +77 -0
- package/desktop/src/lib.rs +29 -0
- package/desktop/src/lifecycle.rs +80 -0
- package/desktop/src/props.rs +177 -0
- package/desktop/src/render.rs +829 -0
- package/desktop/src/stack.rs +396 -0
- package/desktop/src/state.rs +162 -0
- package/desktop/src/transition.rs +193 -0
- package/desktop/src/unsupported.rs +39 -0
- package/package.json +31 -4
- package/react-native.config.js +36 -0
- package/src/FullWindowOverlay.desktop.tsx +19 -0
- package/src/core.desktop.ts +28 -0
- package/src/customDirectEventTypes.desktop.ts +16 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
//! The stack observer: everything react-native-screens does between a mount
|
|
2
|
+
//! batch and a paint.
|
|
3
|
+
//!
|
|
4
|
+
//! Core tells an observer about a batch before it applies it, about each insert
|
|
5
|
+
//! and removal as it applies them, and about the batch once it is done. All the
|
|
6
|
+
//! navigation decisions — which stack is nested, which card is leaving, when a
|
|
7
|
+
//! lifecycle event fires, when a card is kept alive for its exit — are made here
|
|
8
|
+
//! and remembered in `state`.
|
|
9
|
+
use crate::props;
|
|
10
|
+
use crate::state::{self, PopPlan, Role};
|
|
11
|
+
use crate::transition::{self, Role as TransitionRole};
|
|
12
|
+
use react_native_gpui::extensions::{
|
|
13
|
+
BatchOpKind, BatchView, ComponentNode, StoreHandle, StoreObserver, TreeView,
|
|
14
|
+
};
|
|
15
|
+
use std::collections::HashSet;
|
|
16
|
+
|
|
17
|
+
/// The component names this module owns.
|
|
18
|
+
pub const STACK: &str = "RNSScreenStack";
|
|
19
|
+
pub const SCREEN: &str = "RNSScreen";
|
|
20
|
+
pub const HEADER_CONFIG: &str = "RNSScreenStackHeaderConfig";
|
|
21
|
+
pub const HEADER_SUBVIEW: &str = "RNSScreenStackHeaderSubview";
|
|
22
|
+
pub const CONTENT_WRAPPER: &str = "RNSScreenContentWrapper";
|
|
23
|
+
pub const MODAL_SCREEN: &str = "RNSModalScreen";
|
|
24
|
+
pub const FULL_WINDOW_OVERLAY: &str = "RNSFullWindowOverlay";
|
|
25
|
+
|
|
26
|
+
/// Watches the batch and drives the stack.
|
|
27
|
+
pub struct StackObserver;
|
|
28
|
+
|
|
29
|
+
impl StoreObserver for StackObserver {
|
|
30
|
+
fn before_batch(
|
|
31
|
+
&self,
|
|
32
|
+
batch: &BatchView,
|
|
33
|
+
tree: &TreeView,
|
|
34
|
+
handle: &StoreHandle,
|
|
35
|
+
cx: &mut gpui::App,
|
|
36
|
+
) {
|
|
37
|
+
let nested = nested_stacks(batch, tree);
|
|
38
|
+
if std::env::var_os("RN_GPUI_DEBUG_SCREENS").is_some() && !nested.is_empty() {
|
|
39
|
+
log::info!("[screens] nested stack tags={nested:?}");
|
|
40
|
+
}
|
|
41
|
+
let plans = pop_plans(batch, tree, &nested, handle, cx);
|
|
42
|
+
for (tag, plan) in &plans {
|
|
43
|
+
if plan.retain {
|
|
44
|
+
handle.retain_subtree(*tag);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
state::with(|state| {
|
|
48
|
+
state.nested_stacks = nested;
|
|
49
|
+
state.pop_plans = plans;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn on_mount(&self, node: &ComponentNode, _handle: &StoreHandle, _cx: &mut gpui::App) {
|
|
54
|
+
if node.component == FULL_WINDOW_OVERLAY {
|
|
55
|
+
log::info!(
|
|
56
|
+
"[screens] full window overlay created tag={} frame={}x{}",
|
|
57
|
+
node.tag,
|
|
58
|
+
node.frame.w,
|
|
59
|
+
node.frame.h
|
|
60
|
+
);
|
|
61
|
+
} else if crate::unsupported::unsupported_component(&node.component) {
|
|
62
|
+
log::error!(
|
|
63
|
+
"[screens] component {} is not implemented on gpui",
|
|
64
|
+
node.component
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fn on_insert(
|
|
70
|
+
&self,
|
|
71
|
+
child: &ComponentNode,
|
|
72
|
+
parent: i32,
|
|
73
|
+
previous_children: &[i32],
|
|
74
|
+
tree: &TreeView,
|
|
75
|
+
handle: &StoreHandle,
|
|
76
|
+
cx: &mut gpui::App,
|
|
77
|
+
) {
|
|
78
|
+
if child.component != SCREEN || !is_outer_stack(parent) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
let animation = props::stack_animation(child).to_string();
|
|
82
|
+
let presentation = props::stack_presentation(child).to_string();
|
|
83
|
+
log::info!(
|
|
84
|
+
"[screens] screen inserted tag={} animation={} presentation={} reduce_motion={}",
|
|
85
|
+
child.tag,
|
|
86
|
+
animation,
|
|
87
|
+
presentation,
|
|
88
|
+
!tree.animations_enabled()
|
|
89
|
+
);
|
|
90
|
+
let previous_top = previous_children
|
|
91
|
+
.iter()
|
|
92
|
+
.rev()
|
|
93
|
+
.copied()
|
|
94
|
+
.find(|tag| tree.node(*tag).is_some_and(|node| node.component == SCREEN));
|
|
95
|
+
let mode = transition::transition_mode(&animation).filter(|_| tree.animations_enabled());
|
|
96
|
+
if let Some(mode) = mode {
|
|
97
|
+
log::info!(
|
|
98
|
+
"[screens] push transition incoming={} outgoing={:?} animation={} mode={mode:?}",
|
|
99
|
+
child.tag,
|
|
100
|
+
previous_top,
|
|
101
|
+
animation
|
|
102
|
+
);
|
|
103
|
+
if presentation == "push" {
|
|
104
|
+
if let Some(previous) = previous_top {
|
|
105
|
+
let distance = transition::PARALLAX_DISTANCE;
|
|
106
|
+
handle.start_transition(
|
|
107
|
+
previous,
|
|
108
|
+
transition::transition(mode, TransitionRole::Exit, distance),
|
|
109
|
+
cx,
|
|
110
|
+
);
|
|
111
|
+
state::with(|state| {
|
|
112
|
+
state.roles.insert(previous, (Role::Exit, distance));
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
handle.start_transition(
|
|
117
|
+
child.tag,
|
|
118
|
+
transition::transition(mode, TransitionRole::Enter, 1.0),
|
|
119
|
+
cx,
|
|
120
|
+
);
|
|
121
|
+
state::with(|state| {
|
|
122
|
+
state.roles.insert(child.tag, (Role::Enter, 1.0));
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
crate::lifecycle::dispatch_will(handle, previous_top, Some(child.tag), cx);
|
|
126
|
+
match mode {
|
|
127
|
+
Some(mode) => {
|
|
128
|
+
let outgoing = previous_top;
|
|
129
|
+
let incoming = Some(child.tag);
|
|
130
|
+
let handle = handle.clone();
|
|
131
|
+
handle.after(mode.duration(), cx, move |handle, cx| {
|
|
132
|
+
if let Some(outgoing) = outgoing {
|
|
133
|
+
handle.clear_transition(outgoing, cx);
|
|
134
|
+
}
|
|
135
|
+
crate::lifecycle::dispatch_did(handle, outgoing, incoming, cx);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
None => crate::lifecycle::dispatch_did(handle, previous_top, Some(child.tag), cx),
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fn on_remove_child(
|
|
143
|
+
&self,
|
|
144
|
+
child: i32,
|
|
145
|
+
parent: i32,
|
|
146
|
+
retained: bool,
|
|
147
|
+
tree: &TreeView,
|
|
148
|
+
handle: &StoreHandle,
|
|
149
|
+
cx: &mut gpui::App,
|
|
150
|
+
) {
|
|
151
|
+
let Some(node) = tree.node(child) else {
|
|
152
|
+
return;
|
|
153
|
+
};
|
|
154
|
+
if node.component != SCREEN {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
let plan = state::with(|state| state.pop_plans.remove(&child));
|
|
158
|
+
let Some(plan) = plan else {
|
|
159
|
+
return;
|
|
160
|
+
};
|
|
161
|
+
let _ = parent;
|
|
162
|
+
if let Some(revealed) = plan.revealed {
|
|
163
|
+
handle.clear_transition(revealed, cx);
|
|
164
|
+
}
|
|
165
|
+
let native_dismiss = state::with(|state| state.native_dismiss_pending == Some(child));
|
|
166
|
+
let mode = transition::transition_mode(&plan.animation)
|
|
167
|
+
.filter(|_| tree.animations_enabled())
|
|
168
|
+
.map(transition::TransitionMode::reverse);
|
|
169
|
+
let retain = plan.retain && mode.is_some();
|
|
170
|
+
log::info!(
|
|
171
|
+
"[screens] pop transition outgoing={} incoming={:?} animation={} retained={retain}",
|
|
172
|
+
child,
|
|
173
|
+
plan.revealed,
|
|
174
|
+
plan.animation
|
|
175
|
+
);
|
|
176
|
+
if retain {
|
|
177
|
+
if let Some(mode) = mode {
|
|
178
|
+
handle.start_transition(
|
|
179
|
+
child,
|
|
180
|
+
transition::transition(mode, TransitionRole::Exit, 1.0),
|
|
181
|
+
cx,
|
|
182
|
+
);
|
|
183
|
+
state::with(|state| {
|
|
184
|
+
state.roles.insert(child, (Role::Exit, 1.0));
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if !native_dismiss {
|
|
189
|
+
crate::lifecycle::dispatch_will(handle, Some(child), plan.revealed, cx);
|
|
190
|
+
}
|
|
191
|
+
if retained {
|
|
192
|
+
let duration = mode.map(|mode| mode.duration()).unwrap_or_default();
|
|
193
|
+
let revealed = plan.revealed;
|
|
194
|
+
let handle = handle.clone();
|
|
195
|
+
handle.after(duration, cx, move |handle, cx| {
|
|
196
|
+
handle.release_retained(child, cx);
|
|
197
|
+
log::info!("[screens] pop finished outgoing={child}");
|
|
198
|
+
crate::lifecycle::dispatch_did(handle, Some(child), revealed, cx);
|
|
199
|
+
});
|
|
200
|
+
} else if !native_dismiss {
|
|
201
|
+
crate::lifecycle::dispatch_did(handle, Some(child), plan.revealed, cx);
|
|
202
|
+
}
|
|
203
|
+
state::with(|state| {
|
|
204
|
+
if state.native_dismiss_pending == Some(child) {
|
|
205
|
+
state.native_dismiss_pending = None;
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
fn on_remove(&self, tag: i32, _handle: &StoreHandle, _cx: &mut gpui::App) {
|
|
211
|
+
state::with(|state| {
|
|
212
|
+
state.pop_plans.remove(&tag);
|
|
213
|
+
state.roles.remove(&tag);
|
|
214
|
+
state.headers.remove(&tag);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
fn on_update(
|
|
219
|
+
&self,
|
|
220
|
+
node: &ComponentNode,
|
|
221
|
+
previous: &ComponentNode,
|
|
222
|
+
handle: &StoreHandle,
|
|
223
|
+
cx: &mut gpui::App,
|
|
224
|
+
) {
|
|
225
|
+
if node.component != SCREEN {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
let before = props::activity_state(previous);
|
|
229
|
+
let after = props::activity_state(node);
|
|
230
|
+
if before == after {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
for event in crate::lifecycle::lifecycle_events(before, after) {
|
|
234
|
+
log::info!(
|
|
235
|
+
"[screens] tag={} activity={}->{} event={event}",
|
|
236
|
+
node.tag,
|
|
237
|
+
before,
|
|
238
|
+
after
|
|
239
|
+
);
|
|
240
|
+
handle.dispatch_event(node.tag, event, &serde_json::json!({}), cx);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
fn after_batch(&self, _tree: &TreeView, _handle: &StoreHandle, _cx: &mut gpui::App) {
|
|
245
|
+
state::clear_batch_state();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
fn on_transition_frame(
|
|
249
|
+
&self,
|
|
250
|
+
tag: i32,
|
|
251
|
+
progress: f32,
|
|
252
|
+
handle: &StoreHandle,
|
|
253
|
+
cx: &mut gpui::App,
|
|
254
|
+
) {
|
|
255
|
+
let forward = state::with(|state| {
|
|
256
|
+
state
|
|
257
|
+
.roles
|
|
258
|
+
.get(&tag)
|
|
259
|
+
.is_none_or(|(role, distance)| *role == Role::Enter || *distance < 1.0)
|
|
260
|
+
});
|
|
261
|
+
handle.dispatch_event(
|
|
262
|
+
tag,
|
|
263
|
+
"topTransitionProgress",
|
|
264
|
+
&serde_json::json!({
|
|
265
|
+
"progress": progress,
|
|
266
|
+
"closing": if forward { 0 } else { 1 },
|
|
267
|
+
"goingForward": if forward { 1 } else { 0 }
|
|
268
|
+
}),
|
|
269
|
+
cx,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/// Whether `stack` is the outer stack, the only one that animates.
|
|
275
|
+
fn is_outer_stack(stack: i32) -> bool {
|
|
276
|
+
!state::with(|state| state.nested_stacks.contains(&stack))
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/// Every stack that sits inside a screen, found from the batch topology before
|
|
280
|
+
/// any of it is applied.
|
|
281
|
+
fn nested_stacks(batch: &BatchView, tree: &TreeView) -> HashSet<i32> {
|
|
282
|
+
let stacks: Vec<i32> = batch
|
|
283
|
+
.ops
|
|
284
|
+
.iter()
|
|
285
|
+
.filter(|op| op.kind == BatchOpKind::Create && op.component == STACK)
|
|
286
|
+
.map(|op| op.tag)
|
|
287
|
+
.chain(
|
|
288
|
+
batch
|
|
289
|
+
.ops
|
|
290
|
+
.iter()
|
|
291
|
+
.filter(|op| op.kind != BatchOpKind::Create && op.component == STACK)
|
|
292
|
+
.map(|op| op.tag),
|
|
293
|
+
)
|
|
294
|
+
.collect();
|
|
295
|
+
let mut nested = HashSet::new();
|
|
296
|
+
for stack in stacks {
|
|
297
|
+
let mut cursor = Some(stack);
|
|
298
|
+
let mut visited = HashSet::new();
|
|
299
|
+
while let Some(tag) = cursor {
|
|
300
|
+
if !visited.insert(tag) {
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
let Some(parent) = batch.parent_of(tag).or_else(|| tree.parent(tag)) else {
|
|
304
|
+
break;
|
|
305
|
+
};
|
|
306
|
+
if batch.component_of(parent, tree).as_deref() == Some(SCREEN) {
|
|
307
|
+
nested.insert(stack);
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
cursor = Some(parent);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
nested
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/// The cards leaving in this batch that should animate their exit.
|
|
317
|
+
fn pop_plans(
|
|
318
|
+
batch: &BatchView,
|
|
319
|
+
tree: &TreeView,
|
|
320
|
+
nested: &HashSet<i32>,
|
|
321
|
+
handle: &StoreHandle,
|
|
322
|
+
cx: &mut gpui::App,
|
|
323
|
+
) -> std::collections::HashMap<i32, PopPlan> {
|
|
324
|
+
let removed: HashSet<i32> = batch
|
|
325
|
+
.ops
|
|
326
|
+
.iter()
|
|
327
|
+
.filter(|op| op.kind == BatchOpKind::Remove && op.component == SCREEN)
|
|
328
|
+
.map(|op| op.tag)
|
|
329
|
+
.collect();
|
|
330
|
+
let mut plans = std::collections::HashMap::new();
|
|
331
|
+
for op in batch
|
|
332
|
+
.ops
|
|
333
|
+
.iter()
|
|
334
|
+
.filter(|op| op.kind == BatchOpKind::Remove && op.component == SCREEN)
|
|
335
|
+
{
|
|
336
|
+
if nested.contains(&op.parent_tag) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
let Some(stack) = tree.node(op.parent_tag) else {
|
|
340
|
+
continue;
|
|
341
|
+
};
|
|
342
|
+
if stack.component != STACK {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
let live: Vec<i32> = tree
|
|
346
|
+
.children(op.parent_tag)
|
|
347
|
+
.into_iter()
|
|
348
|
+
.filter(|tag| {
|
|
349
|
+
tree.is_live(*tag) && tree.node(*tag).is_some_and(|node| node.component == SCREEN)
|
|
350
|
+
})
|
|
351
|
+
.collect();
|
|
352
|
+
if live.last().copied() != Some(op.tag) {
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
let revealed = live
|
|
356
|
+
.iter()
|
|
357
|
+
.rev()
|
|
358
|
+
.find(|tag| !removed.contains(tag))
|
|
359
|
+
.copied();
|
|
360
|
+
let Some(screen) = tree.node(op.tag) else {
|
|
361
|
+
continue;
|
|
362
|
+
};
|
|
363
|
+
let animation = props::stack_animation(&screen).to_string();
|
|
364
|
+
let native_dismiss = state::with(|state| state.native_dismiss_pending == Some(op.tag));
|
|
365
|
+
let mode = transition::transition_mode(&animation).filter(|_| tree.animations_enabled());
|
|
366
|
+
let retain = !native_dismiss && revealed.is_some() && mode.is_some();
|
|
367
|
+
if retain {
|
|
368
|
+
log::info!(
|
|
369
|
+
"[screens] preserve outgoing={} descendants={}",
|
|
370
|
+
op.tag,
|
|
371
|
+
descendant_count(op.tag, tree)
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
plans.insert(
|
|
375
|
+
op.tag,
|
|
376
|
+
PopPlan {
|
|
377
|
+
revealed,
|
|
378
|
+
animation,
|
|
379
|
+
duration: mode.map(|mode| mode.duration()).unwrap_or_default(),
|
|
380
|
+
retain,
|
|
381
|
+
},
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
plans
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/// How many nodes sit below `tag`, at any depth.
|
|
388
|
+
fn descendant_count(tag: i32, tree: &TreeView) -> usize {
|
|
389
|
+
let mut total = 0;
|
|
390
|
+
let mut stack = tree.children(tag);
|
|
391
|
+
while let Some(next) = stack.pop() {
|
|
392
|
+
total += 1;
|
|
393
|
+
stack.extend(tree.children(next));
|
|
394
|
+
}
|
|
395
|
+
total
|
|
396
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
//! The module's own state, kept outside core.
|
|
2
|
+
//!
|
|
3
|
+
//! Core hands an observer facts about a batch and forgets them; everything a
|
|
4
|
+
//! stack has to remember between batches — which stacks are nested, which cards
|
|
5
|
+
//! are leaving, what a dismissal already animated — is here.
|
|
6
|
+
use std::collections::{HashMap, HashSet};
|
|
7
|
+
use std::sync::{Mutex, OnceLock};
|
|
8
|
+
use std::time::Duration;
|
|
9
|
+
|
|
10
|
+
/// What `decorate` learned about a header, for `content` to use.
|
|
11
|
+
#[derive(Clone, Copy, Debug, Default)]
|
|
12
|
+
pub struct HeaderFacts {
|
|
13
|
+
pub is_top: bool,
|
|
14
|
+
pub shows_back: bool,
|
|
15
|
+
pub has_title_subview: bool,
|
|
16
|
+
/// The screen this header belongs to, which is where a back-button event
|
|
17
|
+
/// has to go: React Native declares `onHeaderBackButtonClicked` on
|
|
18
|
+
/// `RNSScreen`, not on the header config.
|
|
19
|
+
pub screen: Option<i32>,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// Records what `decorate` learned, so `content` can read it.
|
|
23
|
+
pub fn set_header_facts(
|
|
24
|
+
tag: i32,
|
|
25
|
+
is_top: bool,
|
|
26
|
+
shows_back: bool,
|
|
27
|
+
has_title_subview: bool,
|
|
28
|
+
screen: Option<i32>,
|
|
29
|
+
) {
|
|
30
|
+
with(|state| {
|
|
31
|
+
state.headers.insert(
|
|
32
|
+
tag,
|
|
33
|
+
HeaderFacts {
|
|
34
|
+
is_top,
|
|
35
|
+
shows_back,
|
|
36
|
+
has_title_subview,
|
|
37
|
+
screen,
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Whether this header is the top screen's, as of the last `decorate` call.
|
|
44
|
+
pub fn header_is_top(tag: i32) -> bool {
|
|
45
|
+
with(|state| state.headers.get(&tag).is_some_and(|facts| facts.is_top))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Whether this header carries a `title` subview, as of the last `decorate` call.
|
|
49
|
+
///
|
|
50
|
+
/// When it does, that subview paints the title and core must not paint its own.
|
|
51
|
+
pub fn header_has_title_subview(tag: i32) -> bool {
|
|
52
|
+
with(|state| {
|
|
53
|
+
state
|
|
54
|
+
.headers
|
|
55
|
+
.get(&tag)
|
|
56
|
+
.is_some_and(|facts| facts.has_title_subview)
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// The screen a header belongs to, as of the last `decorate` call.
|
|
61
|
+
pub fn header_screen(tag: i32) -> Option<i32> {
|
|
62
|
+
with(|state| state.headers.get(&tag).and_then(|facts| facts.screen))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Whether this header shows a back button, as of the last `decorate` call.
|
|
66
|
+
pub fn header_shows_back(tag: i32) -> bool {
|
|
67
|
+
with(|state| {
|
|
68
|
+
state
|
|
69
|
+
.headers
|
|
70
|
+
.get(&tag)
|
|
71
|
+
.is_some_and(|facts| facts.shows_back)
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// A card that is leaving, and what should happen when it is gone.
|
|
76
|
+
#[derive(Clone, Debug)]
|
|
77
|
+
pub struct PopPlan {
|
|
78
|
+
/// The screen that becomes top when this one is gone.
|
|
79
|
+
pub revealed: Option<i32>,
|
|
80
|
+
/// The animation name React Native asked for, unparsed.
|
|
81
|
+
pub animation: String,
|
|
82
|
+
/// How long the exit runs.
|
|
83
|
+
pub duration: Duration,
|
|
84
|
+
/// Whether the card must stay attached so its exit can paint.
|
|
85
|
+
pub retain: bool,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Everything the module remembers between batches.
|
|
89
|
+
#[derive(Default)]
|
|
90
|
+
pub struct State {
|
|
91
|
+
/// A native dismissal already ran the exit animation, so the removal must
|
|
92
|
+
/// not run a second one and must not send the "will" events again.
|
|
93
|
+
pub native_dismiss_pending: Option<i32>,
|
|
94
|
+
/// Stacks that sit inside a screen, so only the outer stack animates.
|
|
95
|
+
pub nested_stacks: HashSet<i32>,
|
|
96
|
+
/// Cards leaving in this batch, by tag.
|
|
97
|
+
pub pop_plans: HashMap<i32, PopPlan>,
|
|
98
|
+
/// The cards this module started a transition on: whether it is an entry
|
|
99
|
+
/// (the screen is arriving) or an exit, and how far it travels.
|
|
100
|
+
pub roles: HashMap<i32, (Role, f32)>,
|
|
101
|
+
/// The latest facts about each header, written by `decorate`.
|
|
102
|
+
pub headers: HashMap<i32, HeaderFacts>,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Whether a transition the module started is an arrival or a departure.
|
|
106
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
107
|
+
pub enum Role {
|
|
108
|
+
Enter,
|
|
109
|
+
Exit,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fn state() -> &'static Mutex<State> {
|
|
113
|
+
static STATE: OnceLock<Mutex<State>> = OnceLock::new();
|
|
114
|
+
STATE.get_or_init(|| Mutex::new(State::default()))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Runs `f` with the module state.
|
|
118
|
+
pub fn with<R>(f: impl FnOnce(&mut State) -> R) -> R {
|
|
119
|
+
let mut state = state()
|
|
120
|
+
.lock()
|
|
121
|
+
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
122
|
+
f(&mut state)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Forgets the per-batch decisions. Call from `after_batch`.
|
|
126
|
+
pub fn clear_batch_state() {
|
|
127
|
+
with(|state| {
|
|
128
|
+
state.pop_plans.clear();
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#[cfg(test)]
|
|
133
|
+
mod tests {
|
|
134
|
+
use super::*;
|
|
135
|
+
|
|
136
|
+
#[test]
|
|
137
|
+
fn batch_state_is_cleared_but_lifetimes_are_not() {
|
|
138
|
+
with(|state| {
|
|
139
|
+
state.pop_plans.insert(
|
|
140
|
+
7,
|
|
141
|
+
PopPlan {
|
|
142
|
+
revealed: Some(3),
|
|
143
|
+
animation: "slide_from_right".to_string(),
|
|
144
|
+
duration: Duration::from_millis(240),
|
|
145
|
+
retain: true,
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
state.nested_stacks.insert(9);
|
|
149
|
+
state.native_dismiss_pending = Some(7);
|
|
150
|
+
});
|
|
151
|
+
clear_batch_state();
|
|
152
|
+
with(|state| {
|
|
153
|
+
assert!(state.pop_plans.is_empty());
|
|
154
|
+
assert!(state.nested_stacks.contains(&9));
|
|
155
|
+
assert_eq!(state.native_dismiss_pending, Some(7));
|
|
156
|
+
});
|
|
157
|
+
with(|state| {
|
|
158
|
+
state.nested_stacks.clear();
|
|
159
|
+
state.native_dismiss_pending = None;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|