home / skills / copyleftdev / sk1llz / levien

levien skill

/languages/rust/levien

This skill helps you design declarative, GPU-accelerated native UIs with synchronized trees and Rust patterns inspired by Raph Levien.

npx playbooks add skill copyleftdev/sk1llz --skill levien

Review the files below or copy the command above to add this skill to your agents.

Files (3)
SKILL.md
10.9 KB
---
name: levien-native-ui-mastery
description: Build native UIs in the style of Raph Levien, architect of Druid, Xilem, and Vello. Emphasizes declarative reactive architecture, synchronized tree transformations, GPU-accelerated rendering, and idiomatic Rust patterns. Use when designing responsive, beautiful native UIs or 2D graphics systems.
---

# Raph Levien Style Guide

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

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

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

## When Building 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

### 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

### 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

## 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
```

### Adapt Nodes for Composition

```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

```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"
```

### GPU Scene Description

```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
```

## 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

## Raph's Design Questions

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?

## 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

Overview

This skill teaches how to build native UIs in the style of Raph Levien, emphasizing declarative reactive architecture, synchronized tree transformations, GPU-accelerated rendering, and idiomatic Rust patterns. It distills practical patterns like view/widget/render trees, adapt nodes for state slicing, id-path event routing, and memoized incremental updates. Use it to design responsive, accessible, and high-performance native UIs or 2D graphics systems.

How this skill works

The skill frames UI as a pipeline of tree transformations: an ephemeral view tree describes intent, a retained widget tree holds state, and a render tree computes layout and paint. It recommends diffing the view tree to mutate the widget tree incrementally, routing events by id-path with mutable access at each level, and uploading compact scene descriptions to the GPU for massively parallel rendering. Composition uses adapt nodes to slice state and memoization to avoid unnecessary rebuilds.

When to use it

  • Building a native app or 2D graphics renderer that must scale in complexity and performance
  • Designing UI architectures in Rust where ownership and borrow rules constrain shared mutability
  • Creating accessible interfaces that require stable identity and retained structure
  • Implementing virtualized lists or interfaces with tens of thousands of items
  • Optimizing rendering by offloading rasterization and tiling to the GPU

Best practices

  • Model UI as view → widget → render pipeline and keep responsibilities distinct
  • Prefer declarative view descriptions and avoid global shared mutable state
  • Use adapt nodes to give components only the state they need
  • Memoize and diff sparsely; rebuild only changed subtrees using stable ids
  • Describe scenes on the CPU but push heavy work (flattening, rasterization) to GPU compute shaders
  • Design ids and state lifetimes for accessibility and deterministic event routing

Example use cases

  • A document editor that maintains rich widget state while producing GPU-accelerated previews
  • A virtualized file browser with 100k items using sparse collection diffing and memoized rows
  • A custom UI toolkit in Rust that needs ergonomic typesafe views and retained widget state
  • A vector renderer that encodes scenes on CPU and executes path flattening and rasterization on the GPU
  • An accessible application where stable ids enable screen reader traversal and precise event handling

FAQ

Why avoid shared mutable state like Rc<RefCell<T>>?

Shared mutable state often hides architectural issues; the synchronized tree model gives clear ownership and controlled mutation during event routing, which fits Rust's borrow checker and leads to safer, more maintainable code.

When should work run on the GPU vs CPU?

Keep scene description and high-level composition on the CPU, but push heavy parallel work—path flattening, tiling, rasterization, compositing—to the GPU to exploit massive parallelism and reduce CPU serialization.