TauriとLeptosで作るデスクトップアプリ(2)カウンタコンポーネントを作成する
はじめに
TauriとLeptosでデスクトップアプリを作っていきます。今回は、シンプルなカウンタを作成します。
関連記事
環境
- Windows 11 Home
- Rust 1.80
- Tauri 2.0.0-rc
- Leptos 0.7.0-beta
コンポーネントライブラリThaw UIを導入する
Leptosのコンポーネントライブラリはいくつかありますが、個人的にデザインが気に入っているThaw UIを使います。
./apps/src-ui/Cargo.toml
の[dependencies]
にthaw
を追加します。
# 略
[dependencies]
console_error_panic_hook = "0.1.7"
leptos = { version = "0.7.0-beta2", features = ["csr"] }
thaw = { version = "0.4.0-beta", features = ["csr"] }
VS Codeにフォーマッタを導入する
Leptosではview!
マクロを用いてRustソースコードの中にHTMLを書いていきます。VS Codeでソースコードを編集するなら、フォーマッタleptosfmt
を導入すると便利です。
leptosfmt
をインストールします。
> cargo install leptosfmt
以下の2つのファイルを新規作成します。
{
"rust-analyzer.rustfmt.overrideCommand": ["leptosfmt", "--stdin", "--rustfmt"]
}
edition = "2021"
カウンタコンポーネントを作成する
Leptosフロントエンドクレート./apps/src-ui
にコンポーネントをまとめたモジュールcomponents
を作ります。その中にサブモジュールcounter
を作ります。ディレクトリ構成は以下のとおりです。
tauri-leptos-example
├─ apps/
│ ├─ src-tauri/
│ └─ src-ui/
│ ├─ src/
│ │ ├─ components/
│ │ │ ├─ counter.rs
│ │ │ └─ mod.rs
│ │ ├─ app.rs
│ │ ├─ lib.rs
: : └─ main.rs
mod.rs
は単純にサブモジュールcounter
を宣言するだけです。
pub mod counter;
counter.rs
にカウンタを実装します。Leptos公式ページの例を参考にしました。
use leptos::prelude::*;
use thaw::{Button, Tag};
#[component]
pub fn SimpleCounter(
/// The starting value for the counter
initial_value: i32,
/// The change that should be applied each time the button is clicked.
step: i32,
) -> impl IntoView {
let value = RwSignal::new(initial_value);
let dec = move |_| *value.write() -= step;
let inc = move |_| value.update(|value| *value += step);
view! {
<div>
<Button on_click=dec>"-1"</Button>
<Tag>"Value: " {value} "!"</Tag>
<Button on_click=inc>"+1"</Button>
<Button on_click=move |_| value.set(0)>"Clear"</Button>
</div>
}
}
<Button>
コンポーネントのon_click
属性でボタンがクリックされたときの動作を指定します。RwSignal
のメソッドを使っています。[+1]ボタンと[-1]ボタンであえて異なった記述をしています。[Clear]ボタンではクロージャをview!
マクロの中に直に書いています。
SimpleCounter
コンポーネントをアプリのApp
コンポーネント内に配置します。
use leptos::prelude::*;
use thaw::ConfigProvider;
use crate::components::counter::SimpleCounter;
#[component]
pub fn App() -> impl IntoView {
view! {
<ConfigProvider>
<SimpleCounter initial_value=0 step=1 />
</ConfigProvider>
}
}
ConfigProvider
コンポーネントで全体を囲うことで、中のコンポーネントにThaw UIのスタイルが適用されます。SimpleCounter
コンポーネントは属性を2つ取ります。コンポーネントの定義における引数が、そのまま属性となります。初期値0、ステップ1を指定しています。
ライブラリクレートのクレートルートlib.rs
に、モジュールcomponents
の宣言を追加します。
use app::App;
use leptos::prelude::*;
mod app;
pub mod components;
pub fn run() {
console_error_panic_hook::set_once();
mount_to_body(App)
}
準備が整いました。プロジェクトを実行します。
> cargo tauri dev
カウンタがThaw UIライブラリのスタイルで表示され、ボタンが期待通りに動作すれば完成です。
カウンタコンポーネントをテストする
wasm-packを使ってカウンタコンポーネントの動作をテストします。
テストコードはLeptos公式サイトを参考にしました。
まず、./apps/src-ui/Cargo.toml
の[dev-dependencies]
にテスト用のパッケージを追加します。
# 略
[dev-dependencies]
wasm-bindgen = "0.2.93"
wasm-bindgen-test = "0.3.43"
[dev-dependencies.web-sys]
features = ["HtmlElement", "XPathResult"]
version = "0.3.70"
テスト用ディレクトリtests
を作成します。
tauri-leptos-example
├─ apps/
│ ├─ src-tauri/
│ └─ src-ui/
│ ├─ src/
│ ├─ tests/
│ │ ├─ common/
│ │ │ └─ mod.rs
: : └─ counter.rs
他のコンポーネントでも使う可能性のあるコードをcommon
モジュールにまとめておきます。
use leptos::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::HtmlElement;
pub fn find_by_text(text: &str) -> HtmlElement {
let xpath = format!("//*[text()='{}']", text);
let document = document();
document
.evaluate(&xpath, &document)
.unwrap()
.iterate_next()
.unwrap()
.unwrap()
.dyn_into::<HtmlElement>()
.unwrap()
}
pub fn click_text(text: &str) {
find_by_text(text).click();
}
カウンタコンポーネント用のテストコードは以下になります。
use leptos::{prelude::*, spawn::tick};
use wasm_bindgen_test::*;
use src_ui_lib::components::counter::SimpleCounter;
mod common;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
async fn increment() {
open_counter();
common::click_text("+1");
common::click_text("+1");
tick().await;
assert_eq!(get_text(), Some("Value: 12!".to_string()));
}
#[wasm_bindgen_test]
async fn decrement() {
open_counter();
common::click_text("-1");
common::click_text("-1");
tick().await;
assert_eq!(get_text(), Some("Value: 8!".to_string()));
}
#[wasm_bindgen_test]
async fn clear() {
open_counter();
common::click_text("Clear");
tick().await;
assert_eq!(get_text(), Some("Value: 0!".to_string()));
}
fn open_counter() {
if let Some(top_div) = document().query_selector("body div").unwrap() {
top_div.remove();
}
mount_to_body(move || view! { <SimpleCounter initial_value=10 step=1 /> });
}
fn get_text() -> Option<String> {
common::find_by_text("Value: ").text_content()
}
wasm-packをインストールします。
> cargo install wasm-pack
テストを実行します。
> wasm-pack test --chrome apps/src-ui --test counter
--chrome
オプションを指定することによって、Chromeでテストを行います。Chromeがインストールされている必要があります。他には--node
, --firefox
, --safari
が選択できるようです。apps/src-ui
はテストしたいクレートのディレクトリです。--test
オプションでテスト対象を選びます。今回はコンポーネントが1つしかないので必要ありませんが、複数あるときには指定が必要です。cargo test
コマンドと違って、1つのテストファイルしかテストが実行されないためです。(一度に複数のテストをする方法があるかもしれません。)
テストを実行してから、ブラウザでhttp://127.0.0.1:8000
を開きます。下のようにテストにパスしたら成功です。
--headless
オプションを使うことで、ブラウザを開くことなくテスト結果を確認できます。また、このときにはtests
ディレクトリにある全ファイルを一度にテストできるため、--test
を指定することは必須ではありません。
> wasm-pack test --chrome --headless apps/src-ui
[INFO]: Checking for the Wasm target...
Compiling src-ui v0.2.0 (...)
:
test result: ok. 3 passed; ...
サンプルコード
今回作成したプロジェクトは以下からダウンロード可能です。
Discussion