【Tauri + Yew】Yew で Tauri の Events 使ってみた

# はじめに
Tauri + Yew の構成で、[Events](https://tauri.app/v1/guides/features/events) の listen と emit を試してみました！
この記事では、サンプルコードを使って、Yew で [Events](https://tauri.app/v1/guides/features/events) を使うやり方を紹介します。

# サンプルについて
今回扱うサンプルでは、 Tauri が emit した文字列を Yew が listen し、画面に描画するアプリケーションを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3586613/81c1a40d-c9b6-c04a-1c37-e226306a2dda.png)


サンプルコードの全体は、こちらで公開しています。
https://github.com/heptaliane/tauri-yew-events-sample

# セットアップ
まずは Tauri + Yew の構成を作成します。
create-tauri-app を使うことで、簡単に基本的な構成を作ることができます。

``` bash
cargo create-tauri-app sample-app --template yew --manager cargo
```

# Tauri 側のコードの作成
まずは Tauri 側のコードを実装します。

注意すべき点としては、Yew の起動前に emit してしまうと、Yew が Event を取得できません。
そのため今回は、Yew からの emit をトリガーに Tauri が Yew に emit を行うように実装します。
~~(それって実質 command と変わらないのでは)~~

イベント名としては、以下を設定することにします。
 * Tauri -> Yew: `back-to-front`
 * Yew -> Tauri: `front-to-back`

``` rust
use tauri::Manager;

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            let app_handle = app.app_handle();
            // Yew -> Tauri のメッセージを待ち受け
            let _  = app.listen_global("front-to-back", move |_| {
                // Tauri -> Yew に メッセージを送信
                app_handle
                    .emit_all("back-to-front", "tauri-yew-events-sample".to_string())
                    .unwrap();
            });
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

# Yew 側の実装

## UI の作成
`src/app.rs` を編集し、文字列を表示する UI を作ります。
``` rust
use yew::prelude::*;

#[function_component(App)]
pub fn app() -> Html {
    let msg = use_state(|| String::new());

    html! {
        <span>{(*msg).clone()}</span>
    }
}
```

## @tauri-apps/api/event の bind
[Events](https://tauri.app/v1/guides/features/events/) のドキュメントを読むと、js では[@tauri-apps/api/event](https://tauri.app/v1/api/js/event/#functions) の関数を使用しています。
これを Yew でも扱えるようにするため、 wasm-bindgen で listen と emit を import します。
``` rust
use wasm_binggen::prelude::*;

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "event"])]
    async fn listen(event: &str, handler: &Closure<dyn FnMut(JsValue)>) -> JsValue;

    #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "event"])]
    async fn emit(event: &str, payload: JsValue);
}
```

## Yew -> Tauri イベントの emit
Tauri へ `front-to-back` イベントを emit します。
今回は Tauri 側で payload を使用しないため、メッセージに `null` を入れることにしましょう。

[use_effect_with_deps in yew::functional - Rust](https://docs.rs/yew/0.20.0/yew/functional/fn.use_effect_with_deps.html#only-on-first-render) を参考に、
component の初回作成時のみで、`front-to-back` イベントが発生するようにしておきます。
``` rust
use wasm_bindgen_futures::spawn_local;

#[function_component(App)]
pub fn app() -> Html {
    ...

    use_effect_with_deps(
        move |_| {
            spawn_local(async move {
                emit("front-to-back", JsValue::NULL).await;
            });
        },
        (),
    );

    ...
}
```

## Tauri -> Yew イベントの listen
Tauri からの `back-to-front` イベントを listen します。

### Event オブジェクト の定義
Tauri からのメッセージは、[Event<T>](https://tauri.app/v1/api/js/event/#eventt) オブジェクトとして送信されます。
これを Yew で扱うため、オブジェクトの `JsValue` を deserialize するための struct を定義します。
今回は必要な `payload` だけを定義しておきます。
``` rust
use serde::Deserialize;

#[derive(Deserialize)]
struct Payload {
    payload: String,
}
```

### `back-to-front` イベントの listen
まずは空のイベントハンドラを使ってイベントを listen します。
``` rust
#[function_component(App)]
pub fn app() -> Html {
    let msg = use_state(|| String::new());

    spawn_local(async move {
        let handler = Closure::<dyn FnMut(JsValue)>::new(move |_event: JsValue| {});
        let _ = listen("back-to-front", &handler).await;
        handler.forget();
    });
    ...
}
```

続いてイベントハンドラ中で、 `Event` オブジェクトを deserialize します。
``` rust
use serde_wasm_bindgen::from_value;

#[function_component(App)]
pub fn app() -> Html {
    let msg = use_state(|| String::new());

    spawn_local(async move {
        let handler = Closure::<dyn FnMut(JsValue)>::new(move |event: JsValue| {
            if let OK(event) = from_value::<Payload>(payload) {}
        });
        let _ = listen("back-to-front", &handler).await;
        handler.forget();
    });
    ...
}
```

`event` の値で `msg` の値を更新します。
``` rust
use serde_wasm_bindgen::from_value;

#[function_component(App)]
pub fn app() -> Html {
    let msg = use_state(|| String::new());

    {
        let msg = msg.clone();
        spawn_local(async move {
            let handler = Closure::<dyn FnMut(JsValue)>::new(move |payload: JsValue| {
                if let Ok(res) = from_value::<Payload>(payload) {
                    msg.set(res.payload);
                }
            });
            let _ = listen("back-to-front", &handler).await;
            handler.forget();
        });
    }
    ...
}
```

# アプリケーションのビルド
`tauri.conf.json` を編集し、 [identifier](https://tauri.app/v1/api/config#bundleconfig.identifier) を適当な値に設定しておきましょう。

``` json
{
  "tauri": {
    "bundle": {
      "identifier": "tauri-yew-events-sample"
      }
    }
  }
}
```

`tauri build` コマンドで build すると、`target/release/` 配下にアプリケーションが生成されます。
``` bash
cargo tauri build
```

# まとめ
Yew で Tauri の Events を使う方法を紹介しました。

wasm_bindgen を使って [@tauri-apps/api/event](https://tauri.app/v1/api/js/event/#functions) をバインドすることで、Yew で listen や emit を使うことができます。