Lewati ke konten

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”.

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.

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.

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 memakai fn ui(&mut self, ui: &mut egui::Ui, ...).
  • Tauri, Dioxus desktop, dan gtk-rs bergantung pada library sistem. Lihat Kebutuhan tiap platform di bawah.

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]
▶ Coba sendiri

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.

eframe adalah “app framework” resmi egui: ia membuka jendela dan menjalankan egui di dalamnya. Buat proyek baru dan tambahkan eframe:

Terminal window
cargo new counter
cd 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:

  • CounterApp adalah state-mu. #[derive(Default)] membuat count mulai dari 0.
  • run_native membuka jendela. Closure (fungsi anonim) di dalamnya membuat aplikasimu satu kali, di dalam sebuah Box (lihat Smart Pointer).
  • eframe memanggil ui untuk setiap frame. CentralPanel mengisi jendela, dan ui.horizontal menaruh 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.

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):

Terminal window
cargo install create-tauri-app --locked
cargo create-tauri-app

Ada 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 Deserialize dari serde, dan tipe kembaliannya harus mengimplementasikan Serialize (lihat JSON dengan Serde). Di JavaScript, tulis nama argumen dalam camelCase: parameter Rust invoke_message menjadi { invokeMessage: "..." }.
  • generate_handler![greet] mendaftarkan command-nya. Kalau kamu lupa menambahkan command baru di sini, panggilan dari JavaScript akan gagal.
  • Kembalikan Result untuk melaporkan error: Ok me-resolve promise JavaScript, dan Err me-reject-nya, jadi kamu bisa menanganinya dengan .catch(). Contohnya, fn load(path: String) -> Result<String, String>.
  • main.rs hanya memanggil run(). Kodenya ada di lib.rs supaya aplikasi yang sama juga bisa di-build untuk Android dan iOS.

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 mengikuti Elm architecture, yang namanya diambil dari bahasa Elm. Setiap aplikasi punya empat bagian:

  1. State: sebuah struct berisi semua yang diingat aplikasi.
  2. Message: sebuah enum berisi semua hal yang bisa terjadi (“tombol + ditekan”).
  3. update: menerima state dan satu message, lalu mengubah state. Ini satu-satunya tempat perubahan terjadi.
  4. 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.

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 susu
1. [ ] Telepon bank
> setelah ClearDone:
Baru: [Siram tanaman]
0. [ ] Telepon bank
▶ Coba sendiri

Perhatikan 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.

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).

  • Memblokir thread UI. Download atau perhitungan panjang di dalam fungsi ui milik egui atau update milik 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. ui dijalankan ulang di setiap frame, jadi let mut di 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, di lib.rs jangan tandai command sebagai pub: dokumentasi Tauri menjelaskan bahwa ini menyebabkan error E0255 “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.toml milikmu, dan lihat folder examples milik crate itu untuk versi yang kamu pakai.

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: 1
Increment -> Count: 2
Decrement -> Count: 1
Decrement -> Count: 0
Decrement -> Count: 0
Increment -> Count: 1
Reset -> Count: 0
▶ Coba sendiri

Aturan “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).

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)
▶ Coba sendiri

Undo mudah ditambahkan karena setiap perubahan sudah lewat update. Di aplikasi Iced sungguhan, kamu cukup menambahkan satu tombol lagi yang mengirim Message::Undo.

  • 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, dan view, sehingga perubahan mudah diikuti dan diuji.
  • Tauri butuh WebView2 di Windows dan webkit2gtk di Linux. Lihat Tools dan setup Rust untuk dasarnya.

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