import type { CSSProperties } from "vue"; import type { OriginChoreography } from "../choreography"; import type { MaybeOriginAction, OriginNavigationIntent } from "../navigation"; import type { OriginContext } from "../scene/types"; import type { OriginRect } from "../shared"; /* * What a gesture is: the public gesture types, component props, and the * immutable builder that produces executable definitions. Running them is * the recognizer's job. */ /** Direction in which pointer movement advances operation progress. */ export type OriginGestureDirection = "left" | "right" | "up" | "down"; /** Physical side of the gesture host used to admit pointer-down. */ export type OriginGestureEdge = "left" | "right" | "top" | "bottom"; /** * CSS length accepted by an edge-based gesture start rule. * * Numbers are interpreted as CSS pixels. Strings may use normal CSS lengths, * percentages, `calc()`, or `clamp()`, and are resolved against the gesture * host when pointer-down occurs. */ export type OriginGestureDistance = number | string; /** A pointer position measured in viewport and gesture-host coordinates. */ export interface OriginGesturePoint { /** Viewport-relative horizontal position. */ readonly clientX: number; /** Viewport-relative vertical position. */ readonly clientY: number; /** Horizontal position relative to the gesture host's left edge. */ readonly localX: number; /** Vertical position relative to the gesture host's top edge. */ readonly localY: number; } /** Context supplied to a custom `.from.when()` start predicate. */ export interface OriginGestureStartContext { /** Native pointer-down event being considered. */ readonly event: PointerEvent; /** Component instance from which the gesture would originate. */ readonly origin: OriginContext; /** Element carrying the gesture's pointer handlers. */ readonly host: HTMLElement; /** Gesture-host bounds captured at pointer-down. */ readonly bounds: OriginRect; /** Pointer position at pointer-down. */ readonly point: OriginGesturePoint; } /** * Synchronous predicate deciding whether a pointer-down may become a gesture. * * Directional recognition still occurs later, after movement passes the * configured intent threshold. */ export type OriginGestureStartPredicate = ( context: OriginGestureStartContext, ) => boolean; /** Start policy stored in a completed gesture definition. */ export type OriginGestureStart = | { /** Recognize pointer-down anywhere on the host. */ readonly kind: "anywhere"; } | { /** Recognize pointer-down within a CSS distance of one host side. */ readonly kind: "edge"; readonly edge: OriginGestureEdge; readonly distance: OriginGestureDistance; } | { /** Recognize pointer-down when application policy returns `true`. */ readonly kind: "when"; readonly predicate: OriginGestureStartPredicate; }; /** Direction-recognition tuning accepted by `.to.left()` and its siblings. */ export interface OriginGestureDirectionOptions { /** * Minimum directed movement in CSS pixels before the gesture captures. * * @defaultValue `8` */ readonly threshold?: number; /** * Ratio by which directed movement must exceed cross-axis movement. * * @defaultValue `1.15` */ readonly axisDominance?: number; } /** Values available when a custom `.complete()` policy runs on pointer-up. */ export interface OriginGestureCompletionContext { /** Component instance that originated the gesture. */ readonly origin: OriginContext; /** Recognized movement direction. */ readonly direction: OriginGestureDirection; /** Normalized directed distance, clamped to `0..1`. */ readonly progress: number; /** Latest normalized progress units per second. */ readonly velocity: number; /** Directed movement from pointer-down in CSS pixels. */ readonly distance: number; /** Absolute cross-axis movement from pointer-down in CSS pixels. */ readonly crossDistance: number; /** Elapsed time since pointer-down in milliseconds. */ readonly duration: number; /** Native pointer-up event that ended the interaction. */ readonly event: PointerEvent; /** Element carrying the gesture's pointer handlers. */ readonly host: HTMLElement; /** Gesture-host bounds captured at pointer-down. */ readonly bounds: OriginRect; /** Pointer position captured at pointer-down. */ readonly start: OriginGesturePoint; /** Pointer position at release. */ readonly current: OriginGesturePoint; } /** Synchronous commit/cancel policy installed by `.complete()`. */ export type OriginGestureCompletionPredicate = ( context: OriginGestureCompletionContext, ) => boolean; /** * Values accepted from a gesture builder's `.navigate()` factory. * * A promise supports lazy view selection. Stale resolutions are discarded if * the pointer has already ended or been cancelled. */ export type MaybeOriginNavigationIntent = | OriginNavigationIntent | null | undefined | Promise; /** Factory that resolves navigation after directional recognition succeeds. */ export type OriginGestureNavigationFactory = ( context: OriginContext, ) => MaybeOriginNavigationIntent; /** * Immutable, executable result of a complete gesture builder chain. * * Pass this object to {@link useOriginGesture}. An omitted `.from` step is * represented as an `anywhere` start rule. */ export interface OriginGestureDefinition { /** Discriminator used by the compatibility recognizer overload. */ readonly kind: "origin-gesture-definition"; /** Pointer-down eligibility policy. */ readonly start: OriginGestureStart; /** Direction in which movement advances operation progress. */ readonly direction: OriginGestureDirection; /** Directional intent recognition tuning. */ readonly recognition: Readonly; /** * Optional release decision. When omitted, choreography thresholds decide. */ readonly completion?: OriginGestureCompletionPredicate; /** Node-local destination/history resolver. */ readonly navigation: OriginGestureNavigationFactory; /** Visual routine paired with the navigation intent. */ 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`. */ export interface OriginGestureToBuilder { /** Recognize leftward pointer movement. */ left(options?: OriginGestureDirectionOptions): OriginGestureDirectedBuilder; /** Recognize rightward pointer movement. */ right(options?: OriginGestureDirectionOptions): OriginGestureDirectedBuilder; /** Recognize upward pointer movement. */ up(options?: OriginGestureDirectionOptions): OriginGestureDirectedBuilder; /** Recognize downward pointer movement. */ down(options?: OriginGestureDirectionOptions): OriginGestureDirectedBuilder; } /** Stage produced after a `.from` policy has been selected. */ export interface OriginGestureFromSelection { /** Select the direction that advances this gesture. */ readonly to: OriginGestureToBuilder; } /** Optional pointer-down policy exposed at the start of a gesture chain. */ export interface OriginGestureFromBuilder { /** Admit pointer-down within `distance` of the host's left edge. */ left(distance: OriginGestureDistance): OriginGestureFromSelection; /** Admit pointer-down within `distance` of the host's right edge. */ right(distance: OriginGestureDistance): OriginGestureFromSelection; /** Admit pointer-down within `distance` of the host's top edge. */ top(distance: OriginGestureDistance): OriginGestureFromSelection; /** Admit pointer-down within `distance` of the host's bottom edge. */ bottom(distance: OriginGestureDistance): OriginGestureFromSelection; /** Admit pointer-down anywhere on the gesture host. */ anywhere(): OriginGestureFromSelection; /** Admit pointer-down when a synchronous application predicate succeeds. */ when(predicate: OriginGestureStartPredicate): OriginGestureFromSelection; } /** Builder stage after direction is known and completion remains optional. */ export interface OriginGestureDirectedBuilder { /** Override the choreography's default release decision. */ complete( predicate: OriginGestureCompletionPredicate, ): OriginGestureCompletedBuilder; /** Select the target/history mutation while retaining default completion. */ navigate( factory: OriginGestureNavigationFactory, ): 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. */ export interface OriginGestureCompletedBuilder { /** Select the target/history mutation performed on commit. */ navigate( factory: OriginGestureNavigationFactory, ): OriginGestureNavigationBuilder; /** Run `handler` when a release commits. Handlers accumulate in order. */ onCommit(handler: OriginGestureCommitHandler): OriginGestureCompletedBuilder; } /** Final builder stage waiting for visual choreography. */ export interface OriginGestureNavigationBuilder { /** Attach visual choreography and produce an executable definition. */ 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. */ export interface OriginGestureBuilder { /** Optionally constrain where pointer-down may begin. */ readonly from: OriginGestureFromBuilder; /** * Select movement direction with an implicit `from.anywhere()` start. */ readonly to: OriginGestureToBuilder; } /** Configuration consumed by {@link useOriginGesture}. */ export interface OriginGestureOptions { /** * Direction in which the pointer moves to advance the operation. * * The starting edge is the opposite side: `right` begins at the left edge, * `left` at the right edge, `down` at the top, and `up` at the bottom. */ direction: OriginGestureDirection; /** * Restrict pointer-down to this many CSS pixels from the gesture host's * starting edge. Omit it to recognize across the entire host. * * This is relative to the bound element, not necessarily the browser * viewport. Use a positive number such as `24` or `36`. */ edge?: number; /** * Minimum directed movement in CSS pixels before the gesture captures. * * Movement must also dominate the cross-axis by a factor of `1.15`. * * @defaultValue `8` */ threshold?: number; /** * Resolve the scene action after directional recognition succeeds. * * Returning no action abandons recognition without modifying the scene. */ action(context: OriginContext): MaybeOriginAction; } /** * DOM bindings returned by {@link useOriginGesture}. * * Spread or attach all four handlers to the same `HTMLElement`. Apply * {@link style} as well so native scrolling is preserved on the cross-axis. */ export interface OriginGestureBinding { /** Required size and `touch-action` styles for the gesture host. */ readonly style: Readonly; /** Pointer-down handler that records a potentially eligible gesture. */ readonly onPointerdown: (event: PointerEvent) => void; /** Pointer-move handler that recognizes and updates the operation. */ readonly onPointermove: (event: PointerEvent) => void; /** Pointer-up handler that commits or cancels using progress and velocity. */ readonly onPointerup: (event: PointerEvent) => void; /** Pointer-cancel handler that abandons any captured operation. */ readonly onPointercancel: () => void; } // Component props ---------------------------------------------------------- /** Props shared by both `OriginGesture` declaration styles. */ export interface OriginGestureBaseProps { /** * Native HTML tag rendered as the gesture host. * * @defaultValue `"div"` */ as?: string; } /** Builder-definition props accepted by the `OriginGesture` component. */ export interface OriginGestureDefinitionProps extends OriginGestureBaseProps { /** Immutable definition produced by the {@link gesture} builder. */ gesture: OriginGestureDefinition; /** Builder definitions already contain direction. */ direction?: never; /** Builder definitions already contain their start policy. */ edge?: never; /** Builder definitions already contain recognition tuning. */ threshold?: never; /** Builder definitions already contain their navigation factory. */ action?: never; } /** Legacy option props accepted by the `OriginGesture` component. */ export interface OriginGestureLegacyProps extends OriginGestureBaseProps { /** Legacy component declarations do not provide a builder definition. */ gesture?: never; /** Direction in which pointer movement advances operation progress. */ direction: OriginGestureDirection; /** * Eligible pointer-down width in CSS pixels from the host's starting edge. * Omit it to allow the full component surface. */ edge?: number; /** * Directed movement required before capture. * * @defaultValue `8` */ threshold?: number; /** Node-local action factory invoked only after recognition succeeds. */ action(context: OriginContext): MaybeOriginAction; } /** * Public props accepted by the `OriginGesture` convenience component. * * Prefer the builder-definition form. The legacy direction/action form remains * available for compatibility. */ export type OriginGestureProps = OriginGestureDefinitionProps | OriginGestureLegacyProps; /** Public props accepted by the multi-definition gesture surface component. */ export interface OriginGestureSurfaceProps { /** * Native HTML tag rendered as the shared gesture host. * * @defaultValue `"div"` */ as?: string; /** * Complete immutable definitions installed on the shared host. * * The surface adds no start, direction, completion, navigation, or animation * policy. Definitions should be created by the owning page component. */ gestures: readonly OriginGestureDefinition[]; } // Builder ------------------------------------------------------------------ /* * Immutable fluent builder. Each stage is a frozen object holding the choices * 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.].to.()[.complete()].navigate().animate() */ /** Choices accumulated by the builder before `.animate()`. */ interface GestureDraft { readonly start: OriginGestureStart; readonly direction: OriginGestureDirection; readonly recognition: Readonly; readonly completion?: OriginGestureCompletionPredicate; readonly commitHandlers: readonly OriginGestureCommitHandler[]; } const withCommitHandler = ( draft: Draft, handler: OriginGestureCommitHandler, ): Draft => Object.freeze({ ...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({ complete: (completion: OriginGestureCompletionPredicate) => completedStage(Object.freeze({ ...draft, completion })), navigate: navigateFrom(draft), onCommit: (handler: OriginGestureCommitHandler) => directedStage(withCommitHandler(draft, handler)), }); } function toStage(start: OriginGestureStart): OriginGestureToBuilder { const direct = (direction: OriginGestureDirection) => (options: OriginGestureDirectionOptions = {}) => directedStage( Object.freeze({ start, direction, recognition: Object.freeze({ ...options }), commitHandlers: Object.freeze([]), }), ); return Object.freeze({ left: direct("left"), right: direct("right"), up: direct("up"), down: direct("down"), }); } function fromStage(start: OriginGestureStart): OriginGestureFromSelection { return Object.freeze({ to: toStage(Object.freeze(start)) }); } const fromEdge = (edge: OriginGestureEdge) => (distance: OriginGestureDistance) => fromStage({ kind: "edge", edge, distance }); const fromBuilder: OriginGestureFromBuilder = Object.freeze({ left: fromEdge("left"), right: fromEdge("right"), top: fromEdge("top"), bottom: fromEdge("bottom"), anywhere: () => fromStage({ kind: "anywhere" }), when: (predicate: OriginGestureStartPredicate) => fromStage({ kind: "when", predicate }), }); /** * Root of the immutable gesture builder. * * `.from` is optional. Starting at `.to` admits pointer-down anywhere on the * bound host, exactly like `.from.anywhere().to`. * * @example Anywhere-to-right back gesture * ```ts * const swipeBack = gesture * .to.right() * .navigate((context) => context.canGoBack ? back() : null) * .animate(slideRight); * ``` * * @example Predicate-gated gesture with custom completion * ```ts * const openPanel = gesture * .from.when(({ point, bounds }) => point.localX <= bounds.width * 0.08) * .to.down() * .complete(({ progress, velocity }) => progress > 0.5 || velocity > 1) * .navigate(() => above(originView(PanelView))) * .animate(dropPanel); * ``` */ export const gesture: OriginGestureBuilder = Object.freeze({ from: fromBuilder, to: toStage(Object.freeze({ kind: "anywhere" })), });