【Day15】絵の具を使うときに必要なあれ。【QAC24】


# ちなみに答えはパレットです。

絵を描く時、アナログでもデジタルでも、カラーパレットというものがあると思います。
また、タイルエディターでも、タイルのパレット...タイルを選択して塗っていくようなものがあるような...多分。

# パレットウィンドウを召喚してみよう。

Tauri では、現在のメインウィンドウの他にもウィンドウを召喚することが出来ます。

`WebViewWindow`というものを使って、新しいウィンドウを開いてみましょう。

```typescript:/src/maps/editorMap.main.ts
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";

export class EditorMapMain extends GameMap {
  ...

  constructor() {
    super();

    const window = new WebViewWindow("newGoogleWindow", {
      alwaysOnTop: true,
      closable: false,
      title: "Tiles",
      url: "https://www.google.com",
    });
  }
}
```

`alwaysOnTop`は常に最前面に表示するかどうか、`closable`は閉じるボタンを表示するかどうか、`title`はウィンドウのタイトル、`url`は開くページの URL です。
まぁ、英語だけを読めば、Google のページを最前面で開くウィンドウを作成するということですね。

じゃあ実際に実行してみましょう。

# Oops!

多分、なんも起きないと思います。
何なら、Inspector を開いてみても、何も表示されていないかもしれません。

実際には以下のようなエラーが発生しています。

`"webview.create_webview_window not allowed. Permissions associated with this command: webview:allow-create-webview-window"`

> webview.create_webview_window が許可されていません。このコマンドに関連付けられた権限: webview:allow-create-webview-window

新しいウィンドウ 1 つを開くのだけでも権限が要求されます。
セキュリティ上の問題ですわね。

# 権限を追加してみよう～

`/src-tauri/capabilities/default.json`というファイルがあると思うのですが、この中の`permissions`という所で使用する権限を列挙していきます。

`permissions`にさっき言われた`webview:allow-create-webview-window`を追加してみましょう。

...実際には `webview:allow-create-webview-window`ではなく、`core:webview:allow-create-webview-window`ですけど。

これは、Tauri がデフォルトで提供している機能に対する権限の親は`core`という名前空間で管理されているからです。
デフォルトで提供されていない機能として、ファイルを操作したりする`fs`といった機能がありますが、これは`core`ではなく、`fs`という名前空間で管理されています。

これだけで新しく Google なウィンドウが出てきたと思います。
最前面に出てくるので邪魔ですね。

ってなんなら消せないじゃん。おい。

`pnpm tauri dev`を実行したターミナルで`Ctrl + C`を押して強制終了しましょう。

# ウィンドウを消す方法

先ほどのコードで`closable: false`としていたので、閉じるボタンが表示されていませんでした。
しかし、プログラム内からウィンドウを閉じることは可能です。

それは、`window.destroy()`または`window.close()`を実行することで可能です。

これは...そうですね、メインのウィンドウが閉じられたときに、ついでに閉じるようにするのが賢明でしょうか。

そしたら、現在のウィンドウが閉じられたときになんかするようなイベントハンドラが必要ですね。

ウィンドウに対して`listen`を使うことでイベントを登録できます。
イベントについては自作することもできますし、デフォルトで用意されているもの (今回使用する「ウィンドウが閉じられるときに」とか)もあります。
詳しくはドキュメントを見ることをお勧めします。

```typescript:/src/maps/editorMap.main.ts
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
import { getCurrentWindow } from "@tauri-apps/api/window";

export class EditorMapMain extends GameMap {
  ...

  constructor() {
    super();

    const window = new WebViewWindow("newGoogleWindow", {
      alwaysOnTop: true,
      closable: false,
      title: "Tiles",
      url: "https://www.google.com",
    });

    getCurrentWindow().listen("tauri://close-requested", () => {
      window.destroy();
      getCurrentWindow().destroy();
    });
  }
}
```

こんな感じでしょうか。
これで、メインウィンドウが閉じられたときに、ウィンドウも閉じられるようになりそうです。

が、ここでも権限の問題が発生します。

`core:window:allow-destroy`を`permissions`に追加しておきましょう。

これで動作すると思います。

> 一般的には`destroy`ではなく`close`を用いるのですが、なんかうまく閉じないので...強制的に閉じることが出来る`destroy`を使っています。

# タイルパレットを作る

`index.html`をコピーして`tiles.html`、`main.ts`をコピーして`editMap.tiles.ts`を作成しましょう。

`index.html`の中で`<script src="/src/main.ts"></script>`となっている部分を`<script src="/src/editMap.tiles.ts"></script>`に変更しましょう。

そしたら新しいマップとして`editorMap.tiles.ts`を作成しましょう。
(`editorMap.main.ts`をコピーすると楽)

このウィンドウでは、タイルマップ画像を読み込むので、今回は超省略して、`prompt`で画像のパスを入力してもらうことにしましょう。

```typescript:/src/maps/editorMap.tiles.ts
export class EditorMapTiles extends GameMap {
  ...

  private imagePath: string = "";
  private image: HTMLImageElement | null = null;

  private async drawImage() {
    if (this.image) {
      getContext().drawImage(this.image, 0, 0);
    } else {
      image = new Image();
      image.src = imagePath;
      await image.decode();
      getContext().drawImage(image, 0, 0);
    }
  }

  constructor() {
    super();
    imagePath = prompt("画像のパスを入力してください。");
    drawImage();
  }
}
```

そして、先ほどまでウィンドウがどうのこうのっていじってた`editorMap.main.ts`を修正して、`editorMap.tiles.ts`を開くようにしましょう。

```typescript:/src/maps/editorMap.main.ts
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
import { getCurrentWindow } from "@tauri-apps/api/window";

export class EditorMapMain extends GameMap {
  ...

  constructor() {
    super();

    const window = new WebViewWindow("editorMapTiles", {
      alwaysOnTop: true,
      closable: false,
      title: "Tiles",
      url: "./tiles.html",
    });

    getCurrentWindow().listen("tauri://close-requested", () => {
      window.close();
    });
  }
}
```

タイルパレットの方で、タイルマップ画像のタイルを選択したら、メインウィンドウの方でそのタイルを描画出来るようにしましょう。

# window のイベント

`window`には、独自のイベントを作ることができ、これを用いることでウィンドウ間でデータのやり取りとかが行えます。

```typescript:/src/maps/editorMap.main.ts
export class EditorMapMain extends GameMap {
  ...

  private paintTile: RealTile | null = null;

  constructor() {
    super();

    const window = new WebViewWindow("editorMapTiles", {
      alwaysOnTop: true,
      closable: false,
      title: "Tiles",
      url: "./tiles.html",
    });

    getCurrentWindow().listen("tauri://close-requested", () => {
      window.close();
    });

    getCurrentWindow().listen("tile", async (e) => {
      const tile = e.payload as FakeTile;
      console.log(`Received: ${JSON.stringify(tile)}`);
      const realtile = await fakeTileToRealTile(tile);
      this.paintTile = realtile;
    })
  }
}
```

さっきも使った`listen`で、`tile`というイベントを登録しています。
公式には存在しない`tile`というイベントですが、なんでも自作できてしまいます.
`e.payload`には、イベントのデータ...送信されてきたデータが入ります。
型はただの Object だと思うので、アサーションでなんとかしてあげましょう。
(安全にやるなら、TypeGuard な関数を作るといいだろう。)

## タイルをクリックしたら...？

もうそろそろイベントハンドラはお手の物かと思われます。
`click`イベントに対してマウス座標の取得と、その座標にあるタイルを取得する処理を書いてみましょう。

```typescript:/src/maps/editorMap.tiles.ts
import { getCurrentWindow } from "@tauri-apps/api/window";

export class EditorMapTiles extends GameMap {
  ...

  protected async click(e: MouseEvent) {
    e.preventDefault();
    const { x, y, isInside } = getMousePosition(e);
    if (!isInside) return;
    const canvasPos = getCanvasPosition(x, y);
    const tile: FakeTile = {
      path: imagePath,
      x: canvasPos.x,
      y: canvasPos.y,
      width: 1,
      height: 1,
    };
    getCurrentWindow().emit("tile", tile);
  }

  ...
}
```

こんな感じかな？

`emit`を使うことでイベントを発火させることが出来ます。
今回は、CurrentWindow に対して`emit`していますが、ここら辺は正直私もまだ理解しきれていなくて...
(イベント定義元ウィンドウに対して`emit`しなくてもイベントは受け取れるような...？)

https://v2.tauri.app/reference/javascript/api/namespaceevent/

ここにイベントに関するドキュメントがあるので、これを読んで理解を深めるといいかもしれません。

あとは簡単ですね。
`editorMap.main.ts`で、クリックしたらその座標を取得して`this.tiles`にうまい事はめ込んで...ですか。

なお、このままではファイル出力などは出来ませんが...`JSON.stringify`とか`console.log`を使えば外部へ出すことはできます。
ここら辺は、好みでいろんな実装をしてみてください。

# おわりに

ちょっとだけモチベが落ちたんだけど、書かなきゃな～で書いてたから他の記事に比べても突出して乱雑だろうな！