Add tauri-pinch-zoom.md
This commit is contained in:
157
rust/tauri-pinch-zoom.md
Normal file
157
rust/tauri-pinch-zoom.md
Normal file
@@ -0,0 +1,157 @@
|
||||
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.
|
||||
Reference in New Issue
Block a user