main
js 259 lines 9.16 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {FiberRoot} from './ReactInternalTypes';
11 import type {GestureOptions} from 'shared/ReactTypes';
12 import type {GestureTimeline, RunningViewTransition} from './ReactFiberConfig';
13 import type {TransitionTypes} from 'react/src/ReactTransitionType';
14 import type {Lane} from './ReactFiberLane';
15
16 import {
17 GestureLane,
18 markRootEntangled,
19 markRootFinished,
20 NoLane,
21 NoLanes,
22 } from './ReactFiberLane';
23 import {
24 ensureRootIsScheduled,
25 requestTransitionLane,
26 } from './ReactFiberRootScheduler';
27 import {getCurrentGestureOffset, stopViewTransition} from './ReactFiberConfig';
28 import {pingGestureRoot, restartGestureRoot} from './ReactFiberWorkLoop';
29
30 // This type keeps track of any scheduled or active gestures.
31 export type ScheduledGesture = {
32 provider: GestureTimeline,
33 count: number, // The number of times this same provider has been started.
34 rangeStart: number, // The percentage along the timeline where the "current" state starts.
35 rangeEnd: number, // The percentage along the timeline where the "destination" state is reached.
36 types: null | TransitionTypes, // Any addTransitionType call made during startGestureTransition.
37 running: null | RunningViewTransition, // Used to cancel the running transition after we're done.
38 commit: null | (() => void), // Callback to run to commit if there's a pending commit.
39 committing: boolean, // If the gesture was released in a committed state and should actually commit.
40 revertLane: Lane, // The Lane that we'll use to schedule the revert.
41 prev: null | ScheduledGesture, // The previous scheduled gesture in the queue for this root.
42 next: null | ScheduledGesture, // The next scheduled gesture in the queue for this root.
43 };
44
45 export function scheduleGesture(
46 root: FiberRoot,
47 provider: GestureTimeline,
48 ): ScheduledGesture {
49 let prev = root.pendingGestures;
50 while (prev !== null) {
51 if (prev.provider === provider) {
52 // Existing instance found.
53 return prev;
54 }
55 const next = prev.next;
56 // $FlowFixMe[invalid-compare]
57 if (next === null) {
58 break;
59 }
60 prev = next;
61 }
62 const gesture: ScheduledGesture = {
63 provider: provider,
64 count: 0,
65 rangeStart: 0, // Uninitialized
66 rangeEnd: 100, // Uninitialized
67 types: null,
68 running: null,
69 commit: null,
70 committing: false,
71 revertLane: NoLane, // Starts uninitialized.
72 prev: prev,
73 next: null,
74 };
75 if (prev === null) {
76 root.pendingGestures = gesture;
77 } else {
78 prev.next = gesture;
79 }
80 ensureRootIsScheduled(root);
81 return gesture;
82 }
83
84 export function startScheduledGesture(
85 root: FiberRoot,
86 gestureTimeline: GestureTimeline,
87 gestureOptions: ?GestureOptions,
88 transitionTypes: null | TransitionTypes,
89 ): null | ScheduledGesture {
90 const rangeStart =
91 gestureOptions && gestureOptions.rangeStart != null
92 ? gestureOptions.rangeStart
93 : getCurrentGestureOffset(gestureTimeline);
94 const rangeEnd =
95 gestureOptions && gestureOptions.rangeEnd != null
96 ? gestureOptions.rangeEnd
97 : rangeStart < 50
98 ? 100
99 : 0;
100 let prev = root.pendingGestures;
101 while (prev !== null) {
102 if (prev.provider === gestureTimeline) {
103 // Existing instance found.
104 prev.count++;
105 // Update the options.
106 prev.rangeStart = rangeStart;
107 prev.rangeEnd = rangeEnd;
108 if (transitionTypes !== null) {
109 let scheduledTypes = prev.types;
110 if (scheduledTypes === null) {
111 scheduledTypes = prev.types = [];
112 }
113 for (let i = 0; i < transitionTypes.length; i++) {
114 const transitionType = transitionTypes[i];
115 if (scheduledTypes.indexOf(transitionType) === -1) {
116 scheduledTypes.push(transitionType);
117 }
118 }
119 }
120 return prev;
121 }
122 const next = prev.next;
123 // $FlowFixMe[invalid-compare]
124 if (next === null) {
125 break;
126 }
127 prev = next;
128 }
129 // No scheduled gestures. It must mean nothing for this renderer updated but
130 // some other renderer might have updated.
131 return null;
132 }
133
134 export function cancelScheduledGesture(
135 root: FiberRoot,
136 gesture: ScheduledGesture,
137 ): void {
138 // Entangle any Transitions started in this event with the revertLane of the gesture
139 // so that we commit them all together.
140 if (gesture.revertLane !== NoLane) {
141 const entangledLanes = gesture.revertLane | requestTransitionLane(null);
142 markRootEntangled(root, entangledLanes);
143 }
144
145 gesture.count--;
146 if (gesture.count === 0) {
147 // If the end state is closer to the end than the beginning then we commit into the
148 // end state before reverting back (or applying a new Transition).
149 // Otherwise we just revert back and don't commit.
150 let shouldCommit: boolean;
151 const finalOffset = getCurrentGestureOffset(gesture.provider);
152 const rangeStart = gesture.rangeStart;
153 const rangeEnd = gesture.rangeEnd;
154 if (rangeStart < rangeEnd) {
155 shouldCommit = finalOffset > rangeStart + (rangeEnd - rangeStart) / 2;
156 } else {
157 shouldCommit = finalOffset < rangeEnd + (rangeStart - rangeEnd) / 2;
158 }
159 // TODO: If we're currently rendering this gesture, we need to restart the render
160 // on a different gesture or cancel the render..
161 // TODO: We might want to pause the View Transition at this point since you should
162 // no longer be able to update the position of anything but it might be better to
163 // just commit the gesture state.
164 const runningTransition = gesture.running;
165 if (runningTransition !== null && shouldCommit) {
166 // If we are going to commit this gesture in its to state, we need to wait to
167 // stop it until it commits. We should now schedule a render at the gesture
168 // lane to actually commit it.
169 gesture.committing = true;
170 if (root.pendingGestures === gesture) {
171 const commitCallback = gesture.commit;
172 if (commitCallback !== null) {
173 gesture.commit = null;
174 // If we already have a commit prepared we can immediately commit the tree
175 // without rerendering.
176 // TODO: Consider scheduling this in a task instead of synchronously inside the last cancellation.s
177 commitCallback();
178 } else {
179 // Ping the root given the new state. This is similar to pingSuspendedRoot.
180 pingGestureRoot(root);
181 }
182 }
183 } else {
184 // If we're not going to commit this gesture we can stop the View Transition
185 // right away and delete the scheduled gesture from the pending queue.
186 if (gesture.prev === null) {
187 if (root.pendingGestures === gesture) {
188 // This was the currently rendering gesture.
189 root.pendingGestures = gesture.next;
190 let remainingLanes = root.pendingLanes;
191 if (root.pendingGestures === null) {
192 // Gestures don't clear their lanes while the gesture is still active but it
193 // might not be scheduled to do any more renders and so we shouldn't schedule
194 // any more gesture lane work until a new gesture is scheduled.
195 remainingLanes &= ~GestureLane;
196 }
197 markRootFinished(
198 root,
199 GestureLane,
200 remainingLanes,
201 NoLane,
202 NoLane,
203 NoLanes,
204 );
205 // If we had a currently rendering gesture we need to now reset the gesture lane to
206 // now render the next gesture or cancel if there's no more gestures in the queue.
207 restartGestureRoot(root);
208 }
209 gesture.running = null;
210 if (runningTransition !== null) {
211 stopViewTransition(runningTransition);
212 }
213 } else {
214 // This was not the current gesture so it doesn't affect the current render.
215 gesture.prev.next = gesture.next;
216 if (gesture.next !== null) {
217 gesture.next.prev = gesture.prev;
218 }
219 gesture.prev = null;
220 gesture.next = null;
221 }
222 }
223 }
224 }
225
226 export function stopCommittedGesture(root: FiberRoot) {
227 // The top was just committed. We can delete it from the queue
228 // and stop its View Transition now.
229 const committedGesture = root.pendingGestures;
230 if (committedGesture !== null) {
231 // Mark it as no longer committing and should no longer be included in rerenders.
232 committedGesture.committing = false;
233 const nextGesture = committedGesture.next;
234 if (nextGesture === null) {
235 // Gestures don't clear their lanes while the gesture is still active but it
236 // might not be scheduled to do any more renders and so we shouldn't schedule
237 // any more gesture lane work until a new gesture is scheduled.
238 root.pendingLanes &= ~GestureLane;
239 } else {
240 nextGesture.prev = null;
241 }
242 root.pendingGestures = nextGesture;
243 const runningTransition = committedGesture.running;
244 if (runningTransition !== null) {
245 committedGesture.running = null;
246 stopViewTransition(runningTransition);
247 }
248 }
249 }
250
251 export function scheduleGestureCommit(
252 gesture: ScheduledGesture,
253 callback: () => void,
254 ): () => void {
255 gesture.commit = callback;
256 return function () {
257 gesture.commit = null;
258 };
259 }