import type { ConnectionId, BlitSurface } from "./types"; import type { BlitWorkspace } from "./BlitWorkspace"; import type { SurfaceTextInputEvent } from "./SurfaceStore"; export { av1LevelString } from "./videoCodec"; /** * Probe which video codecs the browser can decode via WebCodecs and return * a bitmask of CODEC_SUPPORT_* flags. Result is cached after first call. * * Basic codec support (H.264, AV1) is checked via isConfigSupported(). * 4:4:4 chroma variants are verified by actually decoding a small test * frame, since isConfigSupported() is unreliable for subsampling modes. */ export declare function detectCodecSupport(): Promise; /** Return the cached codec support, or 0 if not yet probed. */ export declare function getCodecSupport(): number; /** * Narrow which codecs this viewer will accept for surface video, on top of * what the probe found. Pass `0xff` (or `0`) to drop the restriction. * * Returns the new wire mask, or `null` when the wire mask is unchanged — * because the preference is the same, because the probe has not answered * yet (its own `sendClientFeatures` will carry the new setting), or because * every allowed codec was already the only thing on offer. */ export declare function setAllowedCodecSupport(mask: number): number | null; /** The allow-list currently in force. */ export declare function getAllowedCodecSupport(): number; /** Codecs the probe confirmed, ignoring demotions and the allow-list — what * the media panel may offer as selectable. */ export declare function getProbedCodecSupport(): number; /** * Drop codec-support bits after the stream they selected proved * undecodable in practice — the probe's tiny test frames pass on decoders * that then reject the real stream. Returns the new mask, or null when * nothing changed: the probe hasn't finished (nothing to demote), the bits * were already clear, or clearing them would zero the mask — which the * wire protocol reads as "accept anything" and would undo the demotion. */ export declare function demoteCodecSupport(bits: number): number | null; /** * Re-offer bits a previous {@link demoteCodecSupport} withdrew, once the * failures that triggered it are far enough behind to have been a transient * fault (a GPU reset, a decoder the browser had briefly wedged) rather than * a codec this platform cannot handle. Returns the new mask, or null when * nothing changed — the probe never confirmed those bits, or they are * already offered. */ export declare function restoreCodecSupport(bits: number): number | null; /** * Largest frame the probe confirmed this browser can decode, as [w, h]. * `[0, 0]` before probing, or when nothing above 1080p was confirmed. */ export declare function getMaxDecodeSize(): [number, number]; /** Bubbling DOM event emitted by a mounted surface when its Wayland client * commits text-input state. The app shell uses fresh `requested` events to * raise a mobile virtual keyboard; embedders can provide their own policy. */ export declare const BLIT_SURFACE_TEXT_INPUT_EVENT = "blit-surface-text-input"; export type BlitSurfaceTextInputEvent = CustomEvent; /** Resolve the live Wayland surface view owning a hidden IME textarea. */ export declare function surfaceCanvasForInput(input: Element | null): BlitSurfaceCanvas | null; export interface BlitSurfaceCanvasOptions { workspace: BlitWorkspace; connectionId: ConnectionId; surfaceId: number; /** * Whether this mount is expected to drive the surface size. This lets an * already-laid-out passive view put its scaled target on the very first * subscribe, while preserving the eager unscaled subscribe for a pane * whose framework binding is about to call `setDisplaySize`. */ resizable?: boolean; /** * Whether this mount owns a server-side video subscription. Cached-only * mounts still paint frames produced by another live view from the shared * SurfaceStore, but never create an encoder themselves. Defaults to true. */ live?: boolean; /** `direct` (the default) forwards every touchscreen contact to the Wayland * client's `wl_touch`. `pointer` opts into Blit's single-finger * click/scroll emulation. */ touchMode?: SurfaceTouchMode; } export type SurfaceTouchMode = "pointer" | "direct"; /** * Framework-agnostic surface canvas. Manages a `` element that renders * decoded video frames from a Wayland-like surface, and forwards * pointer / keyboard / wheel input back to the server. * * Framework bindings (React, Solid, etc.) attach this to a container element * and forward option changes via setters. */ export declare class BlitSurfaceCanvas { /** Live previews do not need to drive applications at monitor refresh. */ private static readonly THUMBNAIL_MAX_FPS; private _workspace; private _connectionId; private _surfaceId; private _live; private _expectsDisplaySize; private _touchMode; private touchCapabilityAcquired; /** * A passive view with a working ResizeObserver must learn its box before * it opens a stream. A newly-created sidebar card is mounted before its * first layout and therefore measures 0x0 synchronously; subscribing at * that point asks for native pixels, only to replace the request with an * octave-rounded thumbnail target in the observer callback. */ private _waitForPresentBox; private container; private canvas; private ctx; /** Pointer overlay for the client currently driving this shared surface. * The originating client is told to hide it and keeps its native cursor. */ private remotePointerSvg; private remotePointerGlyph; private remotePointerImage; private remoteInput; /** Reused rings for mirrored touch contacts. */ private remoteContacts; private remoteCursor; private surface; private disposed; /** Track which mouse buttons are currently pressed so we can send synthetic * pointer-up events on dispose — preventing a dangling compositor grab. */ private pressedButtons; /** Track which keyboard keys are currently pressed (evdev keycodes) so we * can release them when focus leaves or the canvas is disposed — preventing * stuck modifiers and runaway key-repeat in the compositor. */ private pressedKeys; /** One-shot modifiers armed by the mobile extra-keys row. */ private _ctrlModifier; private _ctrlModifierListeners; private _altModifier; private _altModifierListeners; /** Alt presses held back pending dead-key detection (evdev keycodes). * A macOS Option keydown may turn out to be the start of a dead-key * composition (Option+E → é), in which case the Alt press must never * reach the app: Electron apps (Slack) react to a bare Alt press by * activating their menu bar, which then swallows the composed text. */ private pendingAlt; /** Alt presses that a dead-key composition consumed: never forwarded, * so their physical key-up must be ignored too. */ private swallowedAlt; /** Whether the browser's Alt key doubles as the macOS Option character * modifier. Only then is the Alt press held back (pendingAlt above); * on other platforms it is forwarded immediately, keeping Alt-tap and * Alt-hold semantics for apps that react to them. */ private macOptionChars; /** True from compositionstart through compositionend. Some engines report * `KeyboardEvent.isComposing=false` on the keystroke that completes a * dead-key composition; the explicit lifecycle keeps that key on the IME * path instead of forwarding it as ordinary input and cancelling commit. */ private compositionActive; /** Active single-finger gesture used to emulate mouse input on iPadOS. */ private activeTouch; /** Browser contact identifiers currently held by direct-touch mode. */ private directTouchIds; /** * When non-null the surface is in resizable mode: the framework binding's * ResizeObserver calls setDisplaySize with the container's physical pixel * size and a server-side resize is requested. The canvas backing buffer * always mirrors the decoded frame; applyLayout() sizes the CSS box so * one canvas pixel is one device pixel — never upscaled — and centers it * in the container. Keeping the canvas at the frame's native size avoids * a blurry "jump" mid-drag where an old, smaller frame would get * drawImage-upscaled into a prematurely enlarged canvas before the new * keyframe arrives. */ private _displaySize; /** * The container's size in device pixels, tracked for every view. * * A resizable view gets its size through setDisplaySize and sits at 1:1, so * this is only consulted for the views that never learn a size — dock * thumbnails and the React binding — which otherwise hand a full-resolution * frame to a card-sized box and get a point-sampled minification back. * Presentation only: it is never sent to the server, so a thumbnail cannot * shrink the surface for the co-viewers watching it full size. */ private _presentBox; private _presentObserver; /** Whether this mount intersects the document viewport. Hidden BSP * leaves and inactive tabs can remain mounted; keeping their server * subscription alive would make the compositor render and encode a * stream nobody can see. */ private _isIntersecting; private _intersectionObserver; /** This view's surface-subscription token. Allocated lazily and kept * across resubscribes so the connection tracks one entry per view. */ private _surfaceViewId; /** Halvings applied by the last blit, so the observer can tell a resize that * crosses an octave from one that changes nothing on screen. */ private _presentHalvings; /** Source frame size of the last blit, so the observer can recompute the * reduction without going back to the store. */ private _lastFrameSize; /** Reused result for presentationBox(); callers consume it synchronously. */ private readonly _presentationBox; /** Last layout applied by applyLayout(), to skip redundant style writes. */ private _lastLayout; /** {@link layoutEpoch} the IME capture element was last placed against, or * -1 to force the next {@link syncImeTarget} to measure. */ private _imeSyncedEpoch; /** Whether the pointer overlay already carries the fill-the-box style the * no-display-size branch of {@link layoutCanvasBox} writes. */ private _overlayFilled; /** Whether this mount holds a reference on the shared {@link layoutEpoch} * listeners. */ private _layoutEpochHeld; /** True after this view has sent a nonzero surface resize that must be * cleared when the view stops owning foreground/BSP sizing. */ private _resizeConstraintActive; private unsubFrame; private unsubCursor; private unsubRemotePointer; private unsubTextInput; private unsubChange; /** True after the first frame has been blitted. Kept as a tripwire so * resubscribe paths can restart the first-frame fast path. */ private _hasBlitFirstFrame; /** Cached store reference so we can keep the frame listener alive * even when the connection is temporarily unavailable. */ private _store; private _retryUnsub; /** The SurfaceStore generation at the time we last sent a subscribe. * Used to detect reconnects (generation bumps on disconnect) so we * re-subscribe even when the surfaceId hasn't changed. */ private _subscribedGeneration; /** The exact subscription this canvas owns. Kept separate from current * props so prop changes can unsubscribe the old surface correctly. */ private _subscribedSurface; /** Hidden textarea used as the editable keyboard and IME target. */ private textInput; /** Where the app says it is drawing the text under edit, in surface * pixels, from `zwp_text_input_v3.set_cursor_rectangle`. The capture * textarea is parked over it so the host IME's candidate window opens at * the app's own caret. */ private textInputCursorRect; /** Keep the iOS capture field non-empty so a held soft-keyboard Backspace * continues producing deleteContentBackward events. */ private _iosInputPad; /** Refill the iOS pad only after a repeat burst goes idle; changing the * textarea value during the burst can stop WebKit's native repeat. */ private _iosInputRepadTimer; /** Non-zero when a Meta→Ctrl translation is in flight (stores the Meta * evdev keycode that was swapped so the release can be translated back). */ private _metaToCtrl; /** The non-modifier key that Meta→Ctrl translated alongside (e.g. V for * Cmd+V). Used to keep Ctrl held on the Wayland side until this key * is released, so releasing Cmd early doesn't leave a bare V press * that the app interprets as plain 'v' via client-side keyrepeat. */ private _metaToCtrlKey; /** Ctrl release is waiting for the paste-chord key to be released. */ private _ctrlReleaseDeferred; /** In-flight Ctrl+V/Cmd+V state. We defer the V press until the * clipboard read completes — readText resolve/reject, clipboard.read, * or the paste event — so the Wayland app sees `selection` before * `key`, and defer the V release and Ctrl release that may fire * physically during that window, otherwise V arrives at the compositor * with Ctrl already released and the app types 'v' repeatedly. */ private _pendingPaste; private _pendingPasteFlush; /** Stand the in-flight chord down without pressing V, releasing anything * the deferral held back. Runs when the clipboard is known to hold * nothing pastable, when an image we declined is all it held, or when * focus leaves mid-chord. No timer: every clipboard read is a promise * that settles, and the paste event is dispatched with the keydown, so * the chord's outcome is always decided by an event, never guessed. */ private _pendingPasteAbandon; private scrollAccum; private scrollFlushHandle; private scrollStopTimer; /** `axis_source` of the in-flight sequence, null between sequences. * Latched by {@link latchScrollSource} so a momentum tail cannot be * reclassified as a wheel mid-gesture. */ private scrollSource; /** Whether a stop still owes the client. */ private scrollSequenceOpen; /** Reasons this pane has already reported for swallowing a wheel event. * See {@link reportWheelIgnored}. */ private wheelIgnoredReported; private boundMouseDown; private boundMouseUp; private boundMouseMove; private boundWindowMouseUp; private boundWindowMouseMove; private boundWheel; private boundTouchStart; private boundTouchMove; private boundTouchEnd; private boundTouchCancel; private boundPointerDown; private boundPointerMove; private boundPointerUp; private boundPointerCancel; private boundMouseLeave; private boundKeyDown; private boundKeyUp; private boundFocus; private boundBlur; private boundContextMenu; private boundTextInput; private boundCompositionStart; private boundCompositionEnd; private boundPaste; private boundDocumentPaste; private boundWindowBlur; private boundBrowserClipboardChange; private boundDragEnter; private boundDragOver; private boundDragLeave; private boundDrop; private boundDragEnd; /** True between a sent DRAG_ENTER and its LEAVE / DROP / CANCEL — the * compositor has a live wl_data_device drag session we are driving. */ private dragActive; /** The active ENTER was a file drag. Keep this independently of the * current event's DataTransfer: WebKit may expose Files at ENTER, an * empty protected store at DRAGOVER, and concrete files only at DROP. */ private dragFilesActive; /** Last valid surface coordinates seen during this drag. iPad WebKit can * retarget DROP to the document and report its client position as 0,0. */ private dragLastPoint; /** Staging names announced by the most recent file ENTER, in item order. * These names are authoritative at DROP: `DataTransferItem.type` is * visible during hover, while `File.type` is read after release and can * differ (notably for file promises). */ private dragPlannedNames; /** Per-canvas staging sync for file drops (FS_SYNC_STAGING), opened * lazily on the first file drop and reused across drops: the staging * dir is per-connection and lives until the connection closes, so one * sync serves every drop on this canvas. Stopped on dispose. */ private dragStaging; /** An in-flight open of `dragStaging` — concurrent drops share it. */ private dragStagingOpening; constructor(options: BlitSurfaceCanvasOptions); get surfaceInfo(): BlitSurface | undefined; get canvasElement(): HTMLCanvasElement | null; setCtrlModifier(active: boolean): void; get ctrlModifier(): boolean; onCtrlModifierChange(listener: (active: boolean) => void): () => void; setAltModifier(active: boolean): void; get altModifier(): boolean; onAltModifierChange(listener: (active: boolean) => void): () => void; attach(container: HTMLElement): void; /** * Watch the container so blitFromStore knows how far the browser is about * to shrink the canvas. See {@link _presentBox}. */ private observePresentBox; /** Drop this view's server subscription while its mount is off-screen. * * Store listeners stay attached so metadata/cursor state remains current, * and the last decoded frame stays on the canvas. On re-entry we reclaim * the same view token and immediately paint the store's newest frame. */ private observeIntersection; dispose(): void; setConnectionId(connectionId: ConnectionId): void; setSurfaceId(surfaceId: number): void; /** Toggle ownership of the server-side stream without dropping the shared * store listeners that keep a cached preview current. */ setLive(live: boolean): void; setTouchMode(mode: SurfaceTouchMode): void; get touchMode(): SurfaceTouchMode; /** * Request the server to resize the surface to the given pixel dimensions. * The server will respond with a SURFACE_RESIZED message that updates the * surface metadata and canvas size via the normal onChange path. */ requestResize(width: number, height: number, scale120?: number): void; private _pendingResize; private flushPendingResize; private clearResizeConstraint; /** * Set the display (canvas backing-buffer) size in physical pixels. * When set, the canvas resolution is pinned to these dimensions and frames * are drawn scaled to fill rather than the canvas being resized to match * each incoming frame. Call with `null` to revert to frame-tracking mode. * * This should be called by the framework binding's ResizeObserver so the * canvas is immediately at the correct resolution — no CSS scaling needed. * * `scale120` is the scale the *surface* is asked to render at, in 1/120ths * (Wayland convention): the app is handed `width * 120 / scale120` logical * pixels. `cssScale120` is the container's device-pixel ratio and defaults * to `scale120`; a binding applying relative zoom or an exact scale passes * the two separately, since the control moves the surface scale only. */ setDisplaySize(width: number | null, height?: number, scale120?: number, cssScale120?: number): void; /** * Size and position the canvas's CSS box for the current frame. * * The box comes from the view's own display size, not from the frame: * the stream is only ever an approximation of what was asked for — the * server mediates across subscribed clients, rounds to the even 4:2:0 * grid, and may serve a downscale of the surface — and a box derived * from it would move by a pixel or two every time any of those changed, * with the picture never quite reaching the edge of its pane. The frame * is instead fitted to the box, aspect-preserved, so a genuinely * different aspect ratio still letterboxes and nothing shifts when the * stream size does. * * Non-resizable views (thumbnails, the React binding) keep the * fill-and-contain CSS from attach() and let the box drive the size. They * do track the container (see {@link _presentBox}) but only to pick a * halving chain in blitFromStore, never to place the canvas. */ /** * The box, in this view's device pixels, the surface may be drawn into: * the pane, but never larger than the surface's own logical size at this * view's requested scale. Null for views that don't size their own box. * * The server mediates one surface across all its viewers at the * *highest* scale any of them asked for (see `mediated_size_for_surface`), * so a small 3x pane and a large 1x pane settle on a small window * composited at 3x. Filling the 1x pane with that frame would show the * window at 3x zoom — the same window drawn three times too big on the * client that never asked for a high-DPI anything. Capping at * `logical × own scale` draws it at the requested size and lets the * rest of the pane letterbox. * * The cap only bites when it clears the pane by more than rounding * noise. The viewer that *is* sizing the surface gets a cap within a * pixel or two of its own pane — mediation rounds the logical size onto * the even 4:2:0 grid — and snapping those back to the pane keeps its * stream landing on the pane exactly rather than a hairline inside it. */ private presentationBox; private applyLayout; private layoutCanvasBox; /** * Re-queue the current display size as a pending resize so it is sent to * the server for the (possibly new) surface. Analogous to how * {@link BlitTerminalSurface} re-sends dimensions in * `setupResizeObserver()` after a session change — the ResizeObserver * only fires when the container's pixel dimensions change, but after a * surfaceId/connectionId swap the server needs to learn the size for the * new surface even if the container stayed the same size. */ private resendDisplaySize; private getConn; private subscribe; /** Register this visible mount with the connection exactly once. */ private serverSubscribe; private unsubscribeAll; private updateRemotePointerOverlay; /** * Grow/shrink the contact-ring pool to `count` and return it. * * Elements are reused rather than recreated: this runs at the remote user's * touch-move rate, and churning DOM nodes per frame is exactly the cost the * store's dedup exists to avoid. */ private layoutRemoteContacts; /** Copy the shared backing canvas onto our visible canvas. */ private blitFromStore; private resubscribe; private serverUnsubscribe; /** This view's subscription token, allocated on first use and kept for * the life of the canvas so a resubscribe reclaims the same slot. */ private surfaceViewId; /** * The fixed encode size to ask the server for, or null to watch the * surface at its mediated size. * * Only a view that is handed a box asks for one: a resizable view already * drives the surface's size through setDisplaySize, and asking it to * bypass mediation would leave nobody sizing the surface at all. * * The request is this view's own box, octave-rounded — deliberately not * anything derived from the surface's current size. A resubscribe costs * the server an encoder rebuild and this client a keyframe, and the * surface's size moves whenever any *other* viewer resizes its pane; a * request that tracked it would re-ask every time somebody else dragged * a split. The box only moves when this card does. * * Overshooting to the next octave is the cheap side of that trade: the * server inscribes the surface's aspect inside whatever box it is given * and never upscales past native, and the ≤2:1 residual is exactly what * {@link drawHalved} and a single CSS tap already handle. */ private scaledTarget; /** Re-derive the scaled request after the box or the display size * changed. * * Nothing to re-derive before the box has been measured — the request is * the box — or once disposed: `dispose()` clears the display size on its * way to unsubscribing, and re-deriving there would put a subscribe on * the wire, costing the server an encoder rebuild, immediately before * the unsubscribe that makes it moot. */ private refreshScaledTarget; private attachEvents; private detachEvents; private handleMouse; /** Find the mounted surface canvas under a window mouse event. Use the * full hit-test stack so pane chrome layered above a canvas does not turn * a cross-surface drag into a gap. */ private mouseGrabTarget; /** Route held-mouse move/release at window capture phase. This is the * browser half of Wayland's implicit DnD grab: it remains alive through * pane gaps and switches surface ids as the pointer crosses canvases. */ private handleWindowMouseGrab; /** Focus where keystrokes should land: the editable textarea, so an input * method has something to attach to. The canvas routes the same key * handlers, so it stands in only while the textarea does not exist. */ private focusKeyboardTarget; /** * Park the hidden capture textarea over the app's own caret, so the host * IME's candidate window opens where the text is going instead of in the * corner of the screen. * * Only the focused view is worth placing — no other one hosts a * composition — and everything else goes back to the corner, where a * software keyboard can never cover it. */ /** * Park the IME capture element on the caret. * * Called from {@link applyLayout}, i.e. once per presented frame, so the * measuring path is gated on something plausibly having moved since the last * time it ran: this view's own caret rectangle or box (both of which * invalidate {@link _imeSyncedEpoch} directly) or the shared * {@link layoutEpoch}. Guest apps that report a caret at all report it on * every caret move (GTK/Qt) or throughout a composition (Chromium), so the * placement stays fresh exactly when the candidate window is on screen. */ private syncImeTarget; private applyTextInputState; /** `geometry` lets a caller that has already measured this frame pass its * reading in. `drawnGeometry` calls `getBoundingClientRect`, and the wheel * path used to take two of those per event: one for its own scaling and one * in here, either side of a style write. */ private sendPointerAt; /** * A surface position ready for the wire. * * Every one of these coordinates is encoded into an unsigned 16-bit field, so * a position outside the drawn frame — the letterbox margin of an * `object-fit: contain` canvas, or a fractional `rect.left` against an integer * `clientX` — would be sent as its two's-complement wrap. The server reads * ~65535, and since it now mirrors these positions to other viewers, their * overlay clamps the bogus value to the opposite edge. Use this for anything * that reaches `C2S_SURFACE_POINTER` or `C2S_SURFACE_DRAG_*`. */ private surfaceWirePoint; /** Retire this view's shared-pointer overlay on its peers. */ private sendPointerLeave; /** * Where the frame is actually drawn, in CSS pixels, plus the scale that * takes CSS pixels to surface coordinates. * * In resizable views applyLayout() gives the CSS box the frame's own * aspect, so the letterbox degenerates to dx = dy ≈ 0; views still on * the fill-and-contain default (thumbnails) letterbox the intrinsic * aspect within the box via object-fit: contain. * * Pointer positions and scroll distances both go through this, so a * wheel and a drag move content by the same amount on a letterboxed or * downscaled surface. */ private drawnGeometry; private surfacePointFromClient; /** Send synthetic pointer-up for any buttons still held. Prevents the * compositor's implicit pointer grab from outliving this canvas. */ private releaseAllButtons; private clearActiveTouch; private directTouchActive; private syncTouchCapability; private releaseTouchCapability; private cancelPointerTouchGesture; private cancelDirectTouches; private directTouchPoints; private sendDirectTouch; private findActiveTouch; private startTouchGesture; private moveTouchGesture; private endTouchGesture; private handlePointerDown; private handlePointerMove; private handlePointerUp; private handlePointerCancel; private handleTouchStart; private handleTouchMove; private handleTouchEnd; private handleTouchCancel; /** * The `wl_pointer.axis_source` a wheel event deserves. * * Two answers, and neither is `finger`. A DOM wheel event never proves * a finger is on anything: macOS delivers a trackpad and a notched * wheel through the same pixel deltas, having already applied its own * acceleration curve to both. `finger` is the one source that invites * a toolkit to append momentum of its own — it obliges us to send an * `axis_stop`, and Chromium turns that into a fling — so claiming it * off a guess is how one notch of a real wheel ends up gliding. * `continuous` describes the same smooth stream without licensing that * second helping. Real fingers arrive through the touch handlers, * which don't have to guess. * * That leaves only the unmistakable wheels to spot: a `deltaMode` * coarser than pixels, or a whole number of 120px detents. Everything * else takes the harmless path, which costs a misread trackpad * nothing and a misread wheel only its detents. */ private wheelAxisSource; /** * Fold a source into the open sequence and answer with what the * sequence now is. * * A sequence only ever gets smoother. A trackpad's momentum tail can * land on a round 120px mid-flick, and calling that a wheel would hand * the client a detent it scales up by its own lines-per-click factor. * A finger overrides either, since the touch handlers know what they * are holding rather than inferring it from arithmetic. */ private latchScrollSource; /** * Report a swallowed wheel event once per reason. * * Every gate below is per-pane state, so any of them can silence one * pane's wheel while its neighbours scroll normally — and all of them * used to do it silently, which is unfalsifiable from the outside. Once * per reason, because a stuck gate is re-hit at the wheel's event rate, * and the set is cleared by the next wheel that gets through so a * recurrence is reported again. */ private reportWheelIgnored; private handleWheel; /** * Add to the pending scroll and arrange for it to be sent. * * Smooth wheel events already arrive at the browser's input cadence. Send * those immediately: waiting for the next animation frame couples input * to presentation, so one missed viewer frame merges two small trackpad * deltas into one visible jump. Notched wheels and direct touch gestures * retain frame batching to keep bursts bounded. */ private queueScroll; private flushScroll; /** * Close the sequence, and tell the client the gesture is over if a * finger was what drove it. * * A lifted finger is a real event with a real moment, and the toolkits * that fling do it off this: a flick on a touchscreen should coast, * and without a stop it never would. * * Nothing else gets one. `axis_stop` is what a toolkit regresses a * fling velocity from — Chromium starts one off any stop it can find * recent frames behind — and every other sequence we send arrived as * browser wheel events, which already carry whatever momentum the * platform decided they deserved. A stop there would be asking for a * second helping of it, which is exactly what a mouse wheel gliding to * a halt looks like. The protocol agrees for `wheel` at least: the * sequence may or may not be terminated and clients must not rely on * it. */ private endScrollSequence; private handleDragEnter; private handleDragOver; private handleDragLeave; private handleDrop; /** Upload dropped files into the connection's drag staging dir, then * send the DROP naming them. Any failure — the staging open or an * upload — cancels the session so no drag session dangles * compositor-side. */ private dropFiles; /** The canvas's staging sync, opened on first use and reused; a drop * that arrives while one is opening joins the same open. */ private dragStagingHandle; private handlePaste; /** Read an image from a clipboard read started by the Ctrl keydown. * * Starting `clipboard.read()` synchronously is load-bearing on browsers * that gate it on transient user activation: waiting for `readText()` to * settle first loses the key event's activation on macOS, so an image-only * clipboard is refused even though the user just pressed Ctrl+V. We still * wait for `readText()` before consuming this result because text wins when * the clipboard carries both representations. * * Only used for Ctrl chords. A Cmd chord's paste event is guaranteed (the * macOS menu command fires for the textarea focus is forced onto) and owns * the chord's outcome. */ private readClipboardImage; private handleKey; /** Forward any Alt presses held back for dead-key detection, ahead of * the event that proves they are a real modifier chord. */ private flushPendingAlt; /** Send one key with the mobile toolbar's armed modifier, if any. */ private sendOneShotModifiedKey; /** Seed the hidden textarea with deletable filler and park its caret at the * end. The filler is never forwarded to the Wayland client. */ private seedIOSInputPad; private scheduleIOSInputRepad; private resetTextInput; /** Handle text input from the hidden textarea. */ private handleTextInput; /** Handle IME composition end — send the composed text. */ private handleCompositionEnd; /** Send synthetic key-up for every key still held. Prevents stuck * modifiers and runaway key-repeat when focus leaves the canvas. */ private releaseAllKeys; private handleBlur; /** * Reconcile the modifiers the app believes are held with the ones the * browser says are, in both directions. * * A modifier reaches the app only as its own key press: nothing else in the * protocol carries the state, and a surface taking focus is not told it. * So a modifier already down before this surface had focus was never * forwarded here and stays invisible — Ctrl held while a terminal pane had * focus, then Ctrl+K aimed at the app, arrives as a bare k. Drift the other * way is just as real: window managers (especially on Linux) grab Super/Meta * without ever delivering the key-up to the browser, leaving `pressedKeys` * holding a key the user let go of. * * The browser's modifier flags are authoritative for both, so press what * should be held and is not, and release what is held and should not be. * Nothing here says which physical side is down, so a replayed press takes * the left key — the convention the synthesised chords already use. * * A replayed press is undone the ordinary way — by the release of the key * the user is actually holding, which `handleKey` redirects onto the side * this chose — or by the release half here on a later key-down. */ private syncModifiers; /** * Ensure the compositor's CapsLock state matches the browser before the * current key event is forwarded. * * The browser's `getModifierState("CapsLock")` always reflects the OS * state, but the compositor only sees key events forwarded through * `handleKey`. If CapsLock was toggled while the surface was unfocused, * the compositor's XKB state drifts. We detect the mismatch and inject * a synthetic CapsLock press+release to bring it back in sync. * * For a regular key (not CapsLock itself) the rule is simple: if the * browser and compositor disagree, inject a toggle. * * When the key IS CapsLock, `getModifierState` already shows the * *post-toggle* value. The compositor will also toggle when it receives * our forwarded keydown. For the end state to match we need the * compositor's *pre-toggle* state to be the opposite of the browser's * post-toggle value, i.e. `compositorCaps === !browserCaps`. If that * doesn't hold we inject an extra toggle first so the real key lands * correctly. */ private syncCapsLock; private handleFocus; } //# sourceMappingURL=BlitSurfaceCanvas.d.ts.map