Aplikasi GUI di Rust: Tauri, egui, Slint, dan Iced
Cepat atau lambat kamu pasti ingin punya jendela sungguhan dengan tombol-tombol, bukan cuma teks di terminal. Bisakah itu dibuat dengan Rust? Bisa. Aplikasi GUI (antarmuka grafis) buatan Rust itu cepat, kecil, dan aman, dan sekarang sudah ada beberapa toolkit yang bagus. Tapi dunia GUI Rust masih lebih muda dibanding, misalnya, C# dengan WPF atau JavaScript dengan Electron. Jadi ada baiknya kamu mengenal peta lapangannya dulu sebelum memilih.
Halaman ini memberi gambaran yang jujur, tabel untuk membantumu memilih, ide di balik immediate mode dan retained mode, serta program kecil pertama untuk toolkit yang paling populer.
Di mana kodenya bisa dijalankan? GUI butuh jendela sungguhan, jadi program egui, Tauri, Slint, dan Iced memang dibuat untuk dijalankan di komputermu sendiri dengan cargo run. Program egui dan Iced di halaman ini sudah dicek dengan cargo check terhadap versi terbaru. Dua bagian lain memodelkan GUI dengan Rust biasa yang mencetak teks alih-alih menggambar jendela, jadi keduanya bisa jalan di browser. Cari tombol “Coba sendiri”.
Kondisi GUI di Rust saat ini, apa adanya
Section titled “Kondisi GUI di Rust saat ini, apa adanya”Situs komunitas Are we GUI yet? (bahasa Inggris) merangkumnya dalam satu kalimat: “The roots aren’t deep but the seeds are planted.” Artinya kira-kira: akarnya belum dalam, tapi benihnya sudah ditanam. Itu masih adil:
- Yang sudah bagus: kecepatan, ukuran aplikasi yang kecil, dan keamanan Rust. egui, Iced, dan Slint menggambar semuanya sendiri, jadi satu kode bisa jalan di Windows, macOS, dan Linux. Tauri memungkinkan kamu memakai kemampuan web development yang sudah kamu punya, dan aplikasinya jauh lebih kecil daripada Electron karena memakai webview yang sudah ada di sistem.
- Yang masih berkembang: belum ada satu toolkit “standar” seperti Qt di C++ atau WPF di C#. Beberapa crate populer masih di versi 0.x, jadi update bisa merusak kodemu. Dukungan aksesibilitas, teks kanan-ke-kiri, dan tampilan native berbeda-beda antar-toolkit.
- Kabar baiknya: kamu tidak perlu menemukan toolkit yang sempurna. Untuk aplikasi pertama, toolkit mana pun di tabel di bawah sudah cukup bagus.
Untuk perbandingan yang lebih panjang, survei library GUI Rust 2025 (bahasa Inggris) menguji banyak toolkit satu per satu.
Apa yang bisa kamu buat
Section titled “Apa yang bisa kamu buat”Aplikasi nyata yang bisa kamu lihat sekarang:
- Rerun Viewer, tool untuk melihat data dari robot dan computer vision, dibuat dengan egui. (Rerun juga mensponsori pengembangan egui.)
- COSMIC, desktop environment untuk Linux dari System76, dibuat dengan Iced. Begitu juga aplikasi chat IRC Halloy.
- Tool desktop dengan antarmuka bergaya web: aplikasi catatan, dashboard, dan tool bisnis kecil sering cocok dibuat dengan Tauri.
- Layar di perangkat: Slint juga berjalan di microcontroller, jadi cocok untuk layar sentuh di mesin dan peralatan rumah tangga.
Crate mana yang dipilih?
Section titled “Crate mana yang dipilih?”Versi di bawah adalah rilis terbaru per Oktober 2026.
| Crate | Versi | Cara menulis UI | Berjalan di | Pilih kalau |
|---|---|---|---|---|
| Tauri | 2.12 (3.0 masih alpha) | HTML, CSS, dan JavaScript untuk tampilan; Rust untuk logika | Windows, macOS, Linux, Android, iOS | Kamu paham web development, atau ingin antarmuka yang kaya dan penuh gaya |
| egui + eframe | 0.37 | Rust biasa, immediate mode | Windows, macOS, Linux, web (Wasm) | Tool, panel debug, editor, dan awal tercepat dengan Rust murni |
| Slint | 1.18 | Markup .slint miliknya sendiri, plus Rust untuk logika |
Desktop, embedded, mobile, web | Aplikasi yang rapi, atau layar di perangkat embedded |
| Iced | 0.14 | Rust biasa, Elm architecture (state, message, update, view) | Windows, macOS, Linux, web | Kamu suka kode yang bersih dan mudah ditebak, dengan semua perubahan di satu tempat |
| gtk-rs (gtk4) | 0.11 | Binding Rust untuk GTK 4, retained mode | Utamanya Linux; Windows dan macOS bisa dengan setup tambahan | Aplikasi bertampilan native untuk desktop GNOME di Linux |
| Dioxus | 0.7 | Komponen ala React dengan makro rsx! |
Web, desktop (webview), mobile | Kamu paham React dan ingin satu kode untuk web dan desktop |
Beberapa catatan jujur soal lisensi dan kematangan:
- Slint gratis di bawah GPLv3, atau gratis untuk aplikasi desktop, mobile, dan web yang closed-source di bawah lisensi royalty-free miliknya (kamu wajib menampilkan bahwa kamu memakai Slint). Produk embedded yang closed-source butuh lisensi berbayar. Baca slint.dev/pricing (bahasa Inggris) sebelum merilis aplikasimu.
- egui, Iced, dan Dioxus masih di 0.x. Siap-siap ada perubahan kecil yang merusak kode di setiap rilis, dan pastikan tutorial yang kamu ikuti cocok dengan versimu. Contohnya, tutorial egui lama memakai
fn update(&mut self, ctx: &egui::Context, ...), sedangkan eframe 0.37 memakaifn ui(&mut self, ui: &mut egui::Ui, ...). - Tauri, Dioxus desktop, dan gtk-rs bergantung pada library sistem. Lihat Kebutuhan tiap platform di bawah.
Immediate mode vs retained mode
Section titled “Immediate mode vs retained mode”Para toolkit tidak sepakat soal satu pertanyaan besar: siapa yang mengingat widget-nya?
Di retained mode (gtk-rs, Slint, halaman web di Tauri, dan kebanyakan toolkit di bahasa lain), kamu membuat sebuah tombol satu kali saja. Toolkit menyimpannya di dalam pohon widget. Kamu memasang callback, dan ketika pengguna mengklik, toolkit memanggil callback-mu. Kalau nanti ingin mengubah teks tombol itu, kamu butuh “pegangan” (handle) ke tombol tersebut.
Di immediate mode (egui), tidak ada yang disimpan. Fungsi UI-mu dijalankan ulang untuk setiap frame dan cukup menjelaskan apa yang harus tampil saat ini. Sebuah tombol digambar dan dicek dalam satu baris: if ui.button("+").clicked() { ... }. Tidak ada callback dan tidak ada handle. State aplikasimu tinggal di struct milikmu sendiri, dan UI selalu digambar dari situ, jadi layar tidak akan pernah berbeda dari datamu.
Harganya: toolkit menata ulang seluruh UI di setiap frame, jadi UI yang sangat besar memakan lebih banyak waktu CPU, dan beberapa tata letak (misalnya menaruh jendela di tengah ketika ukurannya belum diketahui) jadi lebih susah. egui hanya menggambar frame baru kalau ada sesuatu yang terjadi, seperti mouse bergerak, jadi aplikasi yang sedang diam hampir tidak memakai CPU.
Berikut toolkit immediate-mode pura-pura yang mungil, ditulis dengan Rust biasa. Setiap putaran loop adalah satu frame. Array clicks berperan sebagai mouse:
// "Ui" pura-pura yang mencatat apa saja yang digambar di frame ini.struct Ui { clicked: Option<&'static str>, // tombol yang diklik mouse di frame ini drawn: Vec<String>,}
impl Ui { fn label(&mut self, text: &str) { self.drawn.push(text.to_string()); }
// Menggambar tombol DAN langsung memberi tahu apakah tombol itu diklik. fn button(&mut self, text: &'static str) -> bool { self.drawn.push(format!("[{text}]")); self.clicked == Some(text) }}
// Kode UI-mu: dipanggil ulang untuk setiap frame.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; // state aplikasi tinggal di luar kode 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(" ")); }}Output:
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]Lihat frame 1: kamu mengklik “+”, tapi label masih menunjukkan 0. Label itu digambar sebelum tombolnya tahu ada klik. Nilai baru baru muncul satu frame kemudian. egui yang asli mengurus ini untukmu: setiap klik atau tombol keyboard yang ditekan membuatnya langsung menggambar frame berikutnya, jadi kamu tidak akan pernah melihat angka lama.
Aplikasi egui pertamamu: sebuah counter
Section titled “Aplikasi egui pertamamu: sebuah counter”eframe adalah “app framework” resmi egui: ia membuka jendela dan menjalankan egui di dalamnya. Buat proyek baru dan tambahkan eframe:
cargo new countercd counter[dependencies]eframe = "0.37"Ganti isi src/main.rs dengan ini:
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; } }); }); }}Jalankan dengan cargo run. Build pertama mengunduh dan meng-compile beberapa ratus crate, jadi tunggu beberapa menit. Build berikutnya cepat. Sebuah jendela kecil terbuka dengan tulisan “Count: 0” dan tiga tombol.
Begini cara semuanya tersambung:
CounterAppadalah state-mu.#[derive(Default)]membuatcountmulai dari 0.run_nativemembuka jendela. Closure (fungsi anonim) di dalamnya membuat aplikasimu satu kali, di dalam sebuahBox(lihat Smart Pointer).- eframe memanggil
uiuntuk setiap frame.CentralPanelmengisi jendela, danui.horizontalmenaruh tiga tombol dalam satu baris. - Setiap
if ui.button(..).clicked()adalah pola immediate mode dari bagian sebelumnya: gambar tombolnya, cek, lalu ubah state.
Ini juga alasan kamu harus menyimpan state di self, jangan pernah di variabel lokal di dalam ui: let mut count = 0; di sana akan mulai dari 0 lagi di setiap frame.
Command Tauri: Rust di balik halaman web
Section titled “Command Tauri: Rust di balik halaman web”Aplikasi Tauri punya dua bagian. Frontend adalah halaman web biasa (HTML, CSS, dan JavaScript, atau framework seperti React atau Svelte) yang berjalan di webview sistem. Backend adalah kode Rust-mu. Frontend memanggil fungsi Rust yang disebut command.
Cara memulai paling mudah adalah generator proyek resminya. Ia akan menanyakan nama dan template frontend (template “vanilla”, tanpa framework, adalah pilihan pertama yang bagus):
cargo install create-tauri-app --lockedcargo create-tauri-appAda juga npm create tauri-app@latest kalau kamu sudah memakai Node.js. Setelah itu masuk ke folder baru dan jalankan aplikasinya dengan npm install dan npm run tauri dev (untuk template npm), atau cargo install tauri-cli --version "^2.0.0" --locked dan cargo tauri dev (untuk template yang hanya Rust). Tool-nya akan mencetak perintah yang tepat sesuai pilihanmu.
File src-tauri/src/lib.rs yang dihasilkan berisi command pertamamu:
// 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");}Lalu frontend-nya (src/main.js di template vanilla, di sini dipersingkat) memanggilnya seperti ini:
const { invoke } = window.__TAURI__.core;
greetMsgEl.textContent = await invoke("greet", { name: greetInputEl.value });Apa yang terjadi di sini:
#[tauri::command]adalah attribute macro (lihat Makro prosedural). Ia menuliskan kode perekat yang mengubah panggilan dari JavaScript menjadi panggilan Rust biasa.- Argumen datang dalam bentuk JSON, jadi setiap tipe argumen harus mengimplementasikan
Deserializedari serde, dan tipe kembaliannya harus mengimplementasikanSerialize(lihat JSON dengan Serde). Di JavaScript, tulis nama argumen dalam camelCase: parameter Rustinvoke_messagemenjadi{ invokeMessage: "..." }. generate_handler![greet]mendaftarkan command-nya. Kalau kamu lupa menambahkan command baru di sini, panggilan dari JavaScript akan gagal.- Kembalikan
Resultuntuk melaporkan error:Okme-resolve promise JavaScript, danErrme-reject-nya, jadi kamu bisa menanganinya dengan.catch(). Contohnya,fn load(path: String) -> Result<String, String>. main.rshanya memanggilrun(). Kodenya ada dilib.rssupaya aplikasi yang sama juga bisa di-build untuk Android dan iOS.
Slint: UI dalam bahasa markup tersendiri
Section titled “Slint: UI dalam bahasa markup tersendiri”Slint menjelaskan tampilan layar dengan bahasa deklaratif kecil, kira-kira seperti gabungan HTML dan CSS. Kode Rust-mu mengurus logikanya. Berikut hello world resminya. Tambahkan slint = "1.18" di bawah [dependencies] dan taruh ini di src/main.rs:
slint::slint!{ export component HelloWorld { Text { text: "hello world"; color: green; } }}
fn main() { HelloWorld::new().unwrap().run().unwrap();}Makro slint! meng-compile markup itu saat build dan membuatkan tipe Rust HelloWorld untukmu. Untuk aplikasi sungguhan, markup ditaruh di file .slint tersendiri. Counter berikut berasal dari template Rust resmi Slint (bahasa Inggris). File 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(); } } }}Dan sisi Rust-nya di src/main.rs:
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 memakai retained mode: properti counter tinggal di UI, dan tombolnya memanggil sebuah callback. Slint membuat get_counter, set_counter, dan on_request_increase_value dari markup (tanda minus menjadi garis bawah). Closure-nya memegang handle weak (as_weak()) supaya jendela dan callback-nya sendiri tidak saling menahan satu sama lain agar tetap hidup selamanya (lihat Weak dan Cow). Untuk file .slint kamu juga butuh slint-build = "1.18" di bawah [build-dependencies] dan file build.rs satu baris: slint_build::compile("ui/app-window.slint").expect("Slint build failed");.
Iced dan Elm architecture
Section titled “Iced dan Elm architecture”Iced mengikuti Elm architecture, yang namanya diambil dari bahasa Elm. Setiap aplikasi punya empat bagian:
- State: sebuah struct berisi semua yang diingat aplikasi.
- Message: sebuah enum berisi semua hal yang bisa terjadi (“tombol + ditekan”).
- update: menerima state dan satu message, lalu mengubah state. Ini satu-satunya tempat perubahan terjadi.
- view: membangun widget dari state. Widget tidak mengubah apa pun sendiri; mereka hanya mengirim message.
Berikut counter Iced resminya (tambahkan iced = "0.14" ke [dependencies]):
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) tidak menjalankan kode apa pun. Ia hanya berkata “kalau diklik, kirim message ini”. Iced lalu memanggil update dengan message itu dan memanggil view lagi untuk menampilkan state yang baru. Karena setiap perubahan lewat satu match, mudah sekali melihat (dan menguji) semua hal yang bisa dilakukan aplikasimu. Ini contoh pemakaian enum dan match yang bagus.
Coba Elm architecture di browser
Section titled “Coba Elm architecture di browser”Kamu tidak butuh Iced untuk melatih idenya. Model to-do berikut punya empat bagian yang sama. view mengembalikan teks alih-alih widget, dan sebuah daftar message menggantikan ketikan dan klik pengguna:
// 1. State: semua yang perlu diingat aplikasi.#[derive(Default)]struct App { draft: String, todos: Vec<Todo>,}
struct Todo { title: String, done: bool,}
// 2. Message: semua hal yang bisa terjadi.#[derive(Debug)]enum Message { DraftChanged(String), AddPressed, Toggle(usize), ClearDone,}
impl App { // 3. update: SATU-SATUNYA tempat state berubah. 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: mengubah state menjadi apa yang dilihat pengguna (di sini, teks). fn view(&self) -> String { let mut screen = format!("Baru: [{}]\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();
// Anggap saja ini datang dari ketikan keyboard dan klik tombol. let messages = vec![ Message::DraftChanged("Beli susu".to_string()), Message::AddPressed, Message::DraftChanged("Telepon bank".to_string()), Message::AddPressed, Message::DraftChanged("Siram tanaman".to_string()), Message::Toggle(0), ];
for message in messages { println!("> {message:?}"); app.update(message); } print!("{}", app.view());
app.update(Message::ClearDone); println!("> setelah ClearDone:"); print!("{}", app.view());}Output:
> DraftChanged("Beli susu")> AddPressed> DraftChanged("Telepon bank")> AddPressed> DraftChanged("Siram tanaman")> Toggle(0)Baru: [Siram tanaman]0. [x] Beli susu1. [ ] Telepon bank> setelah ClearDone:Baru: [Siram tanaman]0. [ ] Telepon bankPerhatikan bahwa view tidak pernah mengubah apa pun, dan update tidak pernah mencetak apa pun. Pembagian itulah seluruh triknya. Untuk mengubah ini menjadi aplikasi Iced sungguhan, kamu cukup menulis ulang view supaya mengembalikan widget (sebuah text_input yang mengirim DraftChanged, sebuah button yang mengirim AddPressed). State, message, dan update tetap sama persis.
Kebutuhan tiap platform
Section titled “Kebutuhan tiap platform”Crate GUI berbicara dengan sistem operasimu, jadi kamu butuh sedikit lebih banyak dari sekadar Rust biasa. Siapkan dulu dasarnya dari Tools dan setup Rust. Lalu:
| OS | egui/eframe, Iced, Slint | Tauri juga butuh |
|---|---|---|
| Windows | Tidak ada tambahan selain MSVC C++ tools | WebView2. Sudah menjadi bagian dari Windows 10 (versi 1803 ke atas) dan Windows 11 |
| macOS | Xcode command line tools (xcode-select --install) |
Tidak ada tambahan untuk aplikasi desktop; Xcode lengkap untuk iOS |
| Linux (Debian/Ubuntu) | README eframe mencantumkan libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev |
webkit2gtk: libwebkit2gtk-4.1-dev plus beberapa paket lain dari halaman prerequisites Tauri (bahasa Inggris) |
Kalau build di Linux gagal dengan pesan dari pkg-config tentang library yang tidak ditemukan, instal paket -dev dari library itu lalu build lagi. gtk-rs butuh file development GTK 4 (libgtk-4-dev di Ubuntu).
Kesalahan umum
Section titled “Kesalahan umum”- Memblokir thread UI. Download atau perhitungan panjang di dalam fungsi
uimilik egui atauupdatemilik Iced akan membekukan seluruh jendela sampai selesai. Kerjakan pekerjaan lambat di thread lain atau sebagai task async (lihat Konkurensi dan Async/Await), lalu kirim hasilnya kembali ke UI sebagai message. - Menyimpan state egui di variabel lokal.
uidijalankan ulang di setiap frame, jadilet mutdi dalamnya selalu mulai dari awal. Taruh state di struct aplikasimu. - Command Tauri yang tidak pernah didaftarkan. Setiap command harus muncul di
tauri::generate_handler![...]. Selain itu, dilib.rsjangan tandai command sebagaipub: dokumentasi Tauri menjelaskan bahwa ini menyebabkan errorE0255“defined multiple times”. - Mengikuti tutorial lama. Dengan crate 0.x, contoh dari tahun lalu mungkin sudah tidak bisa di-compile. Cocokkan versi di tutorial dengan
Cargo.tomlmilikmu, dan lihat folderexamplesmilik crate itu untuk versi yang kamu pakai.
Latihan
Section titled “Latihan”1. Counter yang tidak pernah di bawah nol
Section titled “1. Counter yang tidak pernah di bawah nol”Tulis counter bergaya Elm dengan message Increment, Decrement, dan Reset. Nilainya tidak boleh turun di bawah 0, walaupun pengguna terus menekan “-”. Jalankan sebuah daftar message lewat update dan cetak view() setelah setiap message.
Petunjuk: pakai u32 dan saturating_sub(1), yang berhenti di 0 alih-alih overflow (lihat Integer Overflow).
Lihat solusi
#[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()); }}Output:
Increment -> Count: 1Increment -> Count: 2Decrement -> Count: 1Decrement -> Count: 0Decrement -> Count: 0Increment -> Count: 1Reset -> Count: 0Aturan “tidak pernah di bawah nol” hanya ada di satu tempat, yaitu update. Kita memformat nama message sebelum memanggil update, karena update menerima message secara by value dan memindahkannya (move).
2. Tambahkan tombol Undo
Section titled “2. Tambahkan tombol Undo”Kembangkan counter-mu dengan message Undo. Setiap perubahan mengingat nilai lamanya, dan Undo mundur satu langkah. Undo tanpa riwayat tidak melakukan apa-apa. Tampilkan berapa langkah undo yang tersisa di view.
Petunjuk: simpan history: Vec<u32> di state. push nilai lama sebelum setiap perubahan, lalu pop saat Undo.
Lihat solusi
#[derive(Default)]struct Counter { value: u32, history: Vec<u32>, // nilai-nilai lama, yang terbaru paling akhir}
#[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); // ingat nilai lama dulu 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()); }}Output:
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)Undo mudah ditambahkan karena setiap perubahan sudah lewat update. Di aplikasi Iced sungguhan, kamu cukup menambahkan satu tombol lagi yang mengirim Message::Undo.
Ringkasan
Section titled “Ringkasan”- GUI di Rust sudah bisa dipakai dengan baik sekarang, tapi belum ada satu toolkit standar, dan beberapa crate populer masih di 0.x.
- Tauri memasangkan frontend web dengan backend Rust; egui adalah immediate mode dengan Rust biasa; Slint memakai markup sendiri dan juga berjalan di perangkat embedded; Iced memakai Elm architecture.
- Retained mode menyimpan pohon widget dan memanggil callback-mu. Immediate mode menggambar ulang UI dari state-mu di setiap frame, tanpa callback.
- Elm architecture membagi aplikasi menjadi state, message,
update, danview, sehingga perubahan mudah diikuti dan diuji. - Tauri butuh WebView2 di Windows dan webkit2gtk di Linux. Lihat Tools dan setup Rust untuk dasarnya.
Kuis singkat
Section titled “Kuis singkat”Kuis singkat
1. Di egui, kenapa nilai counter harus tinggal di struct aplikasimu, bukan di variabel lokal di dalam ui?
2. Di aplikasi Tauri, apa fungsi #[tauri::command]?
3. Di Elm architecture yang dipakai Iced, di mana state aplikasi berubah?
4. Apa yang dibutuhkan Tauri di Linux tapi tidak dibutuhkan egui?
Skor: 0 / 4
Bacaan lebih lanjut
Section titled “Bacaan lebih lanjut”- Are we GUI yet? (bahasa Inggris): gambaran komunitas tentang crate GUI Rust, dengan tag seperti “immediate mode” dan “reactive”, plus berita terbaru.
- Understanding immediate mode — dokumentasi egui (bahasa Inggris): cara kerja egui, lengkap dengan kelebihan dan kekurangannya, langsung dari pembuatnya.
- Calling Rust from the Frontend — Tauri (bahasa Inggris): semua tentang command: argumen, nilai kembalian, error, command async, dan state.
- Dokumentasi Rust Slint (bahasa Inggris): API Rust, makro
slint!, dan cara memakai file.slintdenganslint-build. - The Iced book: Architecture (bahasa Inggris): penjelasan ramah tentang state, message, update, dan view.