feat: refactor to use gpui event instead of local state (#18)
Reviewed-on: #18 Co-authored-by: Ren Amamiya <reya@lume.nu> Co-committed-by: Ren Amamiya <reya@lume.nu>
This commit was merged in pull request #18.
This commit is contained in:
382
.agents/skills/gpui-entity/references/api-reference.md
Normal file
382
.agents/skills/gpui-entity/references/api-reference.md
Normal file
@@ -0,0 +1,382 @@
|
||||
# Entity API Reference
|
||||
|
||||
Complete API documentation for GPUI's entity system.
|
||||
|
||||
## Entity Types
|
||||
|
||||
### Entity<T>
|
||||
|
||||
A strong reference to state of type `T`.
|
||||
|
||||
**Methods:**
|
||||
- `entity_id()` → `EntityId` - Returns unique identifier
|
||||
- `downgrade()` → `WeakEntity<T>` - Creates weak reference
|
||||
- `read(cx)` → `&T` - Immutable access to state
|
||||
- `read_with(cx, |state, cx| ...)` → `R` - Read with closure, returns closure result
|
||||
- `update(cx, |state, cx| ...)` → `R` - Mutable update with `Context<T>`, returns closure result
|
||||
- `update_in(cx, |state, window, cx| ...)` → `R` - Update with `Window` access (requires `AsyncWindowContext` or `VisualTestContext`)
|
||||
|
||||
**Important Notes:**
|
||||
- Trying to update an entity while it's already being updated will panic
|
||||
- Within closures, use the inner `cx` provided to avoid multiple borrow issues
|
||||
- With async contexts, return values are wrapped in `anyhow::Result`
|
||||
|
||||
### WeakEntity<T>
|
||||
|
||||
A weak reference to state of type `T`.
|
||||
|
||||
**Methods:**
|
||||
- `upgrade()` → `Option<Entity<T>>` - Convert to strong reference if still alive
|
||||
- `read_with(cx, |state, cx| ...)` → `Result<R>` - Read if entity exists
|
||||
- `update(cx, |state, cx| ...)` → `Result<R>` - Update if entity exists
|
||||
- `update_in(cx, |state, window, cx| ...)` → `Result<R>` - Update with window if entity exists
|
||||
|
||||
**Use Cases:**
|
||||
- Avoid circular dependencies between entities
|
||||
- Store references in closures/callbacks without preventing cleanup
|
||||
- Optional relationships between components
|
||||
|
||||
**Important:** All operations return `Result` since the entity may no longer exist.
|
||||
|
||||
### AnyEntity
|
||||
|
||||
Dynamically-typed entity handle for storing entities of different types.
|
||||
|
||||
### AnyWeakEntity
|
||||
|
||||
Dynamically-typed weak entity handle.
|
||||
|
||||
## Entity Creation
|
||||
|
||||
### cx.new()
|
||||
|
||||
Create new entity with initial state.
|
||||
|
||||
```rust
|
||||
let entity = cx.new(|cx| MyState {
|
||||
count: 0,
|
||||
name: "Default".to_string(),
|
||||
});
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `cx: &mut App` or other context type
|
||||
- Closure receiving `&mut Context<T>` returning initial state `T`
|
||||
|
||||
**Returns:** `Entity<T>`
|
||||
|
||||
## Entity Operations
|
||||
|
||||
### Reading State
|
||||
|
||||
#### read()
|
||||
|
||||
Direct read-only access to state.
|
||||
|
||||
```rust
|
||||
let count = my_entity.read(cx).count;
|
||||
```
|
||||
|
||||
**Use when:** Simple field access, no context operations needed.
|
||||
|
||||
#### read_with()
|
||||
|
||||
Read with context access in closure.
|
||||
|
||||
```rust
|
||||
let count = my_entity.read_with(cx, |state, cx| {
|
||||
// Can access both state and context
|
||||
state.count
|
||||
});
|
||||
|
||||
// Return multiple values
|
||||
let (count, theme) = my_entity.read_with(cx, |state, cx| {
|
||||
(state.count, cx.theme().clone())
|
||||
});
|
||||
```
|
||||
|
||||
**Use when:** Need context operations, multiple return values, complex logic.
|
||||
|
||||
### Updating State
|
||||
|
||||
#### update()
|
||||
|
||||
Mutable update with `Context<T>`.
|
||||
|
||||
```rust
|
||||
my_entity.update(cx, |state, cx| {
|
||||
state.count += 1;
|
||||
cx.notify(); // Trigger re-render
|
||||
});
|
||||
```
|
||||
|
||||
**Available Operations:**
|
||||
- `cx.notify()` - Trigger re-render
|
||||
- `cx.entity()` - Get current entity
|
||||
- `cx.emit(event)` - Emit event
|
||||
- `cx.spawn(task)` - Spawn async task
|
||||
- Other `Context<T>` methods
|
||||
|
||||
#### update_in()
|
||||
|
||||
Update with both `Window` and `Context<T>` access.
|
||||
|
||||
```rust
|
||||
my_entity.update_in(cx, |state, window, cx| {
|
||||
state.focused = window.is_window_focused();
|
||||
cx.notify();
|
||||
});
|
||||
```
|
||||
|
||||
**Requires:** `AsyncWindowContext` or `VisualTestContext`
|
||||
|
||||
**Use when:** Need window-specific operations like focus state, window bounds, etc.
|
||||
|
||||
## Context Methods for Entities
|
||||
|
||||
### cx.entity()
|
||||
|
||||
Get current entity being updated.
|
||||
|
||||
```rust
|
||||
impl MyComponent {
|
||||
fn some_method(&mut self, cx: &mut Context<Self>) {
|
||||
let current_entity = cx.entity(); // Entity<MyComponent>
|
||||
let weak = current_entity.downgrade();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### cx.observe()
|
||||
|
||||
Observe entity for changes.
|
||||
|
||||
```rust
|
||||
cx.observe(&entity, |this, observed_entity, cx| {
|
||||
// Called when observed_entity.update() calls cx.notify()
|
||||
println!("Entity changed");
|
||||
}).detach();
|
||||
```
|
||||
|
||||
**Returns:** `Subscription` - Call `.detach()` to make permanent
|
||||
|
||||
### cx.subscribe()
|
||||
|
||||
Subscribe to events from entity.
|
||||
|
||||
```rust
|
||||
cx.subscribe(&entity, |this, emitter, event: &SomeEvent, cx| {
|
||||
// Called when emitter emits SomeEvent
|
||||
match event {
|
||||
SomeEvent::DataChanged => {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}).detach();
|
||||
```
|
||||
|
||||
**Returns:** `Subscription` - Call `.detach()` to make permanent
|
||||
|
||||
### cx.observe_new_entities()
|
||||
|
||||
Register callback for new entities of a type.
|
||||
|
||||
```rust
|
||||
cx.observe_new_entities::<MyState>(|entity, cx| {
|
||||
println!("New entity created: {:?}", entity.entity_id());
|
||||
}).detach();
|
||||
```
|
||||
|
||||
## Async Operations
|
||||
|
||||
### cx.spawn()
|
||||
|
||||
Spawn foreground task (UI thread).
|
||||
|
||||
```rust
|
||||
cx.spawn(async move |this, cx| {
|
||||
// `this`: WeakEntity<T>
|
||||
// `cx`: &mut AsyncApp
|
||||
|
||||
let result = some_async_work().await;
|
||||
|
||||
// Update entity safely
|
||||
let _ = this.update(cx, |state, cx| {
|
||||
state.data = result;
|
||||
cx.notify();
|
||||
});
|
||||
}).detach();
|
||||
```
|
||||
|
||||
**Note:** Always use weak entity reference in spawned tasks to prevent retain cycles.
|
||||
|
||||
### cx.background_spawn()
|
||||
|
||||
Spawn background task (background thread).
|
||||
|
||||
```rust
|
||||
cx.background_spawn(async move {
|
||||
// Long-running computation
|
||||
let result = heavy_computation().await;
|
||||
// Cannot directly update entities here
|
||||
// Use channels or spawn foreground task to update
|
||||
}).detach();
|
||||
```
|
||||
|
||||
## Entity Lifecycle
|
||||
|
||||
### Creation
|
||||
|
||||
Entities are created via `cx.new()` and immediately registered in the app.
|
||||
|
||||
### Reference Counting
|
||||
|
||||
- `Entity<T>` is a strong reference (increases reference count)
|
||||
- `WeakEntity<T>` is a weak reference (does not increase reference count)
|
||||
- Cloning `Entity<T>` increases reference count
|
||||
|
||||
### Disposal
|
||||
|
||||
Entities are automatically disposed when all strong references are dropped.
|
||||
|
||||
```rust
|
||||
{
|
||||
let entity = cx.new(|cx| MyState::default());
|
||||
// entity exists
|
||||
} // entity dropped here if no other strong references exist
|
||||
```
|
||||
|
||||
**Memory Leak Prevention:**
|
||||
- Use `WeakEntity` in closures/callbacks
|
||||
- Use `WeakEntity` for parent-child relationships
|
||||
- Avoid circular strong references
|
||||
|
||||
## EntityId
|
||||
|
||||
Every entity has a unique identifier.
|
||||
|
||||
```rust
|
||||
let id: EntityId = entity.entity_id();
|
||||
|
||||
// EntityIds can be compared
|
||||
if entity1.entity_id() == entity2.entity_id() {
|
||||
// Same entity
|
||||
}
|
||||
```
|
||||
|
||||
**Use Cases:**
|
||||
- Debugging and logging
|
||||
- Entity comparison without borrowing
|
||||
- Hash maps keyed by entity
|
||||
|
||||
## Error Handling
|
||||
|
||||
### WeakEntity Operations
|
||||
|
||||
All `WeakEntity` operations return `Result`:
|
||||
|
||||
```rust
|
||||
let weak = entity.downgrade();
|
||||
|
||||
// Handle potential failure
|
||||
match weak.read_with(cx, |state, cx| state.count) {
|
||||
Ok(count) => println!("Count: {}", count),
|
||||
Err(e) => eprintln!("Entity no longer exists: {}", e),
|
||||
}
|
||||
|
||||
// Or use Result combinators
|
||||
let _ = weak.update(cx, |state, cx| {
|
||||
state.count += 1;
|
||||
cx.notify();
|
||||
}).ok(); // Ignore errors
|
||||
```
|
||||
|
||||
### Update Panics
|
||||
|
||||
Nested updates on the same entity will panic:
|
||||
|
||||
```rust
|
||||
// ❌ Will panic
|
||||
entity.update(cx, |state1, cx| {
|
||||
entity.update(cx, |state2, cx| {
|
||||
// Panic: entity already borrowed
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Solution:** Perform updates sequentially or use different entities.
|
||||
|
||||
## Type Conversions
|
||||
|
||||
### Entity → WeakEntity
|
||||
|
||||
```rust
|
||||
let entity: Entity<T> = cx.new(|cx| T::default());
|
||||
let weak: WeakEntity<T> = entity.downgrade();
|
||||
```
|
||||
|
||||
### WeakEntity → Entity
|
||||
|
||||
```rust
|
||||
let weak: WeakEntity<T> = entity.downgrade();
|
||||
let strong: Option<Entity<T>> = weak.upgrade();
|
||||
```
|
||||
|
||||
### AnyEntity
|
||||
|
||||
```rust
|
||||
let any: AnyEntity = entity.into();
|
||||
let typed: Option<Entity<T>> = any.downcast::<T>();
|
||||
```
|
||||
|
||||
## Best Practice Guidelines
|
||||
|
||||
### Always Use Inner cx
|
||||
|
||||
```rust
|
||||
// ✅ Good: Use inner cx
|
||||
entity.update(cx, |state, inner_cx| {
|
||||
inner_cx.notify(); // Use inner_cx, not outer cx
|
||||
});
|
||||
|
||||
// ❌ Bad: Use outer cx
|
||||
entity.update(cx, |state, inner_cx| {
|
||||
cx.notify(); // Wrong! Multiple borrow error
|
||||
});
|
||||
```
|
||||
|
||||
### Weak References in Closures
|
||||
|
||||
```rust
|
||||
// ✅ Good: Weak reference
|
||||
let weak = cx.entity().downgrade();
|
||||
callback(move || {
|
||||
let _ = weak.update(cx, |state, cx| {
|
||||
cx.notify();
|
||||
});
|
||||
});
|
||||
|
||||
// ❌ Bad: Strong reference (retain cycle)
|
||||
let strong = cx.entity();
|
||||
callback(move || {
|
||||
strong.update(cx, |state, cx| {
|
||||
// May never be dropped
|
||||
cx.notify();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Sequential Updates
|
||||
|
||||
```rust
|
||||
// ✅ Good: Sequential updates
|
||||
entity1.update(cx, |state, cx| { /* ... */ });
|
||||
entity2.update(cx, |state, cx| { /* ... */ });
|
||||
|
||||
// ❌ Bad: Nested updates
|
||||
entity1.update(cx, |_, cx| {
|
||||
entity2.update(cx, |_, cx| {
|
||||
// May panic if entities are related
|
||||
});
|
||||
});
|
||||
```
|
||||
Reference in New Issue
Block a user