親ウィンドウに対してモーダル表示する (Vite Multi-page)

Recipe ID: win-018

レシピ「win-017: 新しいサブウィンドウを開く (Vite Multi-page)」で作成したマルチページ構成をベースに、サブウィンドウをモーダルとして動作させる方法を解説します。

Tauri には標準のモーダル API (親ウィンドウを完全にブロックする機能) がないため、親ウィンドウを一時的に無効化 (setEnabled(false)) し、サブウィンドウを最前面に固定 (alwaysOnTop: true) することで、擬似的なモーダル挙動を実現します。

前提条件

* レシピ win-017 の実装が完了していること (Vite の設定、subwindow.html の作成など)

Permissions (権限) の設定

win-017 の権限設定に加え、ウィンドウの有効/無効を切り替える権限と、フォーカスを設定する権限を追加します。

1. core:window:allow-set-enabled: 親ウィンドウを操作不可にするために必要
2. core:window:allow-set-focus: モーダル終了後に親ウィンドウへフォーカスを戻すために必要

src-tauri/capabilities/default.json:

{
  "permissions": [
    ...,
    "core:window:default",
    "core:webview:allow-create-webview-window",
    "core:window:allow-close",
    "core:window:allow-set-enabled",
    "core:window:allow-set-focus"
  ],
  "windows": ["main", "settings-window"]
}

実装の変更点

win-017 からの変更・追加部分は主にメインウィンドウ側の TypeScript (src/main.ts) です。
サブウィンドウを「開く前」に親を無効化し、「閉じた後」に親を有効化するロジックを追加します。

src/main.ts

getCurrentWindow のインポートを追加し、openSubWindow 関数を以下のように修正します。

import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
import { getCurrentWindow } from '@tauri-apps/api/window'; // 追加

async function openSubWindow() {
  const label = 'settings-window';
  
  // 1. 既に開いている場合はフォーカスして終了(多重起動防止)
  const existingWin = await WebviewWindow.getByLabel(label);
  if (existingWin) {
    await existingWin.setFocus();
    return;
  }

  // 2. 現在のウィンドウ(親)を取得し、無効化する
  const parentWindow = getCurrentWindow();
  await parentWindow.setEnabled(false); // 親ウィンドウへの操作をブロック

  // 3. モーダルウィンドウを作成
  const webview = new WebviewWindow(label, {
    url: '/subwindow.html',
    title: '設定 (モーダル)',
    width: 600,
    height: 400,
    resizable: false,
    center: true,
    alwaysOnTop: true, // 他のウィンドウより手前に表示
    // parent: parentWindow // (Linux/macOS等でOSレベルの親子関係を持たせる場合に有効)
  });

  // 4. 作成完了時のイベント (win-017と同様)
  webview.once('tauri://created', () => {
    console.log('モーダルウィンドウ作成成功');
  });

  // エラー時のハンドリング
  webview.once('tauri://error', async (e) => {
    console.error('モーダルウィンドウ作成失敗:', e);
    // 失敗した場合は親ウィンドウのロックを解除しないと操作不能になるため復帰させる
    await parentWindow.setEnabled(true);
  });

  // 5. モーダルが破棄(クローズ)されたら親ウィンドウを復帰させる
  // 'tauri://destroyed' はウィンドウが完全に閉じられた後に発火します
  webview.once('tauri://destroyed', async () => {
    await parentWindow.setEnabled(true); // 操作ロック解除
    await parentWindow.setFocus();       // フォーカスを戻す
    console.log('モーダルが閉じられました');
  });
}

document.querySelector('#open-settings')?.addEventListener('click', openSubWindow);

src/subwindow/main.ts (変更なし)

サブウィンドウ側のコードは win-017 と変わりません。「閉じる」ボタンが押されたときに appWindow.close() を実行すれば、メインウィンドウ側で tauri://destroyed が検知され、親ウィンドウが復帰します。

// (win-017と同じ)
import { getCurrentWindow } from '@tauri-apps/api/window';

const appWindow = getCurrentWindow();

document.getElementById('close-btn')?.addEventListener('click', async () => {
    await appWindow.close();
});

解説

この実装パターンのポイントは以下の通りです。

1. 擬似的なブロッキング:
setEnabled(false) を使うと、ユーザーは親ウィンドウをクリックしたり入力したりできなくなります。これにより、「モーダルが開いている間は他の操作をさせたくない」という要件を満たせます。

2. イベントによる復帰:
モーダルは「閉じるボタン」「×ボタン」「Alt+F4」など様々な方法で閉じられる可能性があります。tauri://destroyed イベントを監視することで、どのような閉じられ方をしても確実に親ウィンドウのロックを解除できるようにしています。

3. エラーハンドリング:
ウィンドウ作成自体が失敗した場合 (tauri://error) にもロック解除 (setEnabled(true)) を行わないと、アプリ全体が操作不能(フリーズ状態)になってしまうため、必ずエラー時の復帰処理を入れましょう。