Add .onCommit() to the gesture builder
`.onCommit(handler)` is available at every builder stage after the direction, returns the same stage, and accumulates handlers in order. Handlers run once, synchronously at release, when the operation commits and before the settling animation, with the same context `.complete()` receives. A throwing handler is reported without stopping later handlers or the operation. Intended for haptic feedback such as `navigator.vibrate`. Named onCommit rather than then so builder stages never become thenables. The builder now threads one frozen draft through its stages. OriginOperationHandle gains a read-only `intent`, which the recognizer reads right after finish() to know whether the release committed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Faarh2Ts6StJPcz2mRiHiR
This commit is contained in:
@@ -411,6 +411,7 @@ The stages have distinct responsibilities:
|
|||||||
| `.to.*` | Required movement direction and intent-recognition options |
|
| `.to.*` | Required movement direction and intent-recognition options |
|
||||||
| `.complete(predicate)` | Optional release commit/cancel decision |
|
| `.complete(predicate)` | Optional release commit/cancel decision |
|
||||||
| `.navigate(factory)` | Required target and retained-history intent |
|
| `.navigate(factory)` | Required target and retained-history intent |
|
||||||
|
| `.onCommit(handler)` | Optional side effect when a release commits |
|
||||||
| `.animate(routine)` | Required source/target/frame choreography |
|
| `.animate(routine)` | Required source/target/frame choreography |
|
||||||
|
|
||||||
The builder is persistent and immutable. Reusing an earlier stage cannot
|
The builder is persistent and immutable. Reusing an earlier stage cannot
|
||||||
@@ -432,6 +433,27 @@ velocity, directed pixel distance, cross-axis distance, duration, pointer-up
|
|||||||
event, host, bounds, and start/current points. Completion predicates are
|
event, host, bounds, and start/current points. Completion predicates are
|
||||||
synchronous because they select operation intent at release.
|
synchronous because they select operation intent at release.
|
||||||
|
|
||||||
|
`.onCommit(handler)` is available at every stage after `.to.*`, returns the
|
||||||
|
same stage, and may be called more than once; handlers run in order. A handler
|
||||||
|
runs once, synchronously at release, after the operation commits and before
|
||||||
|
the settling animation, and receives the same context as `.complete()`. It
|
||||||
|
never runs for a cancelled release. A throwing handler is reported as an
|
||||||
|
uncaught error without stopping later handlers or the operation. It is the
|
||||||
|
place for haptic feedback:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const openProfile = gesture.to
|
||||||
|
.left()
|
||||||
|
.navigate(() => forward(originView(ProfileView)))
|
||||||
|
.onCommit(() => navigator.vibrate?.(10))
|
||||||
|
.animate(slideLeft);
|
||||||
|
```
|
||||||
|
|
||||||
|
`navigator.vibrate` is unavailable in iOS Safari; Capacitor apps can call
|
||||||
|
`Haptics.impact()` from `@capacitor/haptics` instead. The method is named
|
||||||
|
`onCommit` rather than `then` so builder stages never become promise-like
|
||||||
|
"thenables" when awaited or returned from an async function.
|
||||||
|
|
||||||
### `useOriginGesture(definition)`
|
### `useOriginGesture(definition)`
|
||||||
|
|
||||||
Creates one primary-pointer, single-axis recognizer owned by the component that
|
Creates one primary-pointer, single-axis recognizer owned by the component that
|
||||||
@@ -640,6 +662,9 @@ can immediately begin its own outgoing operation, enabling X → Y → Z chains.
|
|||||||
|
|
||||||
### `OriginOperationHandle`
|
### `OriginOperationHandle`
|
||||||
|
|
||||||
|
`intent` reports the selected outcome (`undecided`, `commit`, or `cancel`). It
|
||||||
|
changes synchronously when `finish()` or `cancel()` is called.
|
||||||
|
|
||||||
| Member | Description |
|
| Member | Description |
|
||||||
| ----------------------------- | --------------------------------------------------- |
|
| ----------------------------- | --------------------------------------------------- |
|
||||||
| `id` | Unique operation ID |
|
| `id` | Unique operation ID |
|
||||||
|
|||||||
@@ -168,8 +168,22 @@ export interface OriginGestureDefinition {
|
|||||||
readonly navigation: OriginGestureNavigationFactory;
|
readonly navigation: OriginGestureNavigationFactory;
|
||||||
/** Visual routine paired with the navigation intent. */
|
/** Visual routine paired with the navigation intent. */
|
||||||
readonly choreography: OriginChoreography;
|
readonly choreography: OriginChoreography;
|
||||||
|
/** Handlers run, in order, when a release commits. See `.onCommit()`. */
|
||||||
|
readonly commitHandlers?: readonly OriginGestureCommitHandler[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Side effect run when a released gesture commits, such as haptic feedback.
|
||||||
|
*
|
||||||
|
* It runs once, synchronously at release, after the commit decision and
|
||||||
|
* before the settling animation. It receives the same context as a
|
||||||
|
* `.complete()` predicate. Errors are reported without interrupting the
|
||||||
|
* operation.
|
||||||
|
*/
|
||||||
|
export type OriginGestureCommitHandler = (
|
||||||
|
context: OriginGestureCompletionContext,
|
||||||
|
) => void;
|
||||||
|
|
||||||
/** Direction-selection stage shared by `gesture.to` and `.from.*().to`. */
|
/** Direction-selection stage shared by `gesture.to` and `.from.*().to`. */
|
||||||
export interface OriginGestureToBuilder {
|
export interface OriginGestureToBuilder {
|
||||||
/** Recognize leftward pointer movement. */
|
/** Recognize leftward pointer movement. */
|
||||||
@@ -214,6 +228,8 @@ export interface OriginGestureDirectedBuilder {
|
|||||||
navigate(
|
navigate(
|
||||||
factory: OriginGestureNavigationFactory,
|
factory: OriginGestureNavigationFactory,
|
||||||
): OriginGestureNavigationBuilder;
|
): OriginGestureNavigationBuilder;
|
||||||
|
/** Run `handler` when a release commits. Handlers accumulate in order. */
|
||||||
|
onCommit(handler: OriginGestureCommitHandler): OriginGestureDirectedBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Builder stage after a custom completion policy has been selected. */
|
/** Builder stage after a custom completion policy has been selected. */
|
||||||
@@ -222,12 +238,16 @@ export interface OriginGestureCompletedBuilder {
|
|||||||
navigate(
|
navigate(
|
||||||
factory: OriginGestureNavigationFactory,
|
factory: OriginGestureNavigationFactory,
|
||||||
): OriginGestureNavigationBuilder;
|
): OriginGestureNavigationBuilder;
|
||||||
|
/** Run `handler` when a release commits. Handlers accumulate in order. */
|
||||||
|
onCommit(handler: OriginGestureCommitHandler): OriginGestureCompletedBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Final builder stage waiting for visual choreography. */
|
/** Final builder stage waiting for visual choreography. */
|
||||||
export interface OriginGestureNavigationBuilder {
|
export interface OriginGestureNavigationBuilder {
|
||||||
/** Attach visual choreography and produce an executable definition. */
|
/** Attach visual choreography and produce an executable definition. */
|
||||||
animate(choreography: OriginChoreography): OriginGestureDefinition;
|
animate(choreography: OriginChoreography): OriginGestureDefinition;
|
||||||
|
/** Run `handler` when a release commits. Handlers accumulate in order. */
|
||||||
|
onCommit(handler: OriginGestureCommitHandler): OriginGestureNavigationBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Root of the immutable fluent gesture-definition API. */
|
/** Root of the immutable fluent gesture-definition API. */
|
||||||
@@ -368,55 +388,86 @@ export interface OriginGestureSurfaceProps {
|
|||||||
// Builder ------------------------------------------------------------------
|
// Builder ------------------------------------------------------------------
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Immutable fluent builder. Each stage is a frozen object closing over the
|
* Immutable fluent builder. Each stage is a frozen object holding the choices
|
||||||
* choices made so far; the final `.animate()` produces a frozen definition.
|
* made so far in a frozen draft; the final `.animate()` produces a frozen
|
||||||
|
* definition. `.onCommit()` is available at every stage after the direction
|
||||||
|
* and returns the same stage with the handler appended.
|
||||||
*
|
*
|
||||||
* gesture[.from.<start>].to.<direction>()[.complete()].navigate().animate()
|
* gesture[.from.<start>].to.<direction>()[.complete()].navigate().animate()
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function navigationStage(
|
/** Choices accumulated by the builder before `.animate()`. */
|
||||||
start: OriginGestureStart,
|
interface GestureDraft {
|
||||||
direction: OriginGestureDirection,
|
readonly start: OriginGestureStart;
|
||||||
recognition: Readonly<OriginGestureDirectionOptions>,
|
readonly direction: OriginGestureDirection;
|
||||||
completion: OriginGestureCompletionPredicate | undefined,
|
readonly recognition: Readonly<OriginGestureDirectionOptions>;
|
||||||
) {
|
readonly completion?: OriginGestureCompletionPredicate;
|
||||||
return (
|
readonly commitHandlers: readonly OriginGestureCommitHandler[];
|
||||||
navigation: OriginGestureNavigationFactory,
|
|
||||||
): OriginGestureNavigationBuilder =>
|
|
||||||
Object.freeze({
|
|
||||||
animate: (choreography: OriginChoreography): OriginGestureDefinition =>
|
|
||||||
Object.freeze({
|
|
||||||
kind: "origin-gesture-definition",
|
|
||||||
start,
|
|
||||||
direction,
|
|
||||||
recognition,
|
|
||||||
completion,
|
|
||||||
navigation,
|
|
||||||
choreography,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function directedStage(
|
const withCommitHandler = <Draft extends GestureDraft>(
|
||||||
start: OriginGestureStart,
|
draft: Draft,
|
||||||
direction: OriginGestureDirection,
|
handler: OriginGestureCommitHandler,
|
||||||
options: OriginGestureDirectionOptions = {},
|
): Draft =>
|
||||||
): OriginGestureDirectedBuilder {
|
Object.freeze({
|
||||||
const recognition = Object.freeze({ ...options });
|
...draft,
|
||||||
|
commitHandlers: Object.freeze([...draft.commitHandlers, handler]),
|
||||||
|
});
|
||||||
|
|
||||||
|
function navigationStage(
|
||||||
|
draft: GestureDraft & { readonly navigation: OriginGestureNavigationFactory },
|
||||||
|
): OriginGestureNavigationBuilder {
|
||||||
|
return Object.freeze({
|
||||||
|
animate: (choreography: OriginChoreography): OriginGestureDefinition =>
|
||||||
|
Object.freeze({
|
||||||
|
kind: "origin-gesture-definition",
|
||||||
|
start: draft.start,
|
||||||
|
direction: draft.direction,
|
||||||
|
recognition: draft.recognition,
|
||||||
|
completion: draft.completion,
|
||||||
|
navigation: draft.navigation,
|
||||||
|
choreography,
|
||||||
|
commitHandlers: draft.commitHandlers,
|
||||||
|
}),
|
||||||
|
onCommit: (handler: OriginGestureCommitHandler) =>
|
||||||
|
navigationStage(withCommitHandler(draft, handler)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigateFrom =
|
||||||
|
(draft: GestureDraft) => (navigation: OriginGestureNavigationFactory) =>
|
||||||
|
navigationStage(Object.freeze({ ...draft, navigation }));
|
||||||
|
|
||||||
|
function completedStage(draft: GestureDraft): OriginGestureCompletedBuilder {
|
||||||
|
return Object.freeze({
|
||||||
|
navigate: navigateFrom(draft),
|
||||||
|
onCommit: (handler: OriginGestureCommitHandler) =>
|
||||||
|
completedStage(withCommitHandler(draft, handler)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function directedStage(draft: GestureDraft): OriginGestureDirectedBuilder {
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
complete: (completion: OriginGestureCompletionPredicate) =>
|
complete: (completion: OriginGestureCompletionPredicate) =>
|
||||||
Object.freeze({
|
completedStage(Object.freeze({ ...draft, completion })),
|
||||||
navigate: navigationStage(start, direction, recognition, completion),
|
navigate: navigateFrom(draft),
|
||||||
}),
|
onCommit: (handler: OriginGestureCommitHandler) =>
|
||||||
navigate: navigationStage(start, direction, recognition, undefined),
|
directedStage(withCommitHandler(draft, handler)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function toStage(start: OriginGestureStart): OriginGestureToBuilder {
|
function toStage(start: OriginGestureStart): OriginGestureToBuilder {
|
||||||
const direct =
|
const direct =
|
||||||
(direction: OriginGestureDirection) =>
|
(direction: OriginGestureDirection) =>
|
||||||
(options?: OriginGestureDirectionOptions) =>
|
(options: OriginGestureDirectionOptions = {}) =>
|
||||||
directedStage(start, direction, options);
|
directedStage(
|
||||||
|
Object.freeze({
|
||||||
|
start,
|
||||||
|
direction,
|
||||||
|
recognition: Object.freeze({ ...options }),
|
||||||
|
commitHandlers: Object.freeze([]),
|
||||||
|
}),
|
||||||
|
);
|
||||||
return Object.freeze({
|
return Object.freeze({
|
||||||
left: direct("left"),
|
left: direct("left"),
|
||||||
right: direct("right"),
|
right: direct("right"),
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import type { OriginContext, OriginOperationHandle } from "../scene/types";
|
|||||||
import { clamp01, measureRect, type OriginRect } from "../shared";
|
import { clamp01, measureRect, type OriginRect } from "../shared";
|
||||||
import type {
|
import type {
|
||||||
OriginGestureBinding,
|
OriginGestureBinding,
|
||||||
|
OriginGestureCommitHandler,
|
||||||
|
OriginGestureCompletionContext,
|
||||||
OriginGestureCompletionPredicate,
|
OriginGestureCompletionPredicate,
|
||||||
OriginGestureDefinition,
|
OriginGestureDefinition,
|
||||||
OriginGestureDirection,
|
OriginGestureDirection,
|
||||||
@@ -290,6 +292,7 @@ export interface RecognizerPolicy {
|
|||||||
readonly threshold: number;
|
readonly threshold: number;
|
||||||
readonly axisDominance: number;
|
readonly axisDominance: number;
|
||||||
readonly completion?: OriginGestureCompletionPredicate;
|
readonly completion?: OriginGestureCompletionPredicate;
|
||||||
|
readonly commitHandlers: readonly OriginGestureCommitHandler[];
|
||||||
readonly action: (context: OriginContext) => MaybeOriginAction;
|
readonly action: (context: OriginContext) => MaybeOriginAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,6 +375,7 @@ export class GestureRecognizer {
|
|||||||
axisDominance:
|
axisDominance:
|
||||||
definition.recognition.axisDominance ?? DEFAULT_AXIS_DOMINANCE,
|
definition.recognition.axisDominance ?? DEFAULT_AXIS_DOMINANCE,
|
||||||
completion: definition.completion,
|
completion: definition.completion,
|
||||||
|
commitHandlers: definition.commitHandlers ?? [],
|
||||||
// Pair the resolved navigation intent with the definition's animation.
|
// Pair the resolved navigation intent with the definition's animation.
|
||||||
action: async (context) => {
|
action: async (context) => {
|
||||||
const navigation = await definition.navigation(context);
|
const navigation = await definition.navigation(context);
|
||||||
@@ -402,6 +406,7 @@ export class GestureRecognizer {
|
|||||||
threshold: options.threshold ?? DEFAULT_THRESHOLD,
|
threshold: options.threshold ?? DEFAULT_THRESHOLD,
|
||||||
axisDominance: DEFAULT_AXIS_DOMINANCE,
|
axisDominance: DEFAULT_AXIS_DOMINANCE,
|
||||||
action: options.action,
|
action: options.action,
|
||||||
|
commitHandlers: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,7 +480,7 @@ export class GestureRecognizer {
|
|||||||
|
|
||||||
const { tracker } = session;
|
const { tracker } = session;
|
||||||
tracker.sample(event, true);
|
tracker.sample(event, true);
|
||||||
const commit = this.policy.completion?.({
|
const context: OriginGestureCompletionContext = {
|
||||||
origin: session.origin,
|
origin: session.origin,
|
||||||
direction: this.policy.direction,
|
direction: this.policy.direction,
|
||||||
progress: tracker.progress,
|
progress: tracker.progress,
|
||||||
@@ -488,13 +493,32 @@ export class GestureRecognizer {
|
|||||||
bounds: session.bounds,
|
bounds: session.bounds,
|
||||||
start: session.start,
|
start: session.start,
|
||||||
current: pointOf(event, session.bounds),
|
current: pointOf(event, session.bounds),
|
||||||
});
|
};
|
||||||
|
const commit = this.policy.completion?.(context);
|
||||||
const { progress, velocity } = tracker;
|
const { progress, velocity } = tracker;
|
||||||
|
|
||||||
const handle = session.operation ? await session.operation : null;
|
const handle = session.operation ? await session.operation : null;
|
||||||
if (!handle) return;
|
if (!handle) return;
|
||||||
handle.update(progress, velocity);
|
handle.update(progress, velocity);
|
||||||
await handle.finish(commit === undefined ? undefined : { commit });
|
// finish() fixes the outcome synchronously, before the spring settles.
|
||||||
|
const settled = handle.finish(
|
||||||
|
commit === undefined ? undefined : { commit },
|
||||||
|
);
|
||||||
|
if (handle.intent === "commit") this.notifyCommit(context);
|
||||||
|
await settled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Run commit handlers; a failing handler never interrupts the others. */
|
||||||
|
private notifyCommit(context: OriginGestureCompletionContext) {
|
||||||
|
for (const handler of this.policy.commitHandlers) {
|
||||||
|
try {
|
||||||
|
handler(context);
|
||||||
|
} catch (error) {
|
||||||
|
queueMicrotask(() => {
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async pointerCancel(): Promise<void> {
|
async pointerCancel(): Promise<void> {
|
||||||
|
|||||||
@@ -175,3 +175,97 @@ describe("haptic feedback", () => {
|
|||||||
expect(vibrate).toHaveBeenCalledTimes(1);
|
expect(vibrate).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe(".onCommit()", () => {
|
||||||
|
it("vibrates once when a release commits, and not when it cancels", async () => {
|
||||||
|
const vibrate = vi.fn((_ms: number) => true);
|
||||||
|
const { host } = await mount(
|
||||||
|
gesture.to
|
||||||
|
.left()
|
||||||
|
.navigate(() => forward(originView(Target)))
|
||||||
|
.onCommit(() => vibrate(10))
|
||||||
|
.animate(motion),
|
||||||
|
);
|
||||||
|
|
||||||
|
await swipe(host, 20, []); // 10% progress: cancelled
|
||||||
|
expect(vibrate).not.toHaveBeenCalled();
|
||||||
|
await swipe(host, 150, []); // 75% progress: committed
|
||||||
|
expect(vibrate).toHaveBeenCalledTimes(1);
|
||||||
|
expect(vibrate).toHaveBeenCalledWith(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is available at every stage after the direction, in order", async () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const definition = gesture.to
|
||||||
|
.left()
|
||||||
|
.onCommit(() => calls.push("directed"))
|
||||||
|
.complete(({ progress }) => progress > 0.5)
|
||||||
|
.onCommit(() => calls.push("completed"))
|
||||||
|
.navigate(() => forward(originView(Target)))
|
||||||
|
.onCommit((context) => calls.push(`navigation ${context.direction}`))
|
||||||
|
.animate(motion);
|
||||||
|
expect(definition.commitHandlers).toHaveLength(3);
|
||||||
|
expect(Object.isFrozen(definition.commitHandlers)).toBe(true);
|
||||||
|
|
||||||
|
const { host } = await mount(definition);
|
||||||
|
await swipe(host, 150, []);
|
||||||
|
expect(calls).toEqual(["directed", "completed", "navigation left"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps earlier builder stages unchanged", () => {
|
||||||
|
const base = gesture.to.left().navigate(() => null);
|
||||||
|
const withHandler = base.onCommit(() => {});
|
||||||
|
expect(base.animate(motion).commitHandlers).toEqual([]);
|
||||||
|
expect(withHandler.animate(motion).commitHandlers).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports a failing handler without stopping later handlers or the commit", async () => {
|
||||||
|
const errors: unknown[] = [];
|
||||||
|
const onError = (error: unknown) => errors.push(error);
|
||||||
|
process.on("uncaughtException", onError);
|
||||||
|
try {
|
||||||
|
const after = vi.fn();
|
||||||
|
const { scene, host } = await mount(
|
||||||
|
gesture.to
|
||||||
|
.left()
|
||||||
|
.navigate(() => forward(originView(Target)))
|
||||||
|
.onCommit(() => {
|
||||||
|
throw new Error("no vibration motor");
|
||||||
|
})
|
||||||
|
.onCommit(after)
|
||||||
|
.animate(motion),
|
||||||
|
);
|
||||||
|
await swipe(host, 150, []);
|
||||||
|
expect(after).toHaveBeenCalledTimes(1);
|
||||||
|
expect(scene.nodes.value.map((node) => node.state)).toEqual([
|
||||||
|
"parked",
|
||||||
|
"active",
|
||||||
|
]);
|
||||||
|
expect(errors).toEqual([new Error("no vibration motor")]);
|
||||||
|
} finally {
|
||||||
|
process.off("uncaughtException", onError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fires at release, before the settling animation finishes", async () => {
|
||||||
|
const frames: FrameRequestCallback[] = [];
|
||||||
|
vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) =>
|
||||||
|
frames.push(cb),
|
||||||
|
);
|
||||||
|
const log: string[] = [];
|
||||||
|
const { scene, host } = await mount(
|
||||||
|
gesture.to
|
||||||
|
.left()
|
||||||
|
.navigate(() => forward(originView(Target)))
|
||||||
|
.onCommit(() => log.push("commit"))
|
||||||
|
.animate(motion),
|
||||||
|
);
|
||||||
|
vi.stubGlobal("matchMedia", () => ({ matches: false }));
|
||||||
|
|
||||||
|
await swipe(host, 150, log);
|
||||||
|
expect(log).toEqual(["recognized", "commit", "released"]);
|
||||||
|
// The spring is still waiting for frames; the operation is settling.
|
||||||
|
expect(scene.operations.value[0]?.phase).toBe("settling");
|
||||||
|
expect(frames.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
normalizedEffect,
|
normalizedEffect,
|
||||||
type OriginEffect,
|
type OriginEffect,
|
||||||
type OriginEffectSet,
|
type OriginEffectSet,
|
||||||
|
type OriginOperationIntent,
|
||||||
} from "../choreography";
|
} from "../choreography";
|
||||||
import type { OriginAction, OriginView } from "../navigation";
|
import type { OriginAction, OriginView } from "../navigation";
|
||||||
import {
|
import {
|
||||||
@@ -225,13 +226,25 @@ export const runSpring = (
|
|||||||
|
|
||||||
/** Imperative controller for one operation, returned by `Scene.begin()`. */
|
/** Imperative controller for one operation, returned by `Scene.begin()`. */
|
||||||
class OperationHandle implements OriginOperationHandle {
|
class OperationHandle implements OriginOperationHandle {
|
||||||
|
readonly id: number;
|
||||||
|
readonly originKey: string;
|
||||||
|
readonly targetKey: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly scene: Scene,
|
private readonly scene: Scene,
|
||||||
readonly id: number,
|
/** Kept after the graph drops it, so `intent` stays readable. */
|
||||||
readonly originKey: string,
|
private readonly operation: MutableOriginOperation,
|
||||||
readonly targetKey: string,
|
/** The target at creation; later edge rewrites do not change it. */
|
||||||
|
targetKey: string,
|
||||||
) {
|
) {
|
||||||
markRaw(this);
|
markRaw(this);
|
||||||
|
this.id = operation.id;
|
||||||
|
this.originKey = operation.originKey;
|
||||||
|
this.targetKey = targetKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
get intent(): OriginOperationIntent {
|
||||||
|
return this.operation.intent;
|
||||||
}
|
}
|
||||||
|
|
||||||
update(progress: number, velocity = 0): void {
|
update(progress: number, velocity = 0): void {
|
||||||
@@ -299,14 +312,14 @@ class Scene implements OriginScene {
|
|||||||
action,
|
action,
|
||||||
this.measure(originKey),
|
this.measure(originKey),
|
||||||
);
|
);
|
||||||
const { id, targetKey } = operation;
|
const { targetKey } = operation;
|
||||||
|
|
||||||
// Let Vue mount the target before measuring it. Gesture recognizers
|
// Let Vue mount the target before measuring it. Gesture recognizers
|
||||||
// buffer pointer progress while this short preparation step is pending.
|
// buffer pointer progress while this short preparation step is pending.
|
||||||
await nextTick();
|
await nextTick();
|
||||||
operation.targetRect = this.measure(targetKey);
|
operation.targetRect = this.measure(targetKey);
|
||||||
operation.phase = "interactive";
|
operation.phase = "interactive";
|
||||||
return new OperationHandle(this, id, originKey, targetKey);
|
return new OperationHandle(this, operation, targetKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
async perform(originKey: string, action: OriginAction): Promise<boolean> {
|
async perform(originKey: string, action: OriginAction): Promise<boolean> {
|
||||||
|
|||||||
@@ -118,6 +118,11 @@ export interface OriginOperationHandle {
|
|||||||
readonly originKey: string;
|
readonly originKey: string;
|
||||||
/** Key of the newly mounted or retained target node. */
|
/** Key of the newly mounted or retained target node. */
|
||||||
readonly targetKey: string;
|
readonly targetKey: string;
|
||||||
|
/**
|
||||||
|
* The operation's selected outcome. It becomes `commit` or `cancel`
|
||||||
|
* synchronously when `finish()` or `cancel()` is called, before settling.
|
||||||
|
*/
|
||||||
|
readonly intent: OriginOperationIntent;
|
||||||
/**
|
/**
|
||||||
* Set interactive progress and optional velocity.
|
* Set interactive progress and optional velocity.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user