ちなみに答えはパレットです。
絵を描く時、アナログでもデジタルでも、カラーパレットというものがあると思います。
また、タイルエディターでも、タイルのパレット...タイルを選択して塗っていくようなものがあるような...多分。
パレットウィンドウを召喚してみよう。
Tauri では、現在のメインウィンドウの他にもウィンドウを召喚することが出来ます。
WebViewWindow
というものを使って、新しいウィンドウを開いてみましょう。
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
を使うことでイベントを登録できます。
イベントについては自作することもできますし、デフォルトで用意されているもの (今回使用する「ウィンドウが閉じられるときに」とか)もあります。
詳しくはドキュメントを見ることをお勧めします。
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
で画像のパスを入力してもらうことにしましょう。
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
を開くようにしましょう。
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
には、独自のイベントを作ることができ、これを用いることでウィンドウ間でデータのやり取りとかが行えます。
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
イベントに対してマウス座標の取得と、その座標にあるタイルを取得する処理を書いてみましょう。
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
しなくてもイベントは受け取れるような...?)
ここにイベントに関するドキュメントがあるので、これを読んで理解を深めるといいかもしれません。
あとは簡単ですね。
editorMap.main.ts
で、クリックしたらその座標を取得してthis.tiles
にうまい事はめ込んで...ですか。
なお、このままではファイル出力などは出来ませんが...JSON.stringify
とかconsole.log
を使えば外部へ出すことはできます。
ここら辺は、好みでいろんな実装をしてみてください。
おわりに
ちょっとだけモチベが落ちたんだけど、書かなきゃな~で書いてたから他の記事に比べても突出して乱雑だろうな!