First-class trackpad pinch input for GPU-surface apps#141
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
- GpuSurfaceInputKind gains phase-explicit pinch_begin/pinch_change/pinch_end and GpuSurfaceInputEvent a scale field carrying the per-event magnification delta (NSEvent.magnification semantics: cumulative scale is the product of 1 + delta); Windows/GTK sources are staged follow-ups noted at the vocabulary. - The AppKit host implements magnifyWithEvent: with begin/changed mapping directly, cancelled folding into pinch_end (deltas are applied as they arrive, so there is no transient to roll back), and change deltas emitted uncoalesced because summing coalesced deltas would drift from the product. - Bump the session journal to v5: gpu-surface input records gain the scale field (a layout change a v4 reader would misparse) plus the pinch kind codes 12-14, with the round-trip test pinning both. Co-authored-by: jhodges10 <18431344+jhodges10@users.noreply.github.com>
- Zig cores gain Options.on_pinch (the on_key shape): pinch is view-global, so the raw journaled gpu_surface_input pinch kinds surface as a phase-explicit PinchEvent with the change delta and view-local centroid, and the Msg dispatch replays from the same journaled event. - TS cores gain the pinchMsg(pinch) export: the wiring adapter maps the named begin/change/end phase alias by member name and widens the numbers, with the record shape validated at transpile time (NS1033) and mirrored in @native-sdk/core/events as PinchPhase/PinchEvent. Co-authored-by: jhodges10 <18431344+jhodges10@users.noreply.github.com>
- widget-pinch <view-label> <scale> [x y] synthesizes pinch_begin, one pinch_change carrying scale - 1 (the product of 1 + delta lands exactly on the commanded cumulative scale), and pinch_end at the given or view-center centroid. - Plain input synthesis per the widget-key discipline: every event journals as itself and replays through the same dispatch - pinch is not a widget verb, so no accessibility-action record. - Bump the automation protocol to v7 for the new verb string. Co-authored-by: jhodges10 <18431344+jhodges10@users.noreply.github.com>
- Magnification deltas are ~0.01..0.3 per event and the centroid is sub-point, so pinchMsg's parameter record fields are host values, never provable integers: without boundary marking, a core's pinch.scale === 0 comparison int-claimed the slot and every zoom product rounded to whole numbers. - frameMsg/keyMsg records keep their historical by-usage classing; the emitter test pins the f64 emission and the NS1033 shape refusal. Co-authored-by: jhodges10 <18431344+jhodges10@users.noreply.github.com>
- ui_app_tests drives begin/change/change/end plus the widget-pinch verb into a Zig core, pinning product-of-deltas cumulative scale (two +25% steps land on 1.5625, never a sum's 1.45), centroid delivery, default view-center aim, non-pinch isolation, and the non-positive-scale refusal. - The ts-core markup fixture exports pinchMsg (phase-gated on change) and the e2e drives raw events plus the automation verb through the transpiled core. - The record/replay reference session pinches once raw and once through the verb; replay re-derives the identical binary-exact zoom and fingerprints from the journaled scale fields alone. Co-authored-by: jhodges10 <18431344+jhodges10@users.noreply.github.com>
- native-ui gains a Trackpad pinch section (both tiers' channel shapes, product-of-deltas semantics, centroid, staged platforms); the automation/cli pages and the automation skill list widget-pinch. - Changelog fragment covers the channel, the verb (protocol v7), and journal v5. Co-authored-by: jhodges10 <18431344+jhodges10@users.noreply.github.com>
- The kind codes 12-14 map to the pinch phases and the ABI event's scale field carries the magnification delta with the converted centroid on x/y — the headless half of the magnifyWithEvent: path. Co-authored-by: jhodges10 <18431344+jhodges10@users.noreply.github.com>
- AppKit documents every magnifyWithEvent: as carrying the delta since the previous event, the Ended/Cancelled one included; the terminal branch now emits a nonzero delta as one last PINCH_CHANGE before PINCH_END so the cumulative product of (1 + delta) matches what the OS delivered - Cancelled deliberately shares the path: pinch applies deltas incrementally with no rollback, so the honest stream reports every measured delta, then ends - Pin the ordering in build.zig's host-source checks and fold a terminal-delta Ended plus a zero-delta Ended into the on_pinch product test
- AppKit reports gesture events at locationInWindow — the pointer location, never a midpoint between the fingers (raw touch positions are trackpad-normalized and have no view-space meaning) — so the API was right and the word "centroid" was wrong - Redocument x/y as the pointer anchor (zoom-at-cursor anchoring) across the platform vocabulary, the app-channel docs in both tiers, the automation verb's usage, the docs pages, the changelog fragment, and test names; the word has zero survivors
- PinchEvent gains window_id/label in Zig and windowId/label in TypeScript (the on_frame GpuFrame identity shape): x/y are view-local, so a coordinate without its view is not a position, and multi-window apps could not tell pinches apart - handlePinch forwards the identity the journaled platform event already carries; the TS adapter, NS1033 shape validation, and the emitted-record pins extend to the six-field contract, with the wholesale boundary classing covering the new numeric field - Pin distinguishable pinches in a two-window/two-view Zig fixture and mirror windowId plus a label match through the transpiled markup core end to end
…eltas - NSEvent.magnification is additive (Apple: gesture total = 1 + sum), so forwarding raw chunks made the product of (1 + delta) depend on event chunking; the macOS host now tracks the gesture's additive sum in f64 and emits per-event ratios, so the product equals Apple's total no matter how the driver sliced the gesture — the app rule is unchanged (zoom *= 1 + scale, memoryless). - A sum crossing -1 (a pinch cannot invert through zero scale) clamps at a floor of 2^-10, keeping every emitted factor positive; the arithmetic lives in tiny pure functions pinned by a file-contains step and mirrored runnably in a Zig test (chunking invariance, terminal-delta participation, round-trip to unity, clamp). - Product pins across ui_app/markup-e2e/session tests update to the normalized truths (1.5/1.75 instead of 1.5625/1.953125), and the widget-pinch docs now say plainly that <scale> is the gesture's final multiplicative zoom — the verb synthesizes events downstream of AppKit, untouched by the normalization.
- Revert the running-sum additive normalization of NSEvent.magnification: WebKit (ViewGestureControllerMac.mm: m_magnification += m_magnification * scaleWithResistance) and Chromium (pinch_update.scale = magnification + 1.0) both compound raw magnification multiplicatively, and Apple's own Event Handling Guide example multiplies — only the API reference's loose prose says "add". Raw magnification IS the multiplicative per-event delta; the doctrine comment at magnifyWithEvent: carries the receipts, and the test-appkit-pinch-magnification-doctrine step pins the comment, the raw forwarding, and the floor so a sum-based transformation cannot come back quietly. - The one guard that stays is per-event: a single magnification at or below -1 (a factor <= 0 on the wire — a zoom inverted through zero scale, physically impossible) clamps to -1 + 2^-10, keeping every emitted factor positive; the terminal-delta forwarding and synthesized-begin structure are unchanged. - Product pins return to the raw-forwarding truths (ui_app 1.5625/1.953125 and "Zoom 1.95", markup-e2e exact 1.5625/3.125, session 1.40625), the Zig normalization mirror test is deleted with the machinery it mirrored, and every contract/doc surface now states the engine convention instead of host-side normalization.
- The parser's finite-and-positive guard is not enough on the f32 wire: any scale at or below 2^-25 (e.g. 1e-20) rounds `scale - 1` to exactly -1, dispatching a pinch_change with factor 1 + delta = 0 — a zoom through zero scale no gesture can perform and no downstream product can recover from. - The dispatch now validates the computed wire delta and refuses with error.PinchScaleBelowWireMinimum before anything dispatches (no partial gesture reaches the journal); the minimum accepted scale — the smallest f32 above 2^-25, ~2.9802326e-8 — is named at the guard. - Tests pin both sides: `widget-pinch <view> 1e-20` is refused with the named error and moves no model state, and the smallest accepted scale round-trips a positive factor (delta -1 + 2^-24, model product exactly 0x1.8p-25).
- Integer inference keyed its host-ABI marking (number params of the entry's exported functions, and pinchMsg's wholesale record classing) on the inline export modifier only, so the first-class list spelling — `function pinchMsg(...) {...}` plus `export { pinchMsg };` — passed validation and wired the channel but skipped the marking: `pinch.scale === 0` int-claimed the slot to i64, every real magnification delta rounded to 0, and the adapter's zero gate silently dropped the events. The seam now reuses exportListBindings (the same export-list truth the checker, emitter, and model-helper surfaces already resolve by): an un-renamed entry-module list binding marks exactly like the modifier, and renamed bindings stay out on purpose — renamed wiring names never reach inference (NS1014/NS1047 fence them first) and a renamed ordinary function keeps its historical classing.
- Family audit: pinchMsg was the only wiring entry observably broken (it force-classes its record's fields; frameMsg/keyMsg/commandMsg carry no number param slots and keep by-usage field classing, appearanceMsg/chromeMsg/envMsgs are consts the emitter already reads list-aware, and model helpers were already list-aware in types.ts) — but the same hole covered every export-list function's number params, where a `=== 0` comparison int-claimed an i64 signature that would truncate host f64 arguments; the one seam fixes both.
- Tests pin the list spelling end to end: emitter mirrors of the pinch boundary-float classing and the frameMsg channel, an f64 host-boundary signature pin, an NS1014 pin that a renamed list entry still cannot bind a wiring name, a run-fidelity case whose 0.25 probe an i64 signature cannot even accept, and the markup e2e fixture's pinchMsg now exports by list so the native battery proves 0.25 deltas survive to the 1.5625 zoom product.
ctate
force-pushed
the
feat/trackpad-pinch
branch
from
July 17, 2026 17:11
eb5a9ae to
1416f8e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the reported gap: AppKit received magnify gestures but the GPU-surface event API dropped them before applications could respond — timeline and canvas apps had no pinch input at all.
pinch_begin/pinch_change/pinch_endjoinGpuSurfaceInputKind, with ascale: f32field carrying the per-change magnification delta (cumulative zoom = product of1+delta; deltas emit uncoalesced because summed coalesced deltas would drift the product) and the centroid on the existing x/y. The kind family stays open for rotate/smart-zoom; Windows precision-touchpad and GTK GestureZoom are staged honestly as follow-up sources (nothing panics — the kinds simply never arrive there yet).magnifyWithEvent:translates phases (Cancelled folds intopinch_end— unlike pointer-cancel there is no uncommitted transient to roll back; a Changed with no active gesture opens with a synthesized begin).Options.on_pinch; TypeScript cores exportpinchMsg(pinch): Msg | null. Zoom is an app concern — pinch deliberately bypasses the widget pipeline, and no registry codes were consumed. One transpiler fix rode along: the pinch record's fields are classed as boundary floats soscale === 0comparisons can't int-claim the slot and round zoom products.widget-pinchautomation verb (begin + one exact-product change + end, journaled as real input) so tests and users drive pinch without a trackpad; journal v5 carries the scale field and automation protocol v7 the verb — both stated as deliberate breaks in the changelog.Suites: full battery, validate, tooling, examples, ts-core e2e, docs check, packages/core npm suite (210/210) — all green.