返回 Skill 列表
extension
分类: 开发与工程无需 API Key

gpui-entities

在GPUI中用于状态管理的实体和上下文模式。当管理组件状态、实现实体生命周期、处理订阅或避免常见的实体访问陷阱时使用。

person作者: jakexiaohubgithub

GPUI Entities

This skill provides comprehensive patterns for working with entities and contexts in GPUI applications.

Overview

Entities are GPUI's core state management primitive:

  • Type-safe handles to state with automatic lifecycle management
  • Read/write access with borrow checking at runtime
  • Weak references to avoid memory leaks
  • Subscriptions for observing state changes

Creating Entities

In Window Context

use gpui::*;

struct MyView {
    count: usize,
}

fn main() {
    let app = Application::new();
    
    app.run(move |cx| {
        cx.spawn(async move |cx| {
            cx.open_window(WindowOptions::default(), |window, cx| {
                // Create entity with cx.new()
                cx.new(|_cx| MyView { count: 0 })
            })?;
            
            Ok::<_, anyhow::Error>(())
        }).detach();
    });
}

In Entity Context

struct ParentView {
    child: Entity<ChildView>,
}

impl ParentView {
    fn new(cx: &mut Context<Self>) -> Self {
        let child = cx.new(|_cx| ChildView::new());
        Self { child }
    }
}

Reading Entities

Direct Read

fn display_count(entity: &Entity<MyView>, cx: &App) {
    let view = entity.read(cx);
    println!("Count: {}", view.count);
}

Read with Closure

fn get_count(entity: &Entity<MyView>, cx: &App) -> usize {
    entity.read_with(cx, |view, _cx| view.count)
}

Important: The closure return value is returned directly from .read_with().

Updating Entities

Basic Update

fn increment(entity: &Entity<MyView>, cx: &mut App) {
    entity.update(cx, |view, cx| {
        view.count += 1;
        cx.notify(); // Trigger re-render
    });
}

Update with Window

Use .update_in() when you need window access:

fn increment_and_dispatch(
    entity: &Entity<MyView>,
    cx: &mut AsyncWindowContext,
) -> anyhow::Result<()> {
    entity.update_in(cx, |view, window, cx| {
        view.count += 1;
        window.dispatch_action(CountChanged.boxed_clone(), cx);
        cx.notify();
    })
}

Return Values

// update returns the closure's return value
let new_count = entity.update(cx, |view, cx| {
    view.count += 1;
    cx.notify();
    view.count // Return new count
});

println!("New count: {}", new_count);

WeakEntity

Use WeakEntity<T> to avoid memory leaks when entities reference each other.

Creating Weak References

struct Parent {
    child: Entity<Child>,
}

struct Child {
    parent: WeakEntity<Parent>, // Weak to avoid cycle
}

impl Parent {
    fn new(cx: &mut Context<Self>) -> Self {
        let parent_weak = cx.entity().downgrade();
        
        let child = cx.new(|_cx| Child {
            parent: parent_weak,
        });
        
        Self { child }
    }
}

Using Weak References

impl Child {
    fn notify_parent(&self, cx: &mut Context<Self>) {
        if let Some(parent) = self.parent.upgrade() {
            parent.update(cx, |parent, cx| {
                parent.handle_child_event(cx);
            });
        }
    }
}

Async Context with WeakEntity

When using async contexts, methods return anyhow::Result:

fn do_async_work(&self, cx: &mut Context<Self>) {
    cx.spawn(async move |this, cx| {
        // this: WeakEntity<Self>
        
        // Async work
        tokio::time::sleep(Duration::from_secs(1)).await;
        
        // Update returns Result in async context
        this.update(&mut *cx, |view, cx| {
            view.count += 1;
            cx.notify();
        })?;
        
        Ok(())
    }).detach();
}

Entity Lifecycle

Entity ID

let id = entity.entity_id();
// id: EntityId - unique identifier that persists

Checking Entity Validity

// Strong entity is always valid
let entity: Entity<MyView> = cx.new(|_| MyView { count: 0 });
// entity is guaranteed to be valid

// Weak entity might be invalid
let weak: WeakEntity<MyView> = entity.downgrade();

if let Some(entity) = weak.upgrade() {
    // Entity still exists
    entity.update(cx, |view, cx| {
        // ...
    });
} else {
    // Entity was dropped
    println!("Entity no longer exists");
}

Subscriptions

Subscribe to entity events to observe state changes.

Defining Events

use gpui::*;

#[derive(Clone, Debug)]
enum MyEvent {
    CountChanged { new_value: usize },
    Reset,
}

// Declare that this entity can emit MyEvent
impl EventEmitter<MyEvent> for MyView {}

Emitting Events

impl MyView {
    fn increment(&mut self, cx: &mut Context<Self>) {
        self.count += 1;
        cx.emit(MyEvent::CountChanged { new_value: self.count });
        cx.notify();
    }
    
    fn reset(&mut self, cx: &mut Context<Self>) {
        self.count = 0;
        cx.emit(MyEvent::Reset);
        cx.notify();
    }
}

Subscribing to Events

struct ParentView {
    child: Entity<ChildView>,
    _subscription: Subscription,
}

impl ParentView {
    fn new(cx: &mut Context<Self>) -> Self {
        let child = cx.new(|_| ChildView::new());
        
        let subscription = cx.subscribe(&child, |this, child, event, cx| {
            // this: &mut ParentView
            // child: Entity<ChildView>
            // event: &MyEvent
            // cx: &mut Context<ParentView>
            
            match event {
                MyEvent::CountChanged { new_value } => {
                    println!("Child count changed: {}", new_value);
                }
                MyEvent::Reset => {
                    println!("Child was reset");
                }
            }
        });
        
        Self {
            child,
            _subscription: subscription, // Keeps subscription alive
        }
    }
}

Important: Store Subscription in a field to keep it active. When dropped, the subscription is automatically cancelled.

Observing Changes

Use cx.observe() to be notified when an entity calls cx.notify():

struct Dashboard {
    counter: Entity<Counter>,
    _observer: Subscription,
}

impl Dashboard {
    fn new(cx: &mut Context<Self>) -> Self {
        let counter = cx.new(|_| Counter { value: 0 });
        
        let observer = cx.observe(&counter, |this, observed, cx| {
            // Called whenever counter.cx.notify() is called
            // observed: Entity<Counter>
            println!("Counter changed!");
            cx.notify(); // Re-render dashboard
        });
        
        Self {
            counter,
            _observer: observer,
        }
    }
}

Context Methods

Getting Entity Reference

From within an entity's method:

impl MyView {
    fn get_self_reference(&self, cx: &Context<Self>) -> WeakEntity<Self> {
        cx.entity().downgrade()
    }
}

Notify

Trigger re-render for this entity:

impl MyView {
    fn update_state(&mut self, cx: &mut Context<Self>) {
        self.count += 1;
        cx.notify(); // Mark this entity as needing re-render
    }
}

Common Patterns

Parent-Child Communication

struct Parent {
    child: Entity<Child>,
}

impl Parent {
    fn tell_child_something(&self, cx: &mut Context<Self>) {
        self.child.update(cx, |child, cx| {
            child.handle_message_from_parent(cx);
        });
    }
}

struct Child {
    parent: WeakEntity<Parent>,
}

impl Child {
    fn tell_parent_something(&self, cx: &mut Context<Self>) {
        if let Some(parent) = self.parent.upgrade() {
            parent.update(cx, |parent, cx| {
                parent.handle_message_from_child(cx);
            });
        }
    }
}

Shared State Entity

#[derive(Clone)]
struct AppState {
    theme: String,
    user: Option<String>,
}

struct MyApp {
    state: Entity<AppState>,
}

impl MyApp {
    fn new(cx: &mut Context<Self>) -> Self {
        let state = cx.new(|_| AppState {
            theme: "dark".into(),
            user: None,
        });
        
        Self { state }
    }
    
    fn change_theme(&self, theme: String, cx: &mut Context<Self>) {
        self.state.update(cx, |state, cx| {
            state.theme = theme;
            cx.notify();
        });
    }
}

Entity as Global State

fn set_global_state(cx: &mut App, state: Entity<AppState>) {
    cx.set_global(state);
}

fn get_global_state(cx: &App) -> Entity<AppState> {
    cx.global::<Entity<AppState>>().clone()
}

// Usage
impl MyView {
    fn access_global(&self, cx: &Context<Self>) {
        let state = get_global_state(cx);
        state.read_with(cx, |state, _| {
            println!("Theme: {}", state.theme);
        });
    }
}

Common Mistakes

| Mistake | Problem | Solution | |---------|---------|----------| | Using outer cx in update | Borrow checker error | Use inner cx from closure | | Nested updates | Runtime panic | Restructure to avoid updating while updating | | Forgetting cx.notify() | UI doesn't update | Call cx.notify() after state changes | | Strong circular references | Memory leak | Use WeakEntity for back-references | | Dropping Subscription | Events stop working | Store Subscription in struct field | | Not handling None from weak upgrade | Panic or undefined behavior | Always check upgrade() result |

Best Practices

  1. Use WeakEntity for back-references: Prevents memory leaks
  2. Store subscriptions: Keep Subscription values in struct fields
  3. Always call cx.notify(): After state changes that affect rendering
  4. Handle weak reference failures: Always check if upgrade() returns Some
  5. Use read_with for simple access: More ergonomic than .read()
  6. Avoid long-lived strong references: Consider WeakEntity if entity won't be used frequently

Summary

  • Create with cx.new()
  • Read with .read() or .read_with()
  • Update with .update() or .update_in()
  • Use WeakEntity to avoid cycles
  • Subscribe with cx.subscribe() for events
  • Observe with cx.observe() for notify signals
  • Always call cx.notify() after state changes

References