levien-native-ui-mastery

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Raph Levien Style Guide

Raph Levien 风格指南

Overview

概述

Raph Levien is a Principal Software Engineer at Canva (formerly Google Fonts) and the architect of the Linebender ecosystem: Druid, Xilem, Vello, Piet, and Kurbo. He has spent decades at the intersection of 2D graphics, UI architecture, and typography. His blog "raphlinus.github.io" is the canonical source for modern thinking about native UI in Rust.
Raph Levien是Canva(前Google Fonts)的首席软件工程师,同时也是Linebender生态系统(包括Druid、Xilem、Vello、Piet和Kurbo)的架构师。他深耕2D图形、UI架构和排版领域数十年。他的博客“raphlinus.github.io”是了解Rust原生UI现代设计思路的权威来源。

Core Philosophy

核心理念

"Architectures that work well in other languages generally don't adapt well to Rust, mostly because they rely on shared mutable state."
"Hidden inside of every UI framework is some kind of incrementalization framework."
"The end-to-end transformation is so complicated it would be very difficult to express directly. So it's best to break it down into smaller chunks, stitched together in a pipeline."
Levien sees UI as a pipeline of tree transformations. The view tree describes intent, the widget tree retains state, and the render tree produces pixels. Fighting Rust's ownership model means your architecture is wrong—find one that works with the language.
“其他语言中表现良好的架构通常很难适配Rust,主要原因是它们依赖共享可变状态。”
“每个UI框架的核心都隐含着某种增量更新框架。”
“端到端的转换过程极为复杂,难以直接实现。因此最好将其拆分为多个小模块,通过流水线的方式串联起来。”
Levien将UI视为树转换流水线:视图树描述设计意图,组件树保存状态,渲染树生成像素。如果你的架构与Rust的所有权模型冲突,那说明架构设计存在问题——应该找到一种能与Rust特性兼容的方案。

Design Principles

设计原则

  1. Declarative Over Imperative: UI should describe what, not how. Application logic produces a view tree; the framework handles the rest.
  2. Synchronized Trees: View tree (ephemeral, typed) → Widget tree (retained, stateful) → Render tree (layout, paint). Each stage has clear responsibilities.
  3. Incremental by Default: Memoize aggressively. Diff sparsely. Fine-grained change propagation beats wholesale re-rendering.
  4. Statically Typed, Ergonomically Used: Leverage Rust's type system to catch errors at compile time, but don't burden the developer with excessive annotations.
  5. GPU-First Rendering: The CPU describes the scene; the GPU does the work. Compute shaders can handle parsing, flattening, and rasterization.
  6. Composition via Adapt Nodes: Components own a slice of state, not the global state. Adapt nodes translate between parent and child state types.
  7. Accessibility Is Architecture: Screen reader support requires retained structure and stable identity. This is not an afterthought—it shapes the design.
  8. Performance Is Research: Willing to solve hard problems (Euler spirals, parallel curves, GPU compute pipelines) rather than accept mediocre solutions.
  1. 声明式优先于命令式:UI应描述“想要什么”,而非“如何实现”。应用逻辑只需生成视图树,其余工作由框架处理。
  2. 同步树模型:视图树(临时、强类型)→ 组件树(持久、带状态)→ 渲染树(布局、绘制)。每个阶段都有明确的职责划分。
  3. 默认支持增量更新:积极使用记忆化(Memoization),仅对差异部分进行对比。细粒度的变更传播优于全量重新渲染。
  4. 静态类型与易用性平衡:利用Rust的类型系统在编译期捕获错误,但不要给开发者增加过多的注解负担。
  5. GPU优先渲染:CPU负责描述场景,GPU负责执行渲染工作。计算着色器可用于处理解析、路径展平以及光栅化等任务。
  6. 通过Adapt节点实现组合:组件仅拥有部分状态,而非全局状态。Adapt节点负责在父组件与子组件的状态类型之间进行转换。
  7. 可访问性是架构的一部分:屏幕阅读器支持依赖持久化的结构和稳定的标识。这并非事后补充的功能,而是架构设计的核心考量因素。
  8. 性能源于深度研究:愿意攻克复杂问题(如欧拉螺旋、平行曲线、GPU计算流水线),而非接受平庸的解决方案。

When Building UI

UI构建实践规范

Always

必须遵循

  • Model UI as a pipeline of tree transformations
  • Use declarative view descriptions that produce typed trees
  • Design for incremental updates from the start
  • Provide stable identity for widgets (id paths)
  • Consider accessibility requirements early—they affect architecture
  • Separate view logic (ephemeral) from widget state (retained)
  • Route events through the tree with mutable access at each stage
  • 将UI建模为树转换流水线
  • 使用声明式视图描述生成强类型树结构
  • 从设计初期就考虑增量更新的实现
  • 为组件提供稳定的标识(id路径)
  • 尽早考虑可访问性需求——它们会影响架构设计
  • 分离视图逻辑(临时)与组件状态(持久)
  • 通过树结构路由事件,每个阶段都可对状态进行可变访问

Never

严禁操作

  • Rely on shared mutable state for UI coordination
  • Use
    Rc<RefCell<T>>
    as a first resort—it's a sign of architectural mismatch
  • Assume immediate mode can handle complex UI (accessibility, virtualized scroll)
  • Create explicit message types for every interaction (Elm-style verbosity)
  • Couple rendering tightly to the CPU—GPUs are massively parallel
  • Ignore the borrow checker—restructure instead
  • 依赖共享可变状态进行UI协调
  • 优先使用
    Rc<RefCell<T>>
    ——这是架构设计不匹配的信号
  • 假设即时模式能处理复杂UI(如可访问性、虚拟滚动)
  • 为每一次交互创建显式的消息类型(Elm风格的冗余设计)
  • 将渲染逻辑与CPU深度耦合——GPU具备强大的并行处理能力
  • 忽略借用检查器——应重构架构而非绕过它

Prefer

推荐方案

  • View trees over imperative widget construction
  • Adapt nodes over global message dispatch
  • Id-path event routing over callback spaghetti
  • Retained widget trees over pure immediate mode
  • GPU compute shaders over CPU rendering loops
  • Sparse collection diffing over full re-renders
  • Typed erasure escape hatches (
    AnyView
    ) over runtime type chaos
  • 使用视图树而非命令式组件构建方式
  • 使用Adapt节点而非全局消息分发
  • 使用id路径事件路由而非回调地狱
  • 使用持久化组件树而非纯即时模式
  • 使用GPU计算着色器而非CPU渲染循环
  • 使用稀疏集合对比而非全量重新渲染
  • 使用类型擦除的逃逸舱(
    AnyView
    )而非运行时类型混乱

Architecture Patterns

架构模式

The Synchronized Tree Model

同步树模型

rust
// UI as a pipeline of tree transformations
//
// 1. App produces View tree (ephemeral, describes intent)
// 2. Framework diffs View tree against previous version
// 3. Diff is applied to Widget tree (retained, holds state)
// 4. Widget tree produces Render tree (layout, paint)
// 5. Render tree is drawn to screen (GPU)

// View trait: the core abstraction
trait View {
    type State;           // View-specific state (persists across cycles)
    type Widget;          // Associated widget type
    
    fn build(&self, cx: &mut Cx) -> (Self::State, Self::Widget);
    fn rebuild(&self, cx: &mut Cx, state: &mut Self::State, widget: &mut Self::Widget);
    fn event(&self, state: &mut Self::State, event: &Event) -> EventResult;
}

// The view tree is statically typed—compiler knows the full structure
// State and widget trees are derived via associated types
rust
// UI as a pipeline of tree transformations
//
// 1. App produces View tree (ephemeral, describes intent)
// 2. Framework diffs View tree against previous version
// 3. Diff is applied to Widget tree (retained, holds state)
// 4. Widget tree produces Render tree (layout, paint)
// 5. Render tree is drawn to screen (GPU)

// View trait: the core abstraction
trait View {
    type State;           // View-specific state (persists across cycles)
    type Widget;          // Associated widget type
    
    fn build(&self, cx: &mut Cx) -> (Self::State, Self::Widget);
    fn rebuild(&self, cx: &mut Cx, state: &mut Self::State, widget: &mut Self::Widget);
    fn event(&self, state: &mut Self::State, event: &Event) -> EventResult;
}

// The view tree is statically typed—compiler knows the full structure
// State and widget trees are derived via associated types

Adapt Nodes for Composition

通过Adapt节点实现组件组合

rust
// BAD: Global app state threaded everywhere
struct AppState {
    user: UserState,
    settings: SettingsState,
    // Every component sees everything
}

fn settings_panel(state: &mut AppState) -> impl View {
    // Has access to user state it doesn't need
    // ...
}

// GOOD: Adapt nodes slice state for components
fn app_view(state: &mut AppState) -> impl View {
    VStack::new((
        // Adapt translates between parent and child state
        Adapt::new(
            |state: &mut AppState, thunk| {
                // Child only sees SettingsState
                thunk.call(&mut state.settings)
            },
            settings_panel,  // Receives &mut SettingsState
        ),
        Adapt::new(
            |state: &mut AppState, thunk| thunk.call(&mut state.user),
            user_panel,  // Receives &mut UserState
        ),
    ))
}

// Components are decoupled—they don't know about global state
fn settings_panel(state: &mut SettingsState) -> impl View {
    // Only has access to what it needs
    Toggle::new("Dark Mode", &mut state.dark_mode)
}
rust
// BAD: Global app state threaded everywhere
struct AppState {
    user: UserState,
    settings: SettingsState,
    // Every component sees everything
}

fn settings_panel(state: &mut AppState) -> impl View {
    // Has access to user state it doesn't need
    // ...
}

// GOOD: Adapt nodes slice state for components
fn app_view(state: &mut AppState) -> impl View {
    VStack::new((
        // Adapt translates between parent and child state
        Adapt::new(
            |state: &mut AppState, thunk| {
                // Child only sees SettingsState
                thunk.call(&mut state.settings)
            },
            settings_panel,  // Receives &mut SettingsState
        ),
        Adapt::new(
            |state: &mut AppState, thunk| thunk.call(&mut state.user),
            user_panel,  // Receives &mut UserState
        ),
    ))
}

// Components are decoupled—they don't know about global state
fn settings_panel(state: &mut SettingsState) -> impl View {
    // Only has access to what it needs
    Toggle::new("Dark Mode", &mut state.dark_mode)
}

Id-Path Event Routing

Id路径事件路由

rust
// Events are routed via id paths, providing mutable access at each stage
//
// When a button is clicked:
// 1. Event enters at root with full id path: [root, container, button]
// 2. Root receives event, can mutate app state, forwards to container
// 3. Container receives event, can mutate its state, forwards to button
// 4. Button handles the click, mutates its state
// 5. Callbacks fire with mutable access to appropriate state slice

struct IdPath(Vec<Id>);

impl View for Button {
    fn event(&self, state: &mut Self::State, id_path: &IdPath, event: &Event) -> EventResult {
        if id_path.is_empty() && matches!(event, Event::Click) {
            // This event is for us
            (self.on_click)(state);
            EventResult::Handled
        } else {
            EventResult::Ignored
        }
    }
}

// Key insight: mutable state access at each level of the tree
// No need for message passing or global dispatch
rust
// Events are routed via id paths, providing mutable access at each stage
//
// When a button is clicked:
// 1. Event enters at root with full id path: [root, container, button]
// 2. Root receives event, can mutate app state, forwards to container
// 3. Container receives event, can mutate its state, forwards to button
// 4. Button handles the click, mutates its state
// 5. Callbacks fire with mutable access to appropriate state slice

struct IdPath(Vec<Id>);

impl View for Button {
    fn event(&self, state: &mut Self::State, id_path: &IdPath, event: &Event) -> EventResult {
        if id_path.is_empty() && matches!(event, Event::Click) {
            // This event is for us
            (self.on_click)(state);
            EventResult::Handled
        } else {
            EventResult::Ignored
        }
    }
}

// Key insight: mutable state access at each level of the tree
// No need for message passing or global dispatch

Memoization for Incremental Updates

增量更新的记忆化实现

rust
// Fine-grained change propagation: only rebuild what changed

fn item_list(items: &[Item]) -> impl View {
    VStack::new(
        items.iter().map(|item| {
            // Memoize: only rebuild if item changed
            Memoize::new(
                item.id,           // Stable identity
                item.clone(),      // Data to compare
                |item| item_row(item),
            )
        })
    )
}

// The framework tracks:
// - Which items are new (build)
// - Which items changed (rebuild)  
// - Which items are gone (destroy)
// - Which items are unchanged (skip)

// Ron Minsky: "hidden inside of every UI framework 
// is some kind of incrementalization framework"
rust
// Fine-grained change propagation: only rebuild what changed

fn item_list(items: &[Item]) -> impl View {
    VStack::new(
        items.iter().map(|item| {
            // Memoize: only rebuild if item changed
            Memoize::new(
                item.id,           // Stable identity
                item.clone(),      // Data to compare
                |item| item_row(item),
            )
        })
    )
}

// The framework tracks:
// - Which items are new (build)
// - Which items changed (rebuild)  
// - Which items are gone (destroy)
// - Which items are unchanged (skip)

// Ron Minsky: "hidden inside of every UI framework 
// is some kind of incrementalization framework"

GPU Scene Description

GPU场景描述

rust
// Vello model: CPU describes, GPU renders
//
// The CPU uploads a scene in a simplified binary format
// Compute shaders handle:
// - Parsing the scene graph
// - Path flattening (curves → line segments)
// - Tiling and binning
// - Rasterization
// - Compositing

struct Scene {
    // Scene description: shapes, transforms, clips, blends
    encoding: Vec<u8>,
}

impl Scene {
    fn fill(&mut self, path: &Path, brush: &Brush) {
        // Encode fill command into binary format
        self.encoding.extend(encode_fill(path, brush));
    }
    
    fn stroke(&mut self, path: &Path, style: &Stroke, brush: &Brush) {
        // Stroke is expanded on GPU via Euler spiral approximation
        self.encoding.extend(encode_stroke(path, style, brush));
    }
    
    fn push_transform(&mut self, transform: Affine) {
        self.encoding.extend(encode_transform(transform));
    }
}

// Key insight: the GPU is massively parallel
// Traditional 2D APIs (Cairo, Skia) serialize work on CPU
// Vello parallelizes across thousands of GPU cores
rust
// Vello model: CPU describes, GPU renders
//
// The CPU uploads a scene in a simplified binary format
// Compute shaders handle:
// - Parsing the scene graph
// - Path flattening (curves → line segments)
// - Tiling and binning
// - Rasterization
// - Compositing

struct Scene {
    // Scene description: shapes, transforms, clips, blends
    encoding: Vec<u8>,
}

impl Scene {
    fn fill(&mut self, path: &Path, brush: &Brush) {
        // Encode fill command into binary format
        self.encoding.extend(encode_fill(path, brush));
    }
    
    fn stroke(&mut self, path: &Path, style: &Stroke, brush: &Brush) {
        // Stroke is expanded on GPU via Euler spiral approximation
        self.encoding.extend(encode_stroke(path, style, brush));
    }
    
    fn push_transform(&mut self, transform: Affine) {
        self.encoding.extend(encode_transform(transform));
    }
}

// Key insight: the GPU is massively parallel
// Traditional 2D APIs (Cairo, Skia) serialize work on CPU
// Vello parallelizes across thousands of GPU cores

Sparse Collection Diffing

稀疏集合对比

rust
// Efficient updates for large collections using immutable data structures

use std::sync::Arc;

// Immutable collection with structural sharing
struct ImList<T> {
    root: Option<Arc<Node<T>>>,
}

impl<T: Clone + Eq> ImList<T> {
    fn diff(&self, other: &Self) -> CollectionDiff<T> {
        // O(changed) not O(n) comparison
        // Structural sharing means unchanged subtrees are pointer-equal
        diff_trees(&self.root, &other.root)
    }
}

// In the view layer:
fn list_view(items: &ImList<Item>) -> impl View {
    // Framework diffs against previous items
    // Only changed items trigger widget updates
    VirtualList::new(items, |item| item_row(item))
}

// This solves the UI collection problem:
// - Complex incremental updates → error-prone
// - Full diffing every frame → slow for large collections
// - Immutable + structural sharing → best of both
rust
// Efficient updates for large collections using immutable data structures

use std::sync::Arc;

// Immutable collection with structural sharing
struct ImList<T> {
    root: Option<Arc<Node<T>>>,
}

impl<T: Clone + Eq> ImList<T> {
    fn diff(&self, other: &Self) -> CollectionDiff<T> {
        // O(changed) not O(n) comparison
        // Structural sharing means unchanged subtrees are pointer-equal
        diff_trees(&self.root, &other.root)
    }
}

// In the view layer:
fn list_view(items: &ImList<Item>) -> impl View {
    // Framework diffs against previous items
    // Only changed items trigger widget updates
    VirtualList::new(items, |item| item_row(item))
}

// This solves the UI collection problem:
// - Complex incremental updates → error-prone
// - Full diffing every frame → slow for large collections
// - Immutable + structural sharing → best of both

Mental Model

思维模型

Levien approaches UI by asking:
  1. What trees are involved? — View, widget, render, draw—each has a role
  2. How does state flow? — Props down, events up, through Adapt nodes
  3. Where is the incrementalization? — What can be memoized? What must be diffed?
  4. Can this be parallelized? — GPU compute? Multi-threaded reconciliation?
  5. What does the type system encode? — Compile-time structure vs runtime flexibility
  6. Is accessibility possible? — Retained structure and stable identity are required
Levien在设计UI时会思考以下问题:
  1. 涉及哪些树结构? —— 视图树、组件树、渲染树、绘制树,每个都有明确的角色
  2. 状态如何流转? —— 属性向下传递,事件向上传递,通过Adapt节点进行中转
  3. 增量更新的实现点在哪里? —— 哪些内容可以记忆化?哪些需要进行差异对比?
  4. 是否可以并行化处理? —— GPU计算?多线程协调?
  5. 类型系统能编码哪些信息? —— 编译期结构 vs 运行时灵活性
  6. 是否具备可访问性? —— 持久化结构和稳定标识是必要条件

Raph's Design Questions

Raph的设计校验问题

When designing UI architecture:
  1. Is this declarative? Can app logic just describe what it wants?
  2. Where is the retained state? Who owns it?
  3. How do events flow back to state? With what granularity of access?
  4. What happens when the collection has 10,000 items?
  5. Can a screen reader traverse this? Is identity stable?
  6. Where is work happening—CPU or GPU? Can it be parallelized?
设计UI架构时,需回答以下问题:
  1. 这是声明式的吗?应用逻辑是否只需描述需求即可?
  2. 持久化状态存储在哪里?由谁负责管理?
  3. 事件如何回流到状态?访问粒度是怎样的?
  4. 当集合包含10000个条目时,系统表现如何?
  5. 屏幕阅读器能否遍历该结构?标识是否稳定?
  6. 计算工作在何处执行——CPU还是GPU?能否并行化?

Signature Moves

标志性设计手法

  • Synchronized tree diffing: View tree is ephemeral, widget tree persists
  • Adapt nodes: State slicing for component composition
  • Id-path event dispatch: Mutable access at each tree level
  • GPU scene upload: CPU describes, GPU renders everything
  • Euler spiral strokes: Mathematically correct parallel curves
  • Sparse collection diffing: Immutable structures with structural sharing
  • Type-driven architecture: Associated types derive state and widget trees
  • 同步树差异对比:视图树是临时的,组件树是持久化的
  • Adapt节点:通过状态切片实现组件组合
  • Id路径事件分发:树结构的每个层级都可对状态进行可变访问
  • GPU场景上传:CPU负责描述,GPU负责所有渲染工作
  • 欧拉螺旋描边:数学上精确的平行曲线实现
  • 稀疏集合差异对比:基于结构共享的不可变数据结构
  • 类型驱动的架构:通过关联类型派生状态树和组件树