Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 27 additions & 7 deletions src/renderer/CanvasView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from '../core';
import {SkiaCanvasLayer} from './SkiaCanvasLayer';
import {CanvasMinimap, type MinimapPosition} from './CanvasMinimap';
import {scrollWheelEvents, type ScrollWheelEvent} from './NativeScrollWheelView';
import {scrollWheelEvents, type ScrollWheelEvent, type SmartMagnifyEvent} from './NativeScrollWheelView';
import {resolveScheme, getMutedTextColor, type ColorScheme} from './theme';
import {CanvasProvider} from './CanvasContext';
import {useViewportCulling} from './useViewportCulling';
Expand Down Expand Up @@ -502,6 +502,25 @@ export function CanvasView({content, basePath, renderMarkdown, initialViewState,
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

// Trackpad two-finger double-tap on macOS — Safari's "Smart Zoom" (#33).
// RNGH's TapGesture can't see trackpad multi-finger taps on RNGH-macos
// (every trackpad tap arrives as a single-pointer event; `minPointers(2)`
// fails on first touch and the gesture never starts). We bypass RNGH
// entirely via the native `smartMagnify` event. iOS keeps the
// single-finger RNGH double-tap below.
useEffect(() => {
if (!scrollWheelEvents) return;
const sub = scrollWheelEvents.addListener(
'onSmartMagnify',
(event: SmartMagnifyEvent) => {
const wx = (event.x - translateX.value) / scale.value;
const wy = (event.y - translateY.value) / scale.value;
handleDoubleTap(wx, wy);
},
);
return () => sub.remove();
}, [translateX, translateY, scale, handleDoubleTap]);

// Two-finger trackpad scroll to pan
useEffect(() => {
if (!scrollWheelEvents) return;
Expand Down Expand Up @@ -658,21 +677,22 @@ export function CanvasView({content, basePath, renderMarkdown, initialViewState,
// both — pinching never blocks tap detection at gesture-handler level
// (handleDoubleTap also gates on isPinching for in-flight pinch frames).
const tapGesture = useMemo(() => {
// macOS native convention for "double-tap to zoom" is a two-finger
// double-tap on the trackpad (Safari Smart Zoom, Preview zoom-to-page).
// iOS / Android / web all use single-finger. Branch the pointer count to
// match each platform's expectation. See #23.
const minPointers = Platform.OS === 'macos' ? 2 : 1;
let g = Gesture.Tap()
.numberOfTaps(2)
.minPointers(minPointers)
.maxDistance(DOUBLE_TAP_MAX_DISTANCE);
if (doubleTapMaxDelayMs != null) {
g = g.maxDelay(doubleTapMaxDelayMs);
}
return g.onEnd((event, success) => {
'worklet';
if (!success) return;
// macOS uses Safari's native two-finger Smart Zoom gesture (#33) —
// handled separately by the `onSmartMagnify` listener above. The
// RNGH tap still races with pan here so single-finger double-clicks
// don't accidentally trigger pan logic, but the click itself is a
// no-op. iOS keeps single-finger double-tap zoom (touchscreen
// convention).
if (Platform.OS === 'macos') return;
const wx = (event.x - translateX.value) / scale.value;
const wy = (event.y - translateY.value) / scale.value;
scheduleOnRN(handleDoubleTap, wx, wy);
Expand Down
24 changes: 24 additions & 0 deletions src/renderer/NativeScrollWheelView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,30 @@ export interface ScrollWheelEvent {
deltaY: number;
}

/**
* macOS trackpad two-finger double-tap — Safari's "Smart Zoom" gesture.
*
* Native `NSResponder.smartMagnifyWithEvent:` translates to top-left-origin
* view coordinates that match RNGH's `TapGesture` event shape, so the JS
* consumer can reuse the same world-coordinate conversion path as a
* single-finger tap on iOS.
*
* We bridge this natively (rather than via RNGH's `Tap().minPointers(2)`)
* because RNGH-macos sees every trackpad tap as a single-pointer event —
* the predicate fails and the gesture never starts. macOS routes two-finger
* taps through `smartMagnifyWithEvent:` on the NSResponder chain, which
* RNGH doesn't bridge.
*
* Consumer-side wiring required: a Swift module that listens for
* `smartMagnify(with:)` and emits an `onSmartMagnify` event through
* `ScrollWheelBridge`. See Workspace's `apps/desktop/macos/.../NativeModules/
* CanvasScrollInterceptor.swift` for the reference implementation.
*/
export interface SmartMagnifyEvent {
x: number;
y: number;
}

const ScrollWheelBridge =
Platform.OS === 'macos' ? NativeModules.ScrollWheelBridge : null;

Expand Down