最近用兩個不同的 Rust GUI 框架實作了同一個計算機專案,分別是 GPUIIced。這篇文章比較兩者的架構差異和開發體驗。

框架背景

框架 開發者 特色
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 個 crates

Iced 的依賴較少,編譯速度稍快。

選擇建議

選擇 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 則體現了函數式、跨平台的設計理念。選擇哪個取決於你的專案需求和個人偏好。

參考資源