Rust GUI 应用:Tauri、egui、Slint 和 Iced
迟早有一天,你会想要一个真正带按钮的窗口,而不只是终端里的文字。用 Rust 能做到吗?能。Rust 写的 GUI 应用速度快、体积小、也很安全,现在已经有好几个不错的工具包。不过,和 C# 的 WPF、JavaScript 的 Electron 比起来,Rust 的 GUI 生态还比较年轻,所以在选择之前,先了解一下整体情况会很有帮助。
这一页会实事求是地介绍现状,给你一张帮你做选择的表格,讲清楚即时模式(immediate mode)和保留模式(retained mode)背后的思路,并为最流行的几个工具包各写一个小程序。
代码在哪里运行? GUI 需要一个真正的窗口,所以 egui、Tauri、Slint 和 Iced 的程序要在你自己的计算机上用 cargo run 运行。这一页的 egui 和 Iced 程序都用 cargo check 在当前版本上编译检查过。另外有两节用纯 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 那样唯一的“标准”工具包。好几个流行的 crate 还停留在 0.x 版本,升级时可能会破坏你的代码。无障碍支持、从右到左的文字和原生外观,各个工具包的支持程度都不一样。
- 好消息:你不用去找“完美”的工具包。写第一个应用时,下表里的任何一个都很好用。
想看更详细的对比,可以读 2025 survey of Rust GUI libraries(英文),它把许多工具包逐一试了一遍。
能用它做什么
Section titled “能用它做什么”现在就能看到的真实应用:
- Rerun Viewer(英文)是一个查看机器人和计算机视觉数据的工具,用 egui 构建。(Rerun 也在资助 egui 的开发。)
- System76 为 Linux 打造的桌面环境 COSMIC 是用 Iced 构建的,IRC 聊天客户端 Halloy 也是。
- 带 Web 风格界面的桌面工具:笔记应用、仪表盘和小型业务工具,常常很适合用 Tauri 来写。
- 设备上的屏幕:Slint 还能在微控制器上运行,所以很适合机器和家电上的触摸屏。
选哪个 crate?
Section titled “选哪个 crate?”版本号是 2026 年 10 月的最新版本。
| crate | 版本 | 怎么写界面 | 运行平台 | 适合的场景 |
|---|---|---|---|---|
| Tauri | 2.12(3.0 处于 alpha 阶段) | 界面用 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 | 你喜欢干净、可预测的代码,所有修改都集中在一个地方 |
| gtk-rs(gtk4) | 0.11 | GTK 4 的 Rust 绑定,保留模式 | 优先支持 Linux;Windows 和 macOS 需要更多配置 | Linux 上 GNOME 桌面风格的原生应用 |
| Dioxus | 0.7 | 类似 React 的组件,用 rsx! 宏 |
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 的网页,以及其他语言的大多数工具包),按钮只创建一次。工具包把它保存在一棵控件树里。你给它挂上一个回调函数,用户点击时,工具包就调用你的回调。以后想改按钮上的文字,你需要拿着这个按钮的句柄。
在即时模式下(egui),什么都不保存。你的界面函数每一帧都会重新运行一遍,只负责描述此刻要显示什么。画一个按钮、检查它有没有被点击,一行就搞定:if ui.button("+").clicked() { ... }。没有回调,也没有句柄。应用的状态放在你自己的结构体里,界面每次都根据它来画,所以屏幕上显示的内容永远不会和数据对不上。
代价是:工具包每一帧都要重新布局整个界面,所以界面特别大时会多占一些 CPU;有些布局(比如让一个大小还不确定的窗口居中)也更难实现。egui 只在发生事情时(比如鼠标移动)才画新的一帧,所以空闲的应用几乎不占 CPU。
下面用纯 Rust 写一个假装的、迷你的即时模式工具包。循环每走一步就是一帧,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) }}
// 你的界面代码:每一帧都会重新调用。fn draw(count: &mut i32, ui: &mut Ui) { ui.label(&format!("计数:{count}")); if ui.button("+") { *count += 1; } if ui.button("重置") { *count = 0; }}
fn main() { let mut count = 0; // 应用状态放在界面代码之外 let clicks = [None, Some("+"), Some("+"), None, Some("重置"), 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} 帧 → {}", ui.drawn.join(" ")); }}输出:
第 0 帧 → 计数:0 [+] [重置]第 1 帧 → 计数:0 [+] [重置]第 2 帧 → 计数:1 [+] [重置]第 3 帧 → 计数:2 [+] [重置]第 4 帧 → 计数:2 [+] [重置]第 5 帧 → 计数:0 [+] [重置]看第 1 帧:你点了“+”,标签却还显示 0。因为标签是在按钮发现被点击之前画出来的,新值要到下一帧才显示。真正的 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 运行。第一次编译要下载并编译几百个 crate,所以请等上几分钟,之后再编译就快了。屏幕上会打开一个小窗口,显示“Count: 0”和三个按钮。
这些代码是怎么配合的:
CounterApp就是你的状态。#[derive(Default)]让count从 0 开始。run_native负责打开窗口。闭包只调用一次,在一个Box里创建你的应用(见智能指针)。- eframe 每一帧都会调用
ui。CentralPanel填满整个窗口,ui.horizontal把三个按钮排成一行。 - 每个
if ui.button(..).clicked()都是上一节讲的即时模式写法:画按钮、检查它、修改状态。
这也是为什么状态必须放在 self 里,而不能放在 ui 里的局部变量中:在那里写 let mut count = 0;,每一帧都会重新从 0 开始。
Tauri 命令:网页背后的 Rust
Section titled “Tauri 命令:网页背后的 Rust”一个 Tauri 应用分成两部分。前端是一个普通的网页(HTML、CSS 和 JavaScript,或者 React、Svelte 这样的框架),运行在系统的 webview 里。后端是你的 Rust 代码。前端调用的 Rust 函数叫作命令(command)。
最简单的起步方式是用官方的项目生成器。它会问你项目名称和前端模板(“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 里,参数名要写成 camelCase: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()。代码放在lib.rs里,是为了让同一个应用也能编译成 Android 和 iOS 版本。
Slint:用专门的标记语言写界面
Section titled “Slint:用专门的标记语言写界面”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(); } } }}Rust 这一侧的 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 是保留模式:counter 属性保存在界面里,按钮会调用一个回调。Slint 根据标记生成 get_counter、set_counter 和 on_request_increase_value(短横线会变成下划线)。闭包持有的是一个弱句柄(as_weak()),这样窗口和它自己的回调就不会互相拉着对方、永远无法释放(见 Weak 与 Cow)。使用 .slint 文件时,还需要在 [build-dependencies] 下加上 slint-build = "1.18",并写一个只有一行的 build.rs:slint_build::compile("ui/app-window.slint").expect("Slint build failed");。
Iced 与 Elm 架构
Section titled “Iced 与 Elm 架构”Iced 遵循 Elm 架构,名字来自 Elm 语言。每个应用都有四个部分:
- 状态:一个结构体,装着应用需要记住的所有东西。
- 消息:一个枚举,列出所有可能发生的事情(“+ 按钮被按下了”)。
- update:接收状态和一条消息,然后修改状态。这是唯一会发生修改的地方。
- 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。下面这个待办事项模型有同样的四个部分。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!("新任务:[{}]\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!("> ClearDone 之后:"); print!("{}", app.view());}输出:
> DraftChanged("买牛奶")> AddPressed> DraftChanged("给银行打电话")> AddPressed> DraftChanged("给植物浇水")> Toggle(0)新任务:[给植物浇水]0. [x] 买牛奶1. [ ] 给银行打电话> ClearDone 之后:新任务:[给植物浇水]0. [ ] 给银行打电话注意,view 从不修改任何东西,update 也从不打印任何东西。这种分工就是全部的诀窍。要把它变成真正的 Iced 应用,你只需要重写 view,让它返回控件(一个发送 DraftChanged 的 text_input,一个发送 AddPressed 的 button)。状态、消息和 update 完全不用改。
平台准备工作
Section titled “平台准备工作”GUI crate 要和操作系统打交道,所以光有 Rust 还不够。先按照 Rust 工具与环境配置把基础环境装好,然后:
| 操作系统 | 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)。
- 阻塞界面线程。 在 egui 的
ui函数或 Iced 的update里下载文件或做长时间的计算,整个窗口都会卡住,直到它完成。把慢的工作放到另一个线程或异步任务里(见并发和异步(async/await)),再把结果作为消息发回界面。 - 把 egui 的状态放在局部变量里。
ui每一帧都会重新运行,所以里面的let mut每次都从头开始。把状态放进你的应用结构体。 - 忘了注册 Tauri 命令。 每个命令都必须出现在
tauri::generate_handler![...]里。另外,在lib.rs里不要把命令标记为pub:Tauri 文档解释过,这会导致E0255“defined multiple times”(重复定义)错误。 - 照着旧教程写。 对 0.x 的 crate 来说,去年的示例可能已经编译不过了。对比一下教程里的版本和你的
Cargo.toml,再去看看 crate 自己的examples文件夹里对应你所用版本的示例。
1. 永远不会低于 0 的计数器
Section titled “1. 永远不会低于 0 的计数器”写一个 Elm 风格的计数器,带 Increment、Decrement 和 Reset 三种消息。即使用户一直按“-”,值也绝不能低于 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!("计数:{}", 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 -> 计数:1Increment -> 计数:2Decrement -> 计数:1Decrement -> 计数:0Decrement -> 计数:0Increment -> 计数:1Reset -> 计数:0“永远不低于 0”这条规则只存在于一个地方:update。我们在调用 update 之前就把消息名格式化好了,因为 update 按值接收消息,会把它移动走。消息名用 {name:<9} 补齐到 9 个字符来对齐,而它们是英文的枚举变体名,所以对齐不受中文影响。
2. 加一个撤销按钮
Section titled “2. 加一个撤销按钮”给你的计数器加一个 Undo 消息。每次修改都记住旧值,Undo 回退一步。没有历史记录时,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!("计数:{}(可撤销 {} 步)", 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 -> 计数:1(可撤销 1 步)Increment -> 计数:2(可撤销 2 步)Decrement -> 计数:1(可撤销 3 步)Increment -> 计数:2(可撤销 4 步)Reset -> 计数:0(可撤销 5 步)Undo -> 计数:2(可撤销 4 步)Undo -> 计数:1(可撤销 3 步)Undo -> 计数:2(可撤销 2 步)撤销功能之所以这么好加,是因为每次修改本来就都经过 update。在真正的 Iced 应用里,你只需要再加一个发送 Message::Undo 的按钮。
- Rust GUI 现在已经很好用,但还没有唯一的标准工具包,好几个流行的 crate 仍然是 0.x 版本。
- Tauri 把 Web 前端和 Rust 后端组合在一起;egui 是纯 Rust 的即时模式;Slint 用自己的标记语言,还能在嵌入式设备上运行;Iced 采用 Elm 架构。
- 保留模式维护一棵控件树,并调用你的回调。即时模式每一帧都根据你的状态重画界面,没有回调。
- Elm 架构把应用分成状态、消息、
update和view,修改过程清楚好跟踪,也容易测试。 - Tauri 在 Windows 上需要 WebView2,在 Linux 上需要 webkit2gtk。基础环境见 Rust 工具与环境配置。
小测验
1. 在 egui 里,为什么计数器的值必须放在应用结构体里,而不能放在 ui 里的局部变量中?
2. 在 Tauri 应用里,#[tauri::command] 有什么作用?
3. 在 Iced 使用的 Elm 架构里,应用状态在哪里改变?
4. 在 Linux 上,Tauri 需要而 egui 不需要的是什么?
得分: 0 / 4
- Are we GUI yet?(英文):社区整理的 Rust GUI crate 概览,带有“immediate mode”“reactive”等标签和最新动态。
- Understanding immediate mode — egui 文档(英文):作者亲自讲解 egui 的工作原理及其优缺点。
- 从前端调用 Rust — Tauri:关于命令的一切:参数、返回值、错误、异步命令和状态。
- Slint Rust documentation(英文):Rust API、
slint!宏,以及如何配合slint-build使用.slint文件。 - The Iced book: Architecture(英文):用通俗的方式讲解状态、消息、update 和 view。