RustのGUIアプリ開発:Tauri・egui・Slint・Iced
ターミナルに文字を出すだけでなく、ボタンのある本物のウィンドウを作りたくなる日がいつか来ます。それはRustでもできるのでしょうか? 答えは「はい」です。RustのGUIアプリは速く、小さく、安全で、今ではよいツールキットがいくつもあります。ただ、RustのGUIの世界は、たとえばC#のWPFやJavaScriptのElectronと比べるとまだ若いので、選ぶ前に全体の様子を知っておくと安心です。
このページでは、正直な現状の紹介、選ぶときに役立つ比較表、イミディエイトモード(immediate mode)とリテインドモード(retained mode)の考え方、そして人気のツールキットで書く最初の小さなプログラムを紹介します。
コードはどこで動かせる? GUIには本物のウィンドウが必要なので、egui、Tauri、Slint、Icedのプログラムは自分のパソコンで cargo run して動かすためのものです。このページのeguiとIcedのプログラムは、最新のバージョンに対して cargo check でコンパイルを確認しています。また、2つのセクションでは、ウィンドウを描く代わりに文字を出力するふつうのRustでGUIを再現しているので、ブラウザでそのまま動かせます。「▶ 実際に試してみよう」ボタンを探してください。
RustのGUIの正直な現状
Section titled “RustのGUIの正直な現状”コミュニティのサイトAre we GUI yet?(英語)は、現状を「The roots aren’t deep but the seeds are planted.」(根はまだ浅いけれど、種はまかれている)という一文でまとめています。今でもこれは的を射ています。
- すばらしい点: 速さ、アプリの小ささ、そしてRustの安全性です。egui、Iced、Slintはすべてを自分で描画するので、ひとつのコードでWindows、macOS、Linuxのどれでも動きます。TauriならWebの知識をそのまま使えて、しかもシステムにもともと入っているWebViewを使うので、Electronよりアプリがずっと小さくなります。
- まだ成長中の点: C++のQtやC#のWPFのような、ひとつに決まった「標準」のツールキットはありません。人気のクレートの中にもまだバージョン0.xのものがいくつかあり、アップデートでコードが動かなくなることがあります。アクセシビリティ、右から左に書く言語への対応、OSらしい見た目(ネイティブなルック&フィール)は、ツールキットごとにばらばらです。
- よいニュース: 完璧なツールキットを探す必要はありません。最初のアプリなら、下の表のどれを選んでもうまくいきます。
もっと詳しく比べたいなら、2025 survey of Rust GUI libraries(英語)がたくさんのツールキットをひとつずつ試しています。
どんなものが作れる?
Section titled “どんなものが作れる?”今すぐ見られる実際のアプリを紹介します。
- Rerun Viewerは、ロボットやコンピュータビジョンのデータを見るためのツールで、eguiで作られています(Rerunはeguiの開発のスポンサーでもあります)。
- System76が作っているLinux向けのデスクトップ環境COSMICは、Icedで作られています。IRCチャットクライアントのHalloyもIcedです。
- Webのような見た目のデスクトップツール:メモアプリ、ダッシュボード、ちょっとした業務ツールはTauriとよく合います。
- 機器の画面:Slintはマイコンの上でも動くので、機械や家電のタッチパネルにぴったりです。
どのクレートを選ぶ?
Section titled “どのクレートを選ぶ?”バージョンは2026年10月時点の最新リリースです。
| クレート | バージョン | UIの書き方 | 動く環境 | こんなときに選ぶ |
|---|---|---|---|---|
| Tauri | 2.12(3.0はアルファ版) | 画面はHTML・CSS・JavaScript、ロジックはRust | Windows、macOS、Linux、Android、iOS | Web開発の経験がある、または凝ったデザインの画面を作りたい |
| egui + eframe | 0.37 | ふつうのRust、イミディエイトモード | Windows、macOS、Linux、Web(Wasm) | ツール、デバッグ用のパネル、エディタ。純粋なRustでいちばん早く始めたい |
| Slint | 1.18 | 独自の .slint マークアップ+ロジックはRust |
デスクトップ、組み込み、モバイル、Web | 見た目の整ったアプリ、または組み込み機器の画面 |
| Iced | 0.14 | ふつうのRust、Elmアーキテクチャ(状態、メッセージ、update、view) | Windows、macOS、Linux、Web | すべての変更が1か所に集まる、すっきりして予測しやすいコードが好き |
| gtk-rs(gtk4) | 0.11 | GTK 4へのRustバインディング、リテインドモード | まずLinux。WindowsとmacOSは追加の準備をすれば動く | LinuxのGNOMEデスクトップになじむアプリを作りたい |
| Dioxus | 0.7 | rsx! マクロを使うReact風のコンポーネント |
Web、デスクトップ(WebView)、モバイル | Reactを知っていて、Webとデスクトップをひとつのコードで作りたい |
ライセンスと成熟度について、正直に補足しておきます。
- SlintはGPLv3なら無料です。また、ロイヤリティフリーのライセンスを使えば、クローズドソースのデスクトップ、モバイル、Webのアプリも無料で作れます(Slintを使っていることを表示する必要があります)。クローズドソースの組み込み製品には有料ライセンスが必要です。リリースする前にslint.dev/pricing(英語)を読んでください。
- egui、Iced、Dioxusはまだ0.xです。リリースのたびに小さな破壊的変更があると思っておき、チュートリアルが自分のバージョンに合っているか確かめましょう。たとえば、古いeguiのチュートリアルは
fn update(&mut self, ctx: &egui::Context, ...)を使っていますが、eframe 0.37ではfn ui(&mut self, ui: &mut egui::Ui, ...)です。 - Tauri、Dioxusのデスクトップ版、gtk-rsはシステムのライブラリに依存します。下のプラットフォームごとの準備を見てください。
イミディエイトモードとリテインドモード
Section titled “イミディエイトモードとリテインドモード”ツールキットの考え方は、ひとつの大きな問いで分かれます。「ウィジェットを覚えておくのは誰か?」です。
リテインドモード(gtk-rs、Slint、TauriのWebページ、そしてほかの言語のほとんどのツールキット)では、ボタンを一度だけ作ります。ツールキットはそれをウィジェットのツリーの中に保持します。コールバックを登録しておくと、ユーザーがクリックしたときにツールキットがそのコールバックを呼びます。あとでボタンの文字を変えたいときは、そのボタンへのハンドルが必要です。
イミディエイトモード(egui)では、何も保持されません。UIの関数がフレームごとに毎回実行され、「今この瞬間に何を表示するか」を書くだけです。ボタンの描画とクリックの確認は1行で済みます:if ui.button("+").clicked() { ... }。コールバックもハンドルもありません。アプリの状態は自分の構造体の中にあり、UIはいつもそこから描かれるので、画面とデータがずれることはありません。
その代わり、ツールキットはフレームごとにUI全体のレイアウトを計算するので、巨大なUIではCPUの時間がかかりますし、一部のレイアウト(たとえば、まだ大きさがわからないウィンドウを中央に置く)は難しくなります。eguiはマウスが動いたときのように何かが起きたときだけ新しいフレームを描くので、何もしていないアプリはCPUをほとんど使いません。
ふつうのRustで書いた、ごく小さな「なんちゃってイミディエイトモード」のツールキットを見てみましょう。ループの1回が1フレームです。配列 clicks がマウスの役をします。
// このフレームで何が描かれたかを記録する、なんちゃって「Ui」struct Ui { clicked: Option<&'static str>, // このフレームでマウスがクリックしたボタン drawn: Vec<String>,}
impl Ui { fn label(&mut self, text: &str) { self.drawn.push(text.to_string()); }
// ボタンを描き、さらにクリックされたかどうかをその場で返す fn button(&mut self, text: &'static str) -> bool { self.drawn.push(format!("[{text}]")); self.clicked == Some(text) }}
// あなたのUIのコード:フレームごとに毎回呼ばれるfn draw(count: &mut i32, ui: &mut Ui) { ui.label(&format!("Count: {count}")); if ui.button("+") { *count += 1; } if ui.button("Reset") { *count = 0; }}
fn main() { let mut count = 0; // アプリの状態はUIのコードの外にある let clicks = [None, Some("+"), Some("+"), None, Some("Reset"), None];
for (frame, click) in clicks.into_iter().enumerate() { let mut ui = Ui { clicked: click, drawn: Vec::new() }; draw(&mut count, &mut ui); println!("frame {frame}: {}", ui.drawn.join(" ")); }}実行結果:
frame 0: Count: 0 [+] [Reset]frame 1: Count: 0 [+] [Reset]frame 2: Count: 1 [+] [Reset]frame 3: Count: 2 [+] [Reset]frame 4: Count: 2 [+] [Reset]frame 5: Count: 0 [+] [Reset]フレーム1を見てください。「+」をクリックしたのに、ラベルはまだ0のままです。ボタンがクリックに気づく前に、ラベルが描かれてしまったからです。新しい値は1フレーム遅れて表示されます。本物のeguiはこれをうまく処理してくれます。クリックやキー入力があるとすぐに次のフレームを描くので、古い数字が目に入ることはありません。
はじめてのeguiアプリ:カウンター
Section titled “はじめてのeguiアプリ:カウンター”eframeはeguiの公式の「アプリフレームワーク」で、ウィンドウを開き、その中でeguiを動かします。プロジェクトを作って追加しましょう。
cargo new countercd counter[dependencies]eframe = "0.37"src/main.rs を次の内容に置き換えます。
use eframe::egui;
fn main() -> eframe::Result { let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default().with_inner_size([300.0, 160.0]), ..Default::default() }; eframe::run_native( "Counter", options, Box::new(|_cc| Ok(Box::<CounterApp>::default())), )}
#[derive(Default)]struct CounterApp { count: i32,}
impl eframe::App for CounterApp { fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { egui::CentralPanel::default().show(ui, |ui| { ui.heading(format!("Count: {}", self.count)); ui.horizontal(|ui| { if ui.button("-").clicked() { self.count -= 1; } if ui.button("+").clicked() { self.count += 1; } if ui.button("Reset").clicked() { self.count = 0; } }); }); }}cargo run で実行します。最初のビルドでは数百個のクレートをダウンロードしてコンパイルするので、数分かかります。2回目からは速くなります。「Count: 0」と3つのボタンがある小さなウィンドウが開きます。
各部分の役割はこうです。
CounterAppがあなたの状態です。#[derive(Default)]によってcountは0から始まります。run_nativeがウィンドウを開きます。クロージャはアプリをBoxの中に一度だけ作ります(スマートポインタを参照)。- eframeはフレームごとに
uiを呼びます。CentralPanelがウィンドウ全体を埋め、ui.horizontalが3つのボタンを横に並べます。 - それぞれの
if ui.button(..).clicked()は、上のセクションで見たイミディエイトモードのパターンです。ボタンを描き、確認して、状態を変えます。
状態を必ず self に置き、ui の中のローカル変数に置いてはいけない理由もここにあります。そこに let mut count = 0; と書くと、フレームのたびに0からやり直しになってしまいます。
Tauriのコマンド:Webページの裏で動くRust
Section titled “Tauriのコマンド:Webページの裏で動くRust”Tauriのアプリは2つの部分からできています。フロントエンドは、システムのWebViewで動くふつうのWebページです(HTML、CSS、JavaScript、またはReactやSvelteのようなフレームワーク)。バックエンドはあなたのRustのコードです。フロントエンドは、コマンド(command)と呼ばれるRustの関数を呼び出します。
いちばん簡単な始め方は、公式のプロジェクト生成ツールを使うことです。名前とフロントエンドのテンプレートを聞かれます(フレームワークを使わない「vanilla」テンプレートが、最初の選択としておすすめです)。
cargo install create-tauri-app --lockedcargo create-tauri-appすでにNode.jsを使っているなら npm create tauri-app@latest もあります。作られたフォルダに移動したら、npmのテンプレートなら npm install と npm run tauri dev、Rustだけのテンプレートなら cargo install tauri-cli --version "^2.0.0" --locked と cargo tauri dev でアプリを起動します。どのコマンドを使えばいいかは、ツールが選んだ内容に合わせて表示してくれます。
生成された src-tauri/src/lib.rs には、最初のコマンドが入っています。
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/#[tauri::command]fn greet(name: &str) -> String { format!("Hello, {}! You've been greeted from Rust!", name)}
#[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .invoke_handler(tauri::generate_handler![greet]) .run(tauri::generate_context!()) .expect("error while running tauri application");}そしてフロントエンド(vanillaテンプレートの src/main.js。ここでは短くしています)からは、次のように呼び出します。
const { invoke } = window.__TAURI__.core;
greetMsgEl.textContent = await invoke("greet", { name: greetInputEl.value });何が起きているのかを見てみましょう。
#[tauri::command]は属性マクロです(手続き的マクロを参照)。JavaScriptからの呼び出しをふつうのRustの関数呼び出しに変える、つなぎのコードを書いてくれます。- 引数はJSONで届くので、引数の型はそれぞれserdeの
Deserializeを、戻り値の型はSerializeを実装している必要があります(serdeでJSONを扱うを参照)。JavaScript側では引数名をキャメルケースで書きます。Rustの引数invoke_messageは{ invokeMessage: "..." }になります。 generate_handler![greet]がコマンドを登録します。新しいコマンドをここに追加し忘れると、JavaScriptからの呼び出しは失敗します。- エラーを伝えるには
Resultを返します。OkならJavaScriptのPromiseが解決(resolve)され、Errなら拒否(reject)されるので、.catch()で処理できます。たとえばfn load(path: String) -> Result<String, String>のように書きます。 main.rsはrun()を呼ぶだけです。同じアプリをAndroidやiOS向けにもビルドできるように、コードはlib.rsに置かれています。
Slint:UIは専用のマークアップ言語で
Section titled “Slint:UIは専用のマークアップ言語で”Slintは、HTMLとCSSを合わせたような小さな宣言的な言語で画面を記述します。ロジックはRustのコードが担当します。公式のhello worldを見てみましょう。[dependencies] に slint = "1.18" を追加して、src/main.rs に次のコードを書きます。
slint::slint!{ export component HelloWorld { Text { text: "hello world"; color: green; } }}
fn main() { HelloWorld::new().unwrap().run().unwrap();}slint! マクロはビルドのときにマークアップをコンパイルし、Rustの型 HelloWorld を作ってくれます。本格的なアプリでは、マークアップは独立した .slint ファイルに書きます。次のカウンターは、公式のSlint Rustテンプレート(英語)からのものです。ファイル ui/app-window.slint はこうなります。
import { Button, VerticalBox } from "std-widgets.slint";
export component AppWindow inherits Window { in-out property <int> counter: 42; callback request-increase-value(); VerticalBox { Text { text: "Counter: \{root.counter}"; }
Button { text: "Increase value"; clicked => { root.request-increase-value(); } } }}そして src/main.rs のRust側です。
slint::include_modules!();
fn main() -> Result<(), Box<dyn std::error::Error>> { let ui = AppWindow::new()?;
let ui_handle = ui.as_weak(); ui.on_request_increase_value(move || { let ui = ui_handle.unwrap(); ui.set_counter(ui.get_counter() + 1); });
ui.run()?; Ok(())}Slintはリテインドモードです。counter プロパティはUIの中にあり、ボタンはコールバックを呼びます。Slintはマークアップから get_counter、set_counter、on_request_increase_value を生成します(ダッシュはアンダースコアになります)。クロージャが弱い(weak)ハンドル(as_weak())を持っているのは、ウィンドウと、そのウィンドウ自身のコールバックが、お互いを永遠に生かし続けないようにするためです(WeakとCowを参照)。.slint ファイルを使う場合は、さらに [build-dependencies] に slint-build = "1.18" を追加し、1行だけの build.rs を用意します:slint_build::compile("ui/app-window.slint").expect("Slint build failed");。
IcedとElmアーキテクチャ
Section titled “IcedとElmアーキテクチャ”Icedは、Elmという言語にちなんで名付けられたElmアーキテクチャに従います。どのアプリも4つの部分からできています。
- 状態(State): アプリが覚えておくすべてを持つ構造体。
- メッセージ(Messages): 起こりうるすべてのことを並べた列挙型(enum)(「+ボタンが押された」など)。
- update: 状態とメッセージを1つ受け取り、状態を変えます。変更が起きるのはここだけです。
- view: 状態からウィジェットを組み立てます。ウィジェット自身は何も変えません。メッセージを送るだけです。
公式のIcedのカウンターを見てみましょう([dependencies] に iced = "0.14" を追加します)。
use iced::widget::{button, column, text, Column};use iced::Center;
fn main() -> iced::Result { iced::run(Counter::update, Counter::view)}
#[derive(Default)]struct Counter { value: i64,}
#[derive(Debug, Clone, Copy)]enum Message { Increment, Decrement,}
impl Counter { fn update(&mut self, message: Message) { match message { Message::Increment => self.value += 1, Message::Decrement => self.value -= 1, } }
fn view(&self) -> Column<'_, Message> { column![ button("Increment").on_press(Message::Increment), text(self.value).size(50), button("Decrement").on_press(Message::Decrement), ] .padding(20) .align_x(Center) }}on_press(Message::Increment) は、何のコードも実行しません。「クリックされたら、このメッセージを送ってね」と言っているだけです。するとIcedがそのメッセージで update を呼び、新しい状態を表示するためにもう一度 view を呼びます。すべての変更がひとつの match を通るので、アプリにできることを全部見渡す(そしてテストする)のが簡単です。列挙型とmatchのよい活用例です。
Elmアーキテクチャをブラウザで試す
Section titled “Elmアーキテクチャをブラウザで試す”この考え方を練習するのに、Icedは必要ありません。次のTODOアプリのモデルにも、同じ4つの部分があります。view はウィジェットの代わりに文字列を返し、ユーザーのキー入力やクリックの代わりにメッセージのリストを使います。
// 1. 状態:アプリが覚えておく必要があるすべて#[derive(Default)]struct App { draft: String, todos: Vec<Todo>,}
struct Todo { title: String, done: bool,}
// 2. メッセージ:起こりうるすべてのこと#[derive(Debug)]enum Message { DraftChanged(String), AddPressed, Toggle(usize), ClearDone,}
impl App { // 3. update:状態が変わる「唯一の」場所 fn update(&mut self, message: Message) { match message { Message::DraftChanged(text) => self.draft = text, Message::AddPressed => { let title = self.draft.trim(); if !title.is_empty() { self.todos.push(Todo { title: title.to_string(), done: false }); } self.draft.clear(); } Message::Toggle(index) => { if let Some(todo) = self.todos.get_mut(index) { todo.done = !todo.done; } } Message::ClearDone => self.todos.retain(|todo| !todo.done), } }
// 4. view:状態をユーザーに見えるもの(ここでは文字列)に変える fn view(&self) -> String { let mut screen = format!("New: [{}]\n", self.draft); for (index, todo) in self.todos.iter().enumerate() { let mark = if todo.done { "x" } else { " " }; screen.push_str(&format!("{index}. [{mark}] {}\n", todo.title)); } screen }}
fn main() { let mut app = App::default();
// キー入力やボタンのクリックから届いたつもりのメッセージ let messages = vec![ Message::DraftChanged("牛乳を買う".to_string()), Message::AddPressed, Message::DraftChanged("銀行に電話する".to_string()), Message::AddPressed, Message::DraftChanged("植物に水をやる".to_string()), Message::Toggle(0), ];
for message in messages { println!("> {message:?}"); app.update(message); } print!("{}", app.view());
app.update(Message::ClearDone); println!("> after ClearDone:"); print!("{}", app.view());}実行結果:
> DraftChanged("牛乳を買う")> AddPressed> DraftChanged("銀行に電話する")> AddPressed> DraftChanged("植物に水をやる")> Toggle(0)New: [植物に水をやる]0. [x] 牛乳を買う1. [ ] 銀行に電話する> after ClearDone:New: [植物に水をやる]0. [ ] 銀行に電話するview は何も変更せず、update は何も出力しないことに注目してください。この役割分担こそが、このしくみのポイントです。これを本物のIcedアプリにするには、view をウィジェットを返すように書き直すだけです(DraftChanged を送る text_input や、AddPressed を送る button など)。状態、メッセージ、update はまったく同じままで大丈夫です。
プラットフォームごとの準備
Section titled “プラットフォームごとの準備”GUIのクレートはOSとやり取りするので、ふつうのRustより少し多くの準備が必要です。まずRustのツールと環境構築で基本をそろえてください。そのうえで、次のものを用意します。
| OS | egui/eframe、Iced、Slint | Tauriでさらに必要なもの |
|---|---|---|
| Windows | MSVCのC++ツール以外は不要 | WebView2。Windows 10(バージョン1803以降)とWindows 11には最初から入っています |
| macOS | Xcodeのコマンドラインツール(xcode-select --install) |
デスクトップアプリならほかには不要。iOS向けには完全版のXcode |
| Linux(Debian/Ubuntu) | eframeのREADMEには libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev が挙がっています |
webkit2gtk:libwebkit2gtk-4.1-dev と、Tauriの前提条件のページ(英語)にあるいくつかのパッケージ |
Linuxでのビルドが、ライブラリが見つからないという pkg-config のメッセージで失敗したら、そのライブラリの -dev パッケージをインストールしてからビルドし直してください。gtk-rsにはGTK 4の開発用ファイル(Ubuntuなら libgtk-4-dev)が必要です。
よくある間違い
Section titled “よくある間違い”- UIのスレッドを止めてしまう。 eguiの
ui関数やIcedのupdateの中でダウンロードや長い計算をすると、終わるまでウィンドウ全体が固まります。時間のかかる処理は別のスレッドかasyncのタスクで行い(並行処理とasync/awaitを参照)、結果はメッセージとしてUIに送り返しましょう。 - eguiの状態をローカル変数に置く。
uiはフレームごとに毎回実行されるので、その中のlet mutは毎回まっさらな状態から始まります。状態はアプリの構造体に置きましょう。 - Tauriのコマンドを登録し忘れる。 すべてのコマンドは
tauri::generate_handler![...]に書く必要があります。また、lib.rsの中ではコマンドにpubを付けないでください。Tauriのドキュメントによると、付けるとE0255の「defined multiple times」(複数回定義されている)エラーになります。 - 古いチュートリアルどおりに書く。 0.xのクレートでは、去年の例がコンパイルできないことがあります。チュートリアルのバージョンと自分の
Cargo.tomlを見比べ、使っているバージョンに合ったクレート自身のexamplesフォルダを見ましょう。
1. 0より小さくならないカウンター
Section titled “1. 0より小さくならないカウンター”Increment、Decrement、Reset のメッセージを持つ、Elm風のカウンターを書いてください。ユーザーが「-」を押し続けても、値は決して0より小さくなってはいけません。メッセージのリストを update に順番に通し、そのたびに view() を出力しましょう。
ヒント: u32 と saturating_sub(1) を使いましょう。オーバーフローせずに0で止まってくれます(整数オーバーフローを参照)。
解答を見る
#[derive(Default)]struct Counter { value: u32,}
#[derive(Debug)]enum Message { Increment, Decrement, Reset,}
impl Counter { fn update(&mut self, message: Message) { match message { Message::Increment => self.value += 1, Message::Decrement => self.value = self.value.saturating_sub(1), Message::Reset => self.value = 0, } }
fn view(&self) -> String { format!("Count: {}", self.value) }}
fn main() { let mut counter = Counter::default(); let messages = [ Message::Increment, Message::Increment, Message::Decrement, Message::Decrement, Message::Decrement, Message::Increment, Message::Reset, ];
for message in messages { let name = format!("{message:?}"); counter.update(message); println!("{name:<9} -> {}", counter.view()); }}実行結果:
Increment -> Count: 1Increment -> Count: 2Decrement -> Count: 1Decrement -> Count: 0Decrement -> Count: 0Increment -> Count: 1Reset -> Count: 0「0より小さくならない」というルールは、update というたった1か所にあります。メッセージの名前を update を呼ぶ前に文字列にしているのは、update がメッセージを値で受け取って、ムーブしてしまうからです。
2. 元に戻す(Undo)ボタンを追加する
Section titled “2. 元に戻す(Undo)ボタンを追加する”カウンターに Undo メッセージを追加してください。変更のたびに古い値を覚えておき、Undo で1つ前に戻します。履歴がないときの Undo は何もしません。あと何回元に戻せるかを view に表示しましょう。
ヒント: 状態に history: Vec<u32> を持たせます。変更の前に古い値を push し、Undo のときに pop します。
解答を見る
#[derive(Default)]struct Counter { value: u32, history: Vec<u32>, // 以前の値。最新のものが末尾}
#[derive(Debug)]enum Message { Increment, Decrement, Reset, Undo,}
impl Counter { fn update(&mut self, message: Message) { let new_value = match message { Message::Increment => self.value + 1, Message::Decrement => self.value.saturating_sub(1), Message::Reset => 0, Message::Undo => { if let Some(previous) = self.history.pop() { self.value = previous; } return; } }; self.history.push(self.value); // まず古い値を覚えておく self.value = new_value; }
fn view(&self) -> String { format!("Count: {} (undo steps: {})", self.value, self.history.len()) }}
fn main() { let mut counter = Counter::default(); let messages = [ Message::Increment, Message::Increment, Message::Decrement, Message::Increment, Message::Reset, Message::Undo, Message::Undo, Message::Undo, ];
for message in messages { let name = format!("{message:?}"); counter.update(message); println!("{name:<9} -> {}", counter.view()); }}実行結果:
Increment -> Count: 1 (undo steps: 1)Increment -> Count: 2 (undo steps: 2)Decrement -> Count: 1 (undo steps: 3)Increment -> Count: 2 (undo steps: 4)Reset -> Count: 0 (undo steps: 5)Undo -> Count: 2 (undo steps: 4)Undo -> Count: 1 (undo steps: 3)Undo -> Count: 2 (undo steps: 2)すべての変更がもともと update を通っているので、Undoは簡単に追加できました。本物のIcedアプリなら、Message::Undo を送るボタンを1つ足すだけです。
- RustのGUIは今でも十分に使えますが、標準のツールキットはまだひとつに決まっておらず、人気のクレートのいくつかはまだ0.xです。
- TauriはWebのフロントエンドとRustのバックエンドを組み合わせます。eguiはふつうのRustで書くイミディエイトモードです。Slintは独自のマークアップを使い、組み込み機器でも動きます。IcedはElmアーキテクチャを使います。
- リテインドモードはウィジェットのツリーを保持し、あなたのコールバックを呼びます。イミディエイトモードはコールバックを使わず、フレームごとにあなたの状態からUIを描き直します。
- Elmアーキテクチャはアプリを状態、メッセージ、
update、viewに分けるので、変更を追いかけたりテストしたりするのが簡単になります。 - TauriにはWindowsでWebView2、Linuxでwebkit2gtkが必要です。基本はRustのツールと環境構築で確認しましょう。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. eguiで、カウンターの値を ui の中のローカル変数ではなく、アプリの構造体に置かなければならないのはなぜですか?
2. Tauriのアプリで、#[tauri::command] は何をしますか?
3. Icedが使うElmアーキテクチャでは、アプリの状態はどこで変わりますか?
4. Linuxで、eguiには不要でTauriには必要なものは何ですか?
スコア: 0 / 4
さらに詳しく
Section titled “さらに詳しく”- Are we GUI yet?(英語):RustのGUIクレートをまとめたコミュニティのサイトです。「immediate mode」や「reactive」といったタグと、最近のニュースがあります。
- Understanding immediate mode — egui docs(英語):eguiのしくみと長所・短所を、作者自身が説明しています。
- Calling Rust from the Frontend — Tauri(英語):引数、戻り値、エラー、asyncのコマンド、状態など、コマンドのすべてがわかります。
- Slint Rust documentation(英語):RustのAPI、
slint!マクロ、そしてslint-buildで.slintファイルを使う方法。 - The Iced book: Architecture(英語):状態、メッセージ、update、viewをわかりやすく説明しています。