最近用兩個不同的 Rust GUI 框架實作了同一個計算機專案,分別是 GPUI 和 Iced。這篇文章比較兩者的架構差異和開發體驗。
框架背景
| 框架 | 開發者 | 特色 |
|---|---|---|
| GPUI | Zed Industries | GPU 加速、為 Zed 編輯器打造 |
| Iced | 社群驅動 | 受 Elm 啟發、跨平台、純 Rust |
兩者都是相對新的框架,目前都還在 0.x 版本階段。
1. 架構設計
GPUI:物件導向 + Render Trait
GPUI 採用類似 React 的元件模式,每個元件是一個 struct,透過實作 Render trait 來定義 UI:
struct CalculatorApp {
input: String,
result: Option<String>,
engine: Calculator,
}
impl Render for CalculatorApp {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.size_full()
.bg(rgb(0x1e1e1e))
.child(self.render_display())
.child(self.render_keypad(cx))
}
}Iced:Elm 架構 (Model-View-Update)
Iced 採用函數式的 Elm 架構,將應用程式分為三個部分:
// Model(狀態)
struct App {
input: String,
result: Option<String>,
engine: Calculator,
}
// Message(事件)
#[derive(Debug, Clone)]
enum Message {
ButtonPressed(String),
Clear,
Evaluate,
}
// Update(狀態轉換)
impl App {
fn update(&mut self, message: Message) {
match message {
Message::Clear => self.input.clear(),
Message::Evaluate => { /* ... */ }
Message::ButtonPressed(s) => self.input.push_str(&s),
}
}
}
// View(UI 描述)
impl App {
fn view(&self) -> Element<'_, Message> {
column![
self.view_display(),
self.view_keypad(),
].into()
}
}關鍵差異:GPUI 的事件處理是內嵌在 view 中的閉包,而 Iced 強制將所有事件抽象為 Message enum,再透過獨立的 update 函數處理。
2. 事件處理
GPUI:閉包直接修改狀態
div()
.on_click(cx.listener(move |this, _event, _window, cx| {
match label.as_str() {
"C" => this.clear(),
"=" => this.evaluate(),
other => this.append(other),
}
cx.notify(); // 手動觸發重新渲染
}))Iced:Message + Pattern Matching
// View 中只發送 Message
button(text("C"))
.on_press(Message::Clear)
button(text("="))
.on_press(Message::Evaluate)
// Update 中統一處理
fn update(&mut self, message: Message) {
match message {
Message::Clear => self.input.clear(),
Message::Evaluate => { /* ... */ }
}
// 自動重新渲染,不需手動觸發
}Iced 的優點:
- 所有狀態變更集中在
update函數,易於追蹤和測試 - Message enum 是可序列化的,方便實作 undo/redo 或時間旅行除錯
GPUI 的優點:
- 閉包可以直接存取局部變數,程式碼更緊湊
- 不需要為每個動作定義 Message variant
3. 樣式系統
GPUI:Tailwind 風格的 Fluent API
div()
.flex()
.flex_col()
.p_4() // padding: 16px
.gap_3() // gap: 12px
.bg(rgb(0x1e1e1e))
.rounded_lg()
.text_xl()
.text_color(rgb(0xffffff))這種 API 對熟悉 Tailwind CSS 的開發者非常直覺。
Iced:Closure-based Styling
container(content)
.padding(16)
.width(Length::Fill)
.style(|_theme| container::Style {
background: Some(color!(0x2d2d2d).into()),
border: iced::Border {
radius: 8.0.into(),
..Default::default()
},
..Default::default()
})Iced 的樣式透過 style 方法傳入閉包,可以根據 theme 和 widget status 動態決定樣式。
按鈕樣式比較
GPUI:
div()
.bg(rgb(0x3c3c3c))
.hover(|style| style.bg(rgb(0x4a4a4a)))
.rounded_md()Iced:
button(content)
.style(move |_theme, status| {
let bg = match status {
button::Status::Hovered => color!(0x5a5a5a),
_ => color!(0x3c3c3c),
};
button::Style {
background: Some(bg.into()),
border: iced::Border { radius: 6.0.into(), ..Default::default() },
..Default::default()
}
})4. 主題支援
GPUI
GPUI 沒有內建主題系統,需要自己定義顏色常數:
const BG_DARK: u32 = 0x1e1e1e;
const BG_BUTTON: u32 = 0x3c3c3c;
const TEXT_PRIMARY: u32 = 0xffffff;Iced
Iced 內建多種主題,只需一行切換:
iced::application("Calculator", App::update, App::view)
.theme(|_| Theme::TokyoNightStorm) // 或 Theme::Dark, Theme::Light 等
.run()5. 程式進入點
GPUI
fn main() -> Result<()> {
let app = Application::new();
app.run(move |cx| {
let bounds = Bounds::centered(None, size(px(340.), px(480.)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_window, cx| cx.new(|_cx| CalculatorApp::new()),
).expect("Failed to open window");
});
Ok(())
}Iced
fn main() -> iced::Result {
iced::application("Iced Calculator", App::update, App::view)
.theme(|_| Theme::TokyoNightStorm)
.window_size((340.0, 480.0))
.run()
}Iced 的 builder pattern 更簡潔,GPUI 則提供更多底層控制。
6. 跨平台支援
| 平台 | GPUI | Iced |
|---|---|---|
| macOS | 完整支援 (Metal) | 完整支援 |
| Windows | 實驗性 | 完整支援 |
| Linux | 支援 (Vulkan) | 完整支援 |
| Web | 不支援 | 支援 (WebGPU/WebGL) |
Iced 在跨平台方面明顯更成熟。
7. 程式碼行數比較
同樣的計算機功能:
| 檔案 | GPUI | Iced |
|---|---|---|
| main.rs | ~250 行 | ~260 行 |
| 總計 | 差不多 | 差不多 |
兩者的程式碼量相當,但結構不同。
8. 編譯時間與依賴
# GPUI
cargo build # ~2 分鐘,743 個 crates
# Iced
cargo build # ~1.5 分鐘,415 個 cratesIced 的依賴較少,編譯速度稍快。
選擇建議
選擇 GPUI 如果你:
- 正在為 Zed 編輯器開發插件
- 偏好物件導向的元件設計
- 需要極致的渲染效能
- 主要目標平台是 macOS
選擇 Iced 如果你:
- 需要跨平台支援(特別是 Web)
- 喜歡 Elm 架構的狀態管理
- 想要內建主題支援
- 偏好穩定的 API(雖然都還在 0.x)
總結
| 特性 | GPUI | Iced |
|---|---|---|
| 架構 | 物件導向 + Render | Elm (MVU) |
| 樣式 | Tailwind-like | Closure-based |
| 主題 | 手動定義 | 內建多種 |
| 事件 | 閉包 + cx.notify() |
Message enum |
| 跨平台 | 有限 | 優秀 |
| 成熟度 | 較新 | 較成熟 |
兩個框架都展示了 Rust GUI 開發的可能性。GPUI 代表了高效能、GPU 加速的方向,而 Iced 則體現了函數式、跨平台的設計理念。選擇哪個取決於你的專案需求和個人偏好。