macOS: touchscreen support (MacBook Pro PROLED XDR, macOS 27 gesture-recognizer touch)
Maintainers usually reply within 1 day
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 25/100
Research direction
Start by tracing the existing Windows touch path through NativeTaoBridge.EventCallback.onTouchInput and TaoComposeSceneHostWindows.onTouchInput, then compare it with the macOS trackpad path in touchpad_gestures.m. The issue proposes a native macOS gesture recognizer, shared Compose touch handling, and plugin configuration, but leaves hardware behavior and defaults unresolved. Done means direct touch reaches Compose without duplicate emulated input, trackpad behavior still works, and the documented hardware or Sidecar probe and regression checks pass.
Written by the indexing model from the issue text.
Description
Context
On 2026-10-28 Apple announced the MacBook Pro with PROLED XDR Display, the first Mac with a built-in multi-touch screen (available 2026-11-06). Apple says it supports scroll, pinch to zoom, rotate, drawing, double-tap and drag & drop on the screen, "as intuitive as they do on a trackpad" (product page).
The input model for AppKit has been documented since WWDC26 (June 2026, macOS 27), originally for Sidecar touch input:
- TN3212: Adopting gesture recognizers for Sidecar touch support
NSIsTouchNative(Info.plist key, macOS 27+)- AppKit updates, June 2026
- WWDC26 289: Modernize your AppKit app
Today Nucleus has a real touchscreen path on Windows (tao WindowEvent::Touch → NativeTaoBridge.EventCallback.onTouchInput → TaoComposeSceneHostWindows) and Linux (NativeTaoLinuxTouchBridge, platform/linux/touch.rs). On macOS there is no direct-touch path: the only "touch" is synthetic, from the trackpad pipeline. Nothing in the repo references NSIsTouchNative, NSGestureRecognizer or touchesBegan.
How macOS 27 delivers touch (summary of TN3212)
- Gesture recognizers are the only way to receive direct touches. Touches don't go through the responder chain,
NSApp/NSWindow.sendEvent(_:), or local/globalNSEventmonitors. - AppKit gathers the recognizers at touch-began by walking up the view hierarchy from the hit-tested top-most view. A view covered by another view, even a transparent one, gets nothing.
- Exclusivity: by default, gestures are active in only one view hierarchy at a time (scrolling excepted). You can lift this with
NSView.exclusiveGestureBehavior(per view, inherited by children) or theNSViewGestureRecognizerIsExclusiveInfo.plist key. - Mouse emulation for third-party UI frameworks, which covers us, since Compose draws into the tao
NSView. By default AppKit injects recognizers that translate:- tap →
mouseDownimmediately followed bymouseUp - touch and immediate pan → trackpad scroll events
- long press without movement →
rightMouseDown(+rightMouseDragged) andrightMouseUpon lift - two-finger pinch or rotation → trackpad magnify and rotate at the same time
- tap →
NSIsTouchNative = YESturns that emulation off, for frameworks that handle touch themselves.- Custom
NSGestureRecognizersubclasses gettouchesBegan/Moved/Ended/Cancelled(with:)withNSTouchobjects. With the macOS 27 SDK they must overridelocation(in:), or it returnsNSZeroPointand logs an error. They must also handle cancellation (touchesCancelled,mouseCancelled) and always reachended/failed/cancelled, or recognizers stay blocked. isCancellableByScrollGesturelets a touch recognizer yield to scrolling.- Capability detection:
NSScreen.TouchCapabilities.multiTouch. On macOS 27 it returnstruefor all displays as soon as a touch-capable Sidecar display is connected. NSApplication.currentEventno longer returns the event that triggered a control action. A touch is not part ofNSEvent.pressedMouseButtons.
What a Nucleus app gets today, with no change (expected, unverified)
| Gesture on screen | Emulated as | Nucleus path | Expected result in Compose |
|---|---|---|---|
| Tap | left mouse down/up | tao mouseDown/mouseUp |
click ✅ |
| One-finger pan | trackpad scroll | TaoSceneScrollRouter → PanStart/Move/End (or Scroll with -Dnucleus.tao.trackpadPanEvents=false) |
Modifier.scrollable scrolls ✅ |
| Long press | right mouse down/up | tao right-click | context menu ✅ |
| Pinch | magnify + rotate together | touchpad_gestures.m local monitor → dispatchTrackpadScale / two synthetic Touch contacts for rotate |
transformable zoom, with the "first gesture to begin owns it" rule from docs/internals/tao-trackpad.md ⚠️ |
| Rotate | magnify + rotate together | same | ⚠️ the owning gesture may be whichever arrives first, not the one the user meant |
Finger drag (slider, DnD, detectDragGestures, window drag area, tab drag) |
probably scroll, not mouse drag | Pan* |
❌ likely broken |
| Multi-finger, drawing, pressure | n/a | n/a | ❌ no PointerType.Touch pointers |
Open questions to check on hardware
- Do the emulated magnify/rotate events reach our local
NSEventmonitor (touchpad_gestures.m)? TN3212 says real touches bypass monitors. It says nothing about the emulated trackpad events. If they bypass it too, pinch-to-zoom is silently dead in every Nucleus app. - Does the emulated scroll carry
phase/momentumPhaselike a real trackpad, soTaoScrollGesturePhaseand fling keep working? - How is a one-finger drag emulated? Is it always scroll, or is there a press-then-move that becomes
mouseDragged? This decides whether sliders, text selection,WindowDragArea, tab drag (workspace/CrossWindowDrag) and DnD work by finger. - Do the tao
NSViewand our overlays (NSPanel popups,NativeView/TextureViewMac, decoration views indecoration.m) take part in hit testing in a way that lets AppKit's injected recognizers fire? A transparent view on top blocks every recognizer. - Do the title bar / traffic lights behave, given custom decoration and Liquid Glass (
macOsSdkVersion = "26.0")? Is double-tap to fill the screen wired? - Does any of this need an app linked against the macOS 27 SDK (vtool
macOsSdkVersion) to switch on?
Proposal: native touch path on macOS
- Native recognizer (ObjC, next to
touchpad_gestures.m): aNSGestureRecognizersubclass attached to the tao contentNSView, implementingtouchesBegan/Moved/Ended/Cancelled(with:)andlocation(in:). Restricted to direct touches (NSTouchTypeDirect), so trackpad touches keep the current path. It never "recognizes" in a way that cancels other input: it only forwards. - Forward through the existing wire:
NativeTaoBridge.EventCallback.onTouchInput(handle, phase, id, xFixed, yFixed, forceFixed), already used on Windows, withTaoTouchEvent.PRESS/MOVE/RELEASE/CANCEL. The finger id comes fromNSTouch.identity, and the position is physical px × 1024, likeCURSOR_MOVED(view coords → flip Y → × backing scale). Force is-1unless the panel reports it. - Compose side: share or move the Windows aggregation in
TaoComposeSceneHostWindows.onTouchInputinto the common host (AbstractTaoComposeSceneHost), so macOS emits realPointerType.Touchpointers. This also coversTaoTouchEventTime, the deadband (TaoPointerDeadband) and NSPanel popups (TaoPopupSceneLayer). - Exclusivity: set
exclusiveGestureBehavioron the tao view so that multi-touch and several simultaneous interactions (faders, two-hand gestures) work. - Turn off the emulation only once the native path exists: the plugin's
InfoPlistBuilderaddsNSIsTouchNative = YES. Otherwise every touch arrives twice, once as a touch and once as emulated mouse / scroll / magnify. Maybe behind a DSL switch (macOS { touchNative = true }, default to be decided) so an app can fall back to the emulation. - Dedupe with the trackpad pipeline: with native touches on, pinch and rotate come from Compose's own
detectTransformGestures/transformableover real pointers. The synthetic two-contact rotate and the magnify Scale path must stay trackpad-only. - Capability API (optional): expose
NSScreen.TouchCapabilities.multiTouch, keeping in mind it is wrong (true for every display) when a Sidecar touch display is connected. - Sidecar: the same recognizer gives iPad Sidecar touch support (iPadOS 27) for free, so we can test without the new MacBook.
- GraalVM: any new JNI entry point needs
reachability-metadata.json.onTouchInputalready exists, so ideally there are none.
Testing
- Hardware: MacBook Pro with PROLED XDR Display (available 2026-11-06), or Sidecar with an iPad on iPadOS 27 + macOS 27, which goes through the same gesture-recognizer pipeline.
- Lab probe: a touch probe that shows the live pointer list (id, type, position, pressure), plus a slider, a
detectDragGesturescanvas,transformableand a text field. Run it with and withoutNSIsTouchNative. - Regression: trackpad scroll / Pan / magnify / rotate unchanged (
TaoScrollWireDriftTest, the headful trackpad coverage indocs/internals/tao-trackpad.md).
Docs
Add a docs/internals/tao-macos-touch.md (emulation vs native, NSIsTouchNative, exclusivity, answers to the open questions), and a link from CLAUDE.md next to "Trackpad on Tao".
- Dominant language
- Kotlin
- Stars
- 393
- Forks
- 27
- Avg merge
- 22h 56m
- Merged PRs (30d)
- 103
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from NucleusFramework/Nucleus
-
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
NucleusFramework/Nucleus#912 ·
Maintainers usually reply within 1 day
-
documentation
Difficulty 1/5 Under an hour Newbie friendliness 85/100
NucleusFramework/Nucleus#475 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 18/100
NucleusFramework/Nucleus#901 ·
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 35/100
NucleusFramework/Nucleus#890 ·
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 30/100
NucleusFramework/Nucleus#882 · 7 comments ·
Maintainers usually reply within 1 day
All issues in NucleusFramework/Nucleus
Similar issues
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 71/100
streetcomplete/StreetComplete#7364 · 7 comments ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Kotlin-server-squad/vizcore#285 ·
Maintainers usually reply within 1 day
-
assertions 🔍
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
Maintainers usually reply within 1 day
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 66/100