押せるカードの上で指の形にする、キャンバスの上で十字にする、処理中は待機カーソルにする、といった変更は、Tauri アプリでも普通の Web ページと同じく CSS の cursor で行うのが基本です。Tauri にも setCursorIcon()(Rust は set_cursor_icon())がありますが、これはウィンドウ単位の指定で、ページの上ではページ側の CSS の指定に負けることがあります。使い分けと、Rust の処理中にアプリ全体を待機カーソルにする方法、カーソルを隠す API の OS ごとの癖をまとめます。
前提条件
CSS の cursor には権限もプラグインも要りません。ウィンドウ単位のカーソルの API はどれも core:default に含まれないので、使うものだけ追加します。
| API | できること | 権限 |
|---|---|---|
setCursorIcon() | ウィンドウのカーソルの種類を変える | core:window:allow-set-cursor-icon |
setCursorVisible() | カーソルを隠す・戻す | core:window:allow-set-cursor-visible |
setCursorGrab() | カーソルをウィンドウの外に出さない | core:window:allow-set-cursor-grab |
setCursorPosition() | カーソルを指定した位置へ動かす | core:window:allow-set-cursor-position |
現在の位置を読む cursorPosition() だけは core:window:default に含まれます(カーソル座標の取得)。
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"core:window:allow-set-cursor-icon",
"core:window:allow-set-cursor-visible"
]
}
CSS とウィンドウの API の使い分け
| やりたいこと | 使うもの |
|---|---|
| ボタンやキャンバスの上だけ変える | CSS の cursor |
| 処理中はアプリ全体を待機カーソルにする | html に付けたクラスと CSS |
| 独自の画像にする | CSS の cursor: url(...) |
| 動画の上などでカーソルを消す | CSS の cursor: none |
| ウィンドウの外に出さない・位置を動かす | setCursorGrab() / setCursorPosition() |
setCursorIcon() に渡せるのは決まった種類(CursorIcon)だけで、画像は指定できません。
1. フロントエンドから実装する (TypeScript)
要素ごとに変える (CSS)
.card[role="button"] { cursor: pointer; }
canvas.editor { cursor: crosshair; }
.sortable { cursor: grab; }
.sortable.dragging { cursor: grabbing; }
button:disabled { cursor: not-allowed; }
/* 独自の画像。数値はクリック位置(ホットスポット)、最後のキーワードは読み込めないときの予備 */
.pen-tool { cursor: url('/cursors/pen.png') 2 30, crosshair; }
独自の画像は 32×32 px 程度の PNG にし、Vite なら public/cursors/ に置きます(url() の相対パスは CSS ファイルの位置が基準)。予備のキーワード(上の crosshair)を省くと宣言全体が無効になり、大きすぎる画像は無視されることがあります。
CSS の名前と setCursorIcon() の名前は一部が違います。
| CSS | CursorIcon |
|---|---|
pointer | 'hand' |
not-allowed | 'notAllowed' |
ew-resize / ns-resize | 'ewResize' / 'nsResize' |
context-menu | 'contextMenu' |
zoom-in | 'zoomIn' |
処理中はアプリ全体を待機カーソルにする
document.body.style.cursor = 'wait' だけでは、cursor: pointer を付けたボタンや入力欄(文字カーソル)の上で元のカーソルに戻ります。子要素が自分の cursor を持っているからです。ルートにクラスを付け、すべての要素を !important で上書きします。
html.busy,
html.busy * {
cursor: progress !important;
}
progress は「処理中だが操作はできる」、wait は「終わるまで待ってほしい」を表すので、処理中も操作させるかどうかで選びます。
import { invoke } from '@tauri-apps/api/core';
let pending = 0;
// 処理の間だけ busy クラスを付ける。並行した処理がすべて終わるまで外さない
export async function withBusy<T>(task: () => Promise<T>): Promise<T> {
pending++;
document.documentElement.classList.add('busy');
try {
return await task();
} finally {
if (--pending === 0) document.documentElement.classList.remove('busy'); // 失敗しても必ず戻す
}
}
document.querySelector('#run')?.addEventListener('click', () => {
withBusy(() => invoke<number>('heavy_task'))
.then((n) => console.log('done:', n))
.catch((e) => console.error(e));
});
ウィンドウ単位の API で変える
import { getCurrentWindow } from '@tauri-apps/api/window';
const win = getCurrentWindow();
// 3 秒間だけウィンドウのカーソルを変えて隠し、元に戻す
export async function windowCursorDemo() {
await win.setCursorIcon('wait'); // core:window:allow-set-cursor-icon が必要
await win.setCursorVisible(false); // core:window:allow-set-cursor-visible が必要
window.setTimeout(() => {
void win.setCursorVisible(true);
void win.setCursorIcon('default');
}, 3000);
}
ページの上では、ページ側の CSS が指定したカーソルが優先されます。setCursorIcon('wait') を呼んでも、マウスを動かした時点で CSS の指定に戻ることがあります。setCursorVisible(false) は OS によって隠れる範囲が違う(後述)ので、ページの中だけ隠すなら CSS の cursor: none が扱いやすいです。
2. バックエンドから実装する (Rust)
Rust の set_cursor_icon() も JS と同じウィンドウ単位の指定です。コマンドの引数を tauri::CursorIcon にすると JS から 'wait' のような名前で受け取れますが、知らない名前はエラーにならず Default として届きます。CSS の 'pointer' を渡しても通常の矢印になるだけです。
トレイやショートカットなど Rust 側から始まる処理では、開始と終了をイベントでページに知らせ、1 章の busy クラスを付け外しします。
use std::time::Duration;
use tauri::{AppHandle, CursorIcon, Emitter};
/// invoke から呼ぶ重い処理の例(カーソルはページの withBusy() で変える)
#[tauri::command]
async fn heavy_task() -> Result<u32, String> {
tauri::async_runtime::spawn_blocking(|| {
std::thread::sleep(Duration::from_millis(1500)); // 重い処理の代わりに 1.5 秒待つ
42
})
.await
.map_err(|e| e.to_string())
}
/// Rust 側から始める処理。開始と終了をイベントでページに知らせる
fn start_sync(app: AppHandle) {
tauri::async_runtime::spawn(async move {
let _ = app.emit("busy", true);
let _ = tauri::async_runtime::spawn_blocking(|| std::thread::sleep(Duration::from_secs(2))).await;
let _ = app.emit("busy", false); // 処理が失敗しても必ず送る
});
}
#[tauri::command]
fn sync_now(app: AppHandle) {
start_sync(app);
}
/// ウィンドウ単位のカーソル。知らない名前は Default として届く
#[tauri::command]
fn set_window_cursor(window: tauri::WebviewWindow, icon: CursorIcon) -> Result<(), String> {
window.set_cursor_icon(icon).map_err(|e| e.to_string())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![heavy_task, sync_now, set_window_cursor])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Rust のコマンド経由なら、JS 側に core:window:allow-set-cursor-icon は要りません。
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
// Rust から届いた開始・終了に合わせて busy クラスを付け外しする
await listen<boolean>('busy', (e) => {
document.documentElement.classList.toggle('busy', e.payload);
});
await invoke('sync_now'); // 約 2 秒間、ページ全体が待機カーソルになる
await invoke('set_window_cursor', { icon: 'crosshair' });
動作確認
npm run tauri dev で起動し、カードやキャンバスにカーソルを乗せると CSS で指定した形に変わります。#run のボタンを押すと、処理が終わるまでボタンや入力欄の上も含めてページ全体が progress のカーソルになり、約 1.5 秒後に元に戻ってコンソールに次のように出ます。
done: 42
よくあるエラーと対処法
- 「window.set_cursor_icon not allowed. Permissions associated with this command: core:window:allow-set-cursor-icon」: 権限の追加漏れです。リリースビルドでは「Command plugin:window|set_cursor_icon not allowed by ACL」と出ます。
setCursorVisible()なども同じ形で、コマンド名と権限名だけが変わります。 - 「Argument of type '"pointer"' is not assignable to parameter of type 'CursorIcon'.」: CSS の名前を
setCursorIcon()に渡しています。上の対応表のとおり'hand'などに直します。 - ボタンや入力欄の上だけ待機カーソルにならない:
bodyにだけ指定しています。html.busy *と!importantで子要素まで上書きします。 setCursorIcon()の指定がすぐ元に戻る: ページ側の CSS が優先されています。アイコンは CSS で変えます。
OS ごとの違いと注意点
- Windows:
setCursorVisible(false)で隠れるのはウィンドウの中だけです。 - macOS:
setCursorVisible(false)は、ウィンドウにフォーカスがある間はウィンドウの外でもカーソルを隠します。ほかのアプリを操作しようとしても見えないので、ページから出たとき(document.documentElementのmouseleave)に表示へ戻すか、CSS のcursor: noneを使います。 setCursorGrab(): Linux は非対応です。macOS ではカーソルがその場に固定され、見た目が不自然になります。- iOS / Android: ウィンドウ単位のカーソルの API はデスクトップ専用です。
- 共通:
waitやprogressの絵柄は OS とユーザーのカーソル設定に従います。どの OS でも同じ見た目にしたいなら、CSS で独自の画像を指定します。
