5.2 KiB
On Linux, Tauri uses WebKitGTK as its webview, which intercepts trackpad pinch
gestures at the native level and applies them as page zoom. Calling
preventDefault() on wheel events from JavaScript does not stop this, because
WebKit handles the magnification through a separate gesture pipeline that
bypasses the DOM event system.
The approach below intercepts the pinch gesture at the GTK layer before WebKit
sees it, then re-dispatches it as a synthetic WheelEvent with ctrlKey: true.
This is the same shape of event that browsers fire natively for trackpad pinch
on Chrome/Firefox, so the React frontend code can stay platform-agnostic and
use a single wheel event handler everywhere.
The Rust side attaches a gtk::GestureZoom controller to the GTK window in the
Capture propagation phase and claims the gesture, which prevents WebKit from
also processing it. It then emits Tauri events containing the scale factor and
the cursor position.
# src-tauri/Cargo.toml
[target."cfg(target_os = \"linux\")".dependencies]
gtk = "0.18"
``#[cfg(target_os = "linux")]
fn setup_linux_pinch(window: &tauri::WebviewWindow) {
use gtk::prelude::*;
use gtk::{EventSequenceState, PropagationPhase};
use tauri::{Emitter, Manager};
let gtk_window = window.gtk_window().expect("gtk window");
let app_handle = window.app_handle().clone();
let gesture = gtk::GestureZoom::new(>k_window);
gesture.set_propagation_phase(PropagationPhase::Capture);
let ah = app_handle.clone();
gesture.connect_begin(move |gesture, _seq| {
gesture.set_state(EventSequenceState::Claimed);
let (x, y) = gesture.bounding_box_center().unwrap_or((0.0, 0.0));
let _ = ah.emit("pinch-begin", (x, y));
});
let ah = app_handle.clone();
gesture.connect_scale_changed(move |_, scale| {
let _ = ah.emit("pinch-scale", scale);
});
unsafe {
gtk_window.set_data("zoom-gesture", gesture);
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
#[cfg(target_os = "linux")]
{
use tauri::Manager;
let window = app.get_webview_window("main").unwrap();
setup_linux_pinch(&window);
}
Ok(())
})
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
On the JavaScript side, a small adapter listens for the Tauri events and
re-dispatches them as synthetic WheelEvents. The adapter checks for
window.__TAURI_INTERNALS__ and bails out silently when running in a plain
browser, where the events would not fire and @tauri-apps/api/event would
fail to initialize.
// src/tauriPinchAdapter.ts
export async function setupTauriPinchAdapter() {
// Bail out if not running inside Tauri
if (typeof window === 'undefined' || !('__TAURI_INTERNALS__' in window)) {
return;
}
// Dynamic import so the @tauri-apps/api code isn't even loaded in the browser
const { listen } = await import('@tauri-apps/api/event');
let pinchOriginX = 0;
let pinchOriginY = 0;
let lastScale = 1;
await listen<[number, number]>('pinch-begin', (event) => {
const [x, y] = event.payload;
pinchOriginX = x;
pinchOriginY = y;
lastScale = 1;
});
await listen<number>('pinch-scale', (event) => {
const currentScale = event.payload;
const scaleRatio = currentScale / lastScale;
lastScale = currentScale;
const deltaY = -Math.log(scaleRatio) * 100;
const target = document.elementFromPoint(pinchOriginX, pinchOriginY) ?? document.body;
const wheelEvent = new WheelEvent('wheel', {
deltaY,
ctrlKey: true,
clientX: pinchOriginX,
clientY: pinchOriginY,
bubbles: true,
cancelable: true,
});
target.dispatchEvent(wheelEvent);
});
}
// src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { setupTauriPinchAdapter } from "./tauriPinchAdapter";
setupTauriPinchAdapter();
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
With the adapter in place, any component that wants to handle zoom can use a
plain wheel event listener with ctrlKey detection. The same code works in
the browser, on Linux Tauri (via the adapter), and on other platforms where the
browser-style events fire natively.
Notes on the other platforms:
- Windows (WebView2): set the env var
WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--disable-pinchbefore launching the app. Pinch then fires as a regularwheelevent withctrlKey: trueand the adapter is not needed. - macOS (WKWebView): access the webview from Rust with
with_webviewand setallowsMagnification = false. The Linux adapter is not needed. - The sensitivity constants (
100in the adapter,0.01in the wheel handler) are tuned to roughly match browser pinch feel. Adjust if the zoom speed feels off.