158 lines
5.2 KiB
Markdown
158 lines
5.2 KiB
Markdown
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.
|
|
|
|
```toml
|
|
# src-tauri/Cargo.toml
|
|
|
|
[target."cfg(target_os = \"linux\")".dependencies]
|
|
gtk = "0.18"
|
|
```
|
|
|
|
```rust
|
|
``#[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 `WheelEvent`s. 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.
|
|
|
|
```ts
|
|
// 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);
|
|
});
|
|
}
|
|
```
|
|
|
|
```tsx
|
|
// 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-pinch` before launching the
|
|
app. Pinch then fires as a regular `wheel` event with `ctrlKey: true` and the
|
|
adapter is not needed.
|
|
- **macOS (WKWebView):** access the webview from Rust with `with_webview` and
|
|
set `allowsMagnification = false`. The Linux adapter is not needed.
|
|
- The sensitivity constants (`100` in the adapter, `0.01` in the wheel handler)
|
|
are tuned to roughly match browser pinch feel. Adjust if the zoom speed feels
|
|
off.
|