Skip to content
25 changes: 14 additions & 11 deletions fixtures/view-transition/src/components/Page.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,17 +171,20 @@ export default function Page({url, navigate}) {
}}>
<h1>{!show ? 'A' + counter : 'B'}</h1>
</ViewTransition>
{show ? (
<div>
{a}
{b}
</div>
) : (
<div>
{b}
{a}
</div>
)}
{
// Using url instead of renderedUrl here lets us only update this on commit.
url === '/?b' ? (
<div>
{a}
{b}
</div>
) : (
<div>
{b}
{a}
</div>
)
}
<ViewTransition>
{show ? (
<div>hello{exclamation}</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,11 @@ function measureViewTransitionHostInstancesRecursive(
}
viewTransitionCancelableChildren.push(
instance,
oldName,
viewTransitionHostInstanceIdx === 0
? oldName
: // If we have multiple Host Instances below, we add a suffix to the name to give
// each one a unique name.
oldName + '_' + viewTransitionHostInstanceIdx,
child.memoizedProps,
);
}
Expand Down
159 changes: 99 additions & 60 deletions packages/react-reconciler/src/ReactFiberGestureScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,21 @@ import type {FiberRoot} from './ReactInternalTypes';
import type {GestureOptions} from 'shared/ReactTypes';
import type {GestureTimeline, RunningViewTransition} from './ReactFiberConfig';
import type {TransitionTypes} from 'react/src/ReactTransitionType';
import type {Lane} from './ReactFiberLane';

import {
GestureLane,
includesBlockingLane,
includesTransitionLane,
markRootEntangled,
markRootFinished,
NoLane,
NoLanes,
} from './ReactFiberLane';
import {ensureRootIsScheduled} from './ReactFiberRootScheduler';
import {
ensureRootIsScheduled,
requestTransitionLane,
} from './ReactFiberRootScheduler';
import {getCurrentGestureOffset, stopViewTransition} from './ReactFiberConfig';
import {pingGestureRoot, restartGestureRoot} from './ReactFiberWorkLoop';

// This type keeps track of any scheduled or active gestures.
export type ScheduledGesture = {
Expand All @@ -28,6 +35,8 @@ export type ScheduledGesture = {
rangeEnd: number, // The percentage along the timeline where the "destination" state is reached.
types: null | TransitionTypes, // Any addTransitionType call made during startGestureTransition.
running: null | RunningViewTransition, // Used to cancel the running transition after we're done.
committing: boolean, // If the gesture was released in a committed state and should actually commit.
revertLane: Lane, // The Lane that we'll use to schedule the revert.
prev: null | ScheduledGesture, // The previous scheduled gesture in the queue for this root.
next: null | ScheduledGesture, // The next scheduled gesture in the queue for this root.
};
Expand Down Expand Up @@ -55,6 +64,8 @@ export function scheduleGesture(
rangeEnd: 100, // Uninitialized
types: null,
running: null,
committing: false,
revertLane: NoLane, // Starts uninitialized.
prev: prev,
next: null,
};
Expand Down Expand Up @@ -120,79 +131,107 @@ export function cancelScheduledGesture(
root: FiberRoot,
gesture: ScheduledGesture,
): void {
// Entangle any Transitions started in this event with the revertLane of the gesture
// so that we commit them all together.
if (gesture.revertLane !== NoLane) {
const entangledLanes = gesture.revertLane | requestTransitionLane(null);
markRootEntangled(root, entangledLanes);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing that's a bit strange with this one is that the revert lane isn't actually scheduled at this point yet because it's not pending until we actually commit the gesture lane which is the one that leaves the work behind. Ideally this should probably be done somewhere in the commit phase instead.

}

gesture.count--;
if (gesture.count === 0) {
// Delete the scheduled gesture from the pending queue.
deleteScheduledGesture(root, gesture);
// If the end state is closer to the end than the beginning then we commit into the
// end state before reverting back (or applying a new Transition).
// Otherwise we just revert back and don't commit.
let shouldCommit: boolean;
const finalOffset = getCurrentGestureOffset(gesture.provider);
const rangeStart = gesture.rangeStart;
const rangeEnd = gesture.rangeEnd;
if (rangeStart < rangeEnd) {
shouldCommit = finalOffset > rangeStart + (rangeEnd - rangeStart) / 2;
} else {
shouldCommit = finalOffset < rangeEnd + (rangeStart - rangeEnd) / 2;
}
// TODO: If we're currently rendering this gesture, we need to restart the render
// on a different gesture or cancel the render..
// TODO: We might want to pause the View Transition at this point since you should
// no longer be able to update the position of anything but it might be better to
// just commit the gesture state.
const runningTransition = gesture.running;
if (runningTransition !== null) {
const pendingLanesExcludingGestureLane = root.pendingLanes & ~GestureLane;
if (
includesBlockingLane(pendingLanesExcludingGestureLane) ||
includesTransitionLane(pendingLanesExcludingGestureLane)
) {
// If we have pending work we schedule the gesture to be stopped at the next commit.
// This ensures that we don't snap back to the previous state until we have
// had a chance to commit any resulting updates.
const existing = root.stoppingGestures;
if (existing !== null) {
gesture.next = existing;
existing.prev = gesture;
if (runningTransition !== null && shouldCommit) {
// If we are going to commit this gesture in its to state, we need to wait to
// stop it until it commits. We should now schedule a render at the gesture
// lane to actually commit it.
gesture.committing = true;
if (root.pendingGestures === gesture) {
// Ping the root given the new state. This is similar to pingSuspendedRoot.
// This will either schedule the gesture lane to be committed possibly from its current state.
pingGestureRoot(root);
}
} else {
// If we're not going to commit this gesture we can stop the View Transition
// right away and delete the scheduled gesture from the pending queue.
if (gesture.prev === null) {
if (root.pendingGestures === gesture) {
// This was the currently rendering gesture.
root.pendingGestures = gesture.next;
let remainingLanes = root.pendingLanes;
if (root.pendingGestures === null) {
// Gestures don't clear their lanes while the gesture is still active but it
// might not be scheduled to do any more renders and so we shouldn't schedule
// any more gesture lane work until a new gesture is scheduled.
remainingLanes &= ~GestureLane;
}
markRootFinished(
root,
GestureLane,
remainingLanes,
NoLane,
NoLane,
NoLanes,
);
// If we had a currently rendering gesture we need to now reset the gesture lane to
// now render the next gesture or cancel if there's no more gestures in the queue.
restartGestureRoot(root);
}
root.stoppingGestures = gesture;
} else {
gesture.running = null;
// If there's no work scheduled so we can stop the View Transition right away.
stopViewTransition(runningTransition);
if (runningTransition !== null) {
stopViewTransition(runningTransition);
}
} else {
// This was not the current gesture so it doesn't affect the current render.
gesture.prev.next = gesture.next;
if (gesture.next !== null) {
gesture.next.prev = gesture.prev;
}
gesture.prev = null;
gesture.next = null;
}
}
}
}

export function deleteScheduledGesture(
root: FiberRoot,
gesture: ScheduledGesture,
): void {
if (gesture.prev === null) {
if (root.pendingGestures === gesture) {
root.pendingGestures = gesture.next;
if (root.pendingGestures === null) {
// Gestures don't clear their lanes while the gesture is still active but it
// might not be scheduled to do any more renders and so we shouldn't schedule
// any more gesture lane work until a new gesture is scheduled.
root.pendingLanes &= ~GestureLane;
}
}
if (root.stoppingGestures === gesture) {
// This should not really happen the way we use it now but just in case we start.
root.stoppingGestures = gesture.next;
export function stopCommittedGesture(root: FiberRoot) {
// The top was just committed. We can delete it from the queue
// and stop its View Transition now.
const committedGesture = root.pendingGestures;
if (committedGesture !== null) {
// Mark it as no longer committing and should no longer be included in rerenders.
committedGesture.committing = false;
const nextGesture = committedGesture.next;
if (nextGesture === null) {
// Gestures don't clear their lanes while the gesture is still active but it
// might not be scheduled to do any more renders and so we shouldn't schedule
// any more gesture lane work until a new gesture is scheduled.
root.pendingLanes &= ~GestureLane;
} else {
nextGesture.prev = null;
}
} else {
gesture.prev.next = gesture.next;
if (gesture.next !== null) {
gesture.next.prev = gesture.prev;
}
gesture.prev = null;
gesture.next = null;
}
}

export function stopCompletedGestures(root: FiberRoot) {
let gesture = root.stoppingGestures;
root.stoppingGestures = null;
while (gesture !== null) {
if (gesture.running !== null) {
stopViewTransition(gesture.running);
gesture.running = null;
root.pendingGestures = nextGesture;
const runningTransition = committedGesture.running;
if (runningTransition !== null) {
committedGesture.running = null;
stopViewTransition(runningTransition);
}
const nextGesture = gesture.next;
gesture.next = null;
gesture.prev = null;
gesture = nextGesture;
}
}
11 changes: 9 additions & 2 deletions packages/react-reconciler/src/ReactFiberHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -1384,7 +1384,7 @@ function updateReducerImpl<S, A>(
// ScheduledGesture.
const scheduledGesture = update.gesture;
if (scheduledGesture !== null) {
if (scheduledGesture.count === 0) {
if (scheduledGesture.count === 0 && !scheduledGesture.committing) {
// This gesture has already been cancelled. We can clean up this update.
update = update.next;
continue;
Expand Down Expand Up @@ -3792,7 +3792,14 @@ function dispatchOptimisticSetState<S, A>(
if (provider !== null) {
// If this was a gesture, ensure we have a scheduled gesture and that
// we associate this update with this specific gesture instance.
update.gesture = scheduleGesture(root, provider);
const gesture = (update.gesture = scheduleGesture(root, provider));
// Ensure the gesture always uses the same revert lane. This can happen for
// two startGestureTransition calls to the same provider in different events.
if (gesture.revertLane === NoLane) {
gesture.revertLane = update.revertLane;
} else {
update.revertLane = gesture.revertLane;
}
}
}
}
Expand Down
1 change: 0 additions & 1 deletion packages/react-reconciler/src/ReactFiberRoot.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ function FiberRootNode(

if (enableGestureTransition) {
this.pendingGestures = null;
this.stoppingGestures = null;
this.gestureClone = null;
}

Expand Down
Loading
Loading