DOM の mousemove で取れるのはウィンドウ内の座標だけで、カーソルがウィンドウの外にある間は何も分かりません。スクリーンショットツールやカーソル追従型のオーバーレイには画面全体の座標が必要です。Tauri では @tauri-apps/api/window の cursorPosition() と Rust の Window::cursor_position() がそれを返します。どちらも 物理ピクセル なので、DPI スケーリング環境での換算も説明します。
前提条件
プラグインは不要です。権限 core:window:allow-cursor-position は core:window:default に含まれているため、core:default を有効にしていればそのまま使えます。個別指定にしている場合だけ追加します。
{
"permissions": [
"core:window:allow-cursor-position",
"core:window:allow-scale-factor",
"core:window:allow-outer-position"
]
}
1. フロントエンドから実装する (TypeScript)
import { cursorPosition, getCurrentWindow } from '@tauri-apps/api/window';
const win = getCurrentWindow();
async function readCursor() {
const phys = await cursorPosition(); // 物理ピクセル。デスクトップ左上が原点。ウィンドウ外でも取れる
const logical = phys.toLogical(await win.scaleFactor()); // 論理ピクセル (CSS px 相当)
const outer = await win.outerPosition(); // これも物理ピクセル
const rel = { x: phys.x - outer.x, y: phys.y - outer.y }; // ウィンドウ左上からの相対 (物理)
return { phys, logical, rel };
}
// mousemove はウィンドウ外で止まるので、ポーリングで追う
setInterval(async () => {
const { phys, logical } = await readCursor();
document.getElementById('pos')!.textContent =
`physical (${phys.x}, ${phys.y}) / logical (${logical.x.toFixed(0)}, ${logical.y.toFixed(0)})`;
}, 200);
cursorPosition() は PhysicalPosition を返し、toLogical(scaleFactor) で LogicalPosition に変換できます。引き算するときに物理と論理を混ぜないよう注意してください。
2. バックエンドから実装する (Rust)
Window / WebviewWindow の cursor_position() が Result<PhysicalPosition<f64>> を返します。Rust 側で定期的に取ってイベントで流すと、フロントは listen するだけで済み、IPC の往復も減ります。
use serde::Serialize;
use std::time::Duration;
use tauri::{Emitter, Manager, WebviewWindow};
#[derive(Serialize, Clone)]
struct Cursor { x: f64, y: f64, lx: f64, ly: f64 }
fn read(window: &WebviewWindow) -> tauri::Result<Cursor> {
let p = window.cursor_position()?; // 物理座標
let l = p.to_logical::<f64>(window.scale_factor()?); // 論理座標
Ok(Cursor { x: p.x, y: p.y, lx: l.x, ly: l.y })
}
#[tauri::command]
fn cursor_now(window: WebviewWindow) -> Result<Cursor, String> {
read(&window).map_err(|e| e.to_string())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
let window = app.get_webview_window("main").unwrap();
std::thread::spawn(move || loop {
if let Ok(c) = read(&window) {
let _ = window.emit("cursor", c);
}
std::thread::sleep(Duration::from_millis(100));
});
Ok(())
})
.invoke_handler(tauri::generate_handler![cursor_now])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
import { listen } from '@tauri-apps/api/event';
type Cursor = { x: number; y: number; lx: number; ly: number };
await listen<Cursor>('cursor', (e) => console.log(e.payload));
動作確認
npm run tauri dev で起動し、カーソルをウィンドウの外へ出しても数値が更新され続ければ成功です。150% スケーリングの Windows では physical (1500, 900) / logical (1000, 600) のように物理が論理の 1.5 倍になります。モニターを 2 台つないで左側のモニターにカーソルを移すと x が負になります。
よくあるエラーと対処法
window.cursor_position not allowed. Permissions associated with this command: core:window:allow-cursor-position, ...という趣旨のエラー:core:window:defaultを外して個別指定にしている場合に起きます。上記の権限を追加します。- DOM の座標と一致しない:
cursorPosition()は物理ピクセルでデスクトップ原点、MouseEvent.clientXは論理ピクセルでビューポート原点です。toLogical()とinnerPosition()(タイトルバーを除いた原点)で揃えます。 - Android / iOS で失敗する: このコマンドはデスクトップ専用で、モバイルでは
Errになります。 - CPU 使用率が上がる: 100ms 未満の間隔で IPC を回すと目に見えて重くなります。Rust 側でループさせ、値が変わったときだけ
emitすると軽くなります。
OS ごとの違いと注意点
- Windows: モニターごとにスケーリングが違う構成では、どのモニター上にいるかで論理換算の係数が変わります。
win.scaleFactor()は「ウィンドウがあるモニター」の係数なので、他モニター上のカーソルはmonitorFromPoint(x, y)でそのモニターのscaleFactorを使います。 - macOS: OS 標準は論理ピクセル・左下原点ですが、Tauri は物理ピクセル・左上原点に統一して返します。Retina では 2 倍の値です。
- Linux: X11 では Windows と同様に動きます。Wayland はセキュリティ設計上、アプリが自分のウィンドウ外のポインタ位置を取得できず、ウィンドウ外では最後にウィンドウ内にいた座標が返るか、エラーになります。Wayland を主対象にするならウィンドウ内の
mousemoveで代替してください。 - マルチモニター: 原点はデスクトップ全体の左上(通常はプライマリモニターの左上)で、プライマリより左や上のモニターでは負の値になります。
