add gpui skills

This commit is contained in:
2026-03-08 08:41:46 +07:00
parent 34a32a1bd8
commit aec32e450a
21 changed files with 7057 additions and 0 deletions

View File

@@ -0,0 +1,180 @@
---
name: gpui-action
description: Action definitions and keyboard shortcuts in GPUI. Use when implementing actions, keyboard shortcuts, or key bindings.
---
## Overview
Actions provide declarative keyboard-driven UI interactions in GPUI.
**Key Concepts:**
- Define actions with `actions!` macro or `#[derive(Action)]`
- Bind keys with `cx.bind_keys()`
- Handle with `.on_action()` on elements
- Context-aware via `key_context()`
## Quick Start
### Simple Actions
```rust
use gpui::actions;
actions!(editor, [MoveUp, MoveDown, Save, Quit]);
const CONTEXT: &str = "Editor";
pub fn init(cx: &mut App) {
cx.bind_keys([
KeyBinding::new("up", MoveUp, Some(CONTEXT)),
KeyBinding::new("down", MoveDown, Some(CONTEXT)),
KeyBinding::new("cmd-s", Save, Some(CONTEXT)),
KeyBinding::new("cmd-q", Quit, Some(CONTEXT)),
]);
}
impl Render for Editor {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.key_context(CONTEXT)
.on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down))
.on_action(cx.listener(Self::save))
}
}
impl Editor {
fn move_up(&mut self, _: &MoveUp, cx: &mut Context<Self>) {
// Handle move up
cx.notify();
}
fn move_down(&mut self, _: &MoveDown, cx: &mut Context<Self>) {
cx.notify();
}
fn save(&mut self, _: &Save, cx: &mut Context<Self>) {
// Save logic
cx.notify();
}
}
```
### Actions with Parameters
```rust
#[derive(Clone, PartialEq, Action, Deserialize)]
#[action(namespace = editor)]
pub struct InsertText {
pub text: String,
}
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
#[action(namespace = editor, no_json)]
pub struct Digit(pub u8);
cx.bind_keys([
KeyBinding::new("0", Digit(0), Some(CONTEXT)),
KeyBinding::new("1", Digit(1), Some(CONTEXT)),
// ...
]);
impl Editor {
fn on_digit(&mut self, action: &Digit, cx: &mut Context<Self>) {
self.insert_digit(action.0, cx);
}
}
```
## Key Formats
```rust
// Modifiers
"cmd-s" // Command (macOS) / Ctrl (Windows/Linux)
"ctrl-c" // Control
"alt-f" // Alt
"shift-tab" // Shift
"cmd-ctrl-f" // Multiple modifiers
// Keys
"a-z", "0-9" // Letters and numbers
"f1-f12" // Function keys
"up", "down", "left", "right"
"enter", "escape", "space", "tab"
"backspace", "delete"
"-", "=", "[", "]", etc. // Special characters
```
## Action Naming
Prefer verb-noun pattern:
```rust
actions!([
OpenFile, // ✅ Good
CloseWindow, // ✅ Good
ToggleSidebar, // ✅ Good
Save, // ✅ Good (common exception)
]);
```
## Context-Aware Bindings
```rust
const EDITOR_CONTEXT: &str = "Editor";
const MODAL_CONTEXT: &str = "Modal";
// Same key, different contexts
cx.bind_keys([
KeyBinding::new("escape", CloseModal, Some(MODAL_CONTEXT)),
KeyBinding::new("escape", ClearSelection, Some(EDITOR_CONTEXT)),
]);
// Set context on element
div()
.key_context(EDITOR_CONTEXT)
.child(editor_content)
```
## Best Practices
### ✅ Use Contexts
```rust
// ✅ Good: Context-aware
div()
.key_context("MyComponent")
.on_action(cx.listener(Self::handle))
```
### ✅ Name Actions Clearly
```rust
// ✅ Good: Clear intent
actions!([
SaveDocument,
CloseTab,
TogglePreview,
]);
```
### ✅ Handle with Listeners
```rust
// ✅ Good: Proper handler naming
impl MyComponent {
fn on_action_save(&mut self, _: &Save, cx: &mut Context<Self>) {
// Handle save
cx.notify();
}
}
div().on_action(cx.listener(Self::on_action_save))
```
## Reference Documentation
- **Complete Guide**: See [reference.md](references/reference.md)
- Action definition, keybinding, dispatch
- Focus-based routing, best practices
- Performance, accessibility

View File

@@ -0,0 +1,175 @@
---
name: gpui-async
description: Async operations and background tasks in GPUI. Use when working with async, spawn, background tasks, or concurrent operations. Essential for handling async I/O, long-running computations, and coordinating between foreground UI updates and background work.
---
## Overview
GPUI provides integrated async runtime for foreground UI updates and background computation.
**Key Concepts:**
- **Foreground tasks**: UI thread, can update entities (`cx.spawn`)
- **Background tasks**: Worker threads, CPU-intensive work (`cx.background_spawn`)
- All entity updates happen on foreground thread
## Quick Start
### Foreground Tasks (UI Updates)
```rust
impl MyComponent {
fn fetch_data(&mut self, cx: &mut Context<Self>) {
let entity = cx.entity().downgrade();
cx.spawn(async move |cx| {
// Runs on UI thread, can await and update entities
let data = fetch_from_api().await;
entity.update(cx, |state, cx| {
state.data = Some(data);
cx.notify();
}).ok();
}).detach();
}
}
```
### Background Tasks (Heavy Work)
```rust
impl MyComponent {
fn process_file(&mut self, cx: &mut Context<Self>) {
let entity = cx.entity().downgrade();
cx.background_spawn(async move {
// Runs on background thread, CPU-intensive
let result = heavy_computation().await;
result
})
.then(cx.spawn(move |result, cx| {
// Back to foreground to update UI
entity.update(cx, |state, cx| {
state.result = result;
cx.notify();
}).ok();
}))
.detach();
}
}
```
### Task Management
```rust
struct MyView {
_task: Task<()>, // Prefix with _ if stored but not accessed
}
impl MyView {
fn new(cx: &mut Context<Self>) -> Self {
let entity = cx.entity().downgrade();
let _task = cx.spawn(async move |cx| {
// Task automatically cancelled when dropped
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
entity.update(cx, |state, cx| {
state.tick();
cx.notify();
}).ok();
}
});
Self { _task }
}
}
```
## Core Patterns
### 1. Async Data Fetching
```rust
cx.spawn(async move |cx| {
let data = fetch_data().await?;
entity.update(cx, |state, cx| {
state.data = Some(data);
cx.notify();
})?;
Ok::<_, anyhow::Error>(())
}).detach();
```
### 2. Background Computation + UI Update
```rust
cx.background_spawn(async move {
heavy_work()
})
.then(cx.spawn(move |result, cx| {
entity.update(cx, |state, cx| {
state.result = result;
cx.notify();
}).ok();
}))
.detach();
```
### 3. Periodic Tasks
```rust
cx.spawn(async move |cx| {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
// Update every 5 seconds
}
}).detach();
```
### 4. Task Cancellation
Tasks are automatically cancelled when dropped. Store in struct to keep alive.
## Common Pitfalls
### ❌ Don't: Update entities from background tasks
```rust
// ❌ Wrong: Can't update entities from background thread
cx.background_spawn(async move {
entity.update(cx, |state, cx| { // Compile error!
state.data = data;
});
});
```
### ✅ Do: Use foreground task or chain
```rust
// ✅ Correct: Chain with foreground task
cx.background_spawn(async move { data })
.then(cx.spawn(move |data, cx| {
entity.update(cx, |state, cx| {
state.data = data;
cx.notify();
}).ok();
}))
.detach();
```
## Reference Documentation
### Complete Guides
- **API Reference**: See [api-reference.md](references/api-reference.md)
- Task types, spawning methods, contexts
- Executors, cancellation, error handling
- **Patterns**: See [patterns.md](references/patterns.md)
- Data fetching, background processing
- Polling, debouncing, parallel tasks
- Pattern selection guide
- **Best Practices**: See [best-practices.md](references/best-practices.md)
- Error handling, cancellation
- Performance optimization, testing
- Common pitfalls and solutions

View File

@@ -0,0 +1,161 @@
---
name: gpui-context
description: Context management in GPUI including App, Window, and AsyncApp. Use when working with contexts, entity updates, or window operations. Different context types provide different capabilities for UI rendering, entity management, and async operations.
---
## Overview
GPUI uses different context types for different scenarios:
**Context Types:**
- **`App`**: Global app state, entity creation
- **`Window`**: Window-specific operations, painting, layout
- **`Context<T>`**: Entity-specific context for component `T`
- **`AsyncApp`**: Async context for foreground tasks
- **`AsyncWindowContext`**: Async context with window access
## Quick Start
### Context<T> - Component Context
```rust
impl MyComponent {
fn update_state(&mut self, cx: &mut Context<Self>) {
self.value = 42;
cx.notify(); // Trigger re-render
// Spawn async task
cx.spawn(async move |cx| {
// Async work
}).detach();
// Get current entity
let entity = cx.entity();
}
}
```
### App - Global Context
```rust
fn main() {
let app = Application::new();
app.run(|cx: &mut App| {
// Create entities
let entity = cx.new(|cx| MyState::default());
// Open windows
cx.open_window(WindowOptions::default(), |window, cx| {
cx.new(|cx| Root::new(view, window, cx))
});
});
}
```
### Window - Window Context
```rust
impl Render for MyView {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
// Window operations
let is_focused = window.is_window_focused();
let bounds = window.bounds();
div().child("Content")
}
}
```
### AsyncApp - Async Context
```rust
cx.spawn(async move |cx: &mut AsyncApp| {
let data = fetch_data().await;
entity.update(cx, |state, inner_cx| {
state.data = data;
inner_cx.notify();
}).ok();
}).detach();
```
## Common Operations
### Entity Operations
```rust
// Create entity
let entity = cx.new(|cx| MyState::default());
// Update entity
entity.update(cx, |state, cx| {
state.value = 42;
cx.notify();
});
// Read entity
let value = entity.read(cx).value;
```
### Notifications and Events
```rust
// Trigger re-render
cx.notify();
// Emit event
cx.emit(MyEvent::Updated);
// Observe entity
cx.observe(&entity, |this, observed, cx| {
// React to changes
}).detach();
// Subscribe to events
cx.subscribe(&entity, |this, source, event, cx| {
// Handle event
}).detach();
```
### Window Operations
```rust
// Window state
let focused = window.is_window_focused();
let bounds = window.bounds();
let scale = window.scale_factor();
// Close window
window.remove_window();
```
### Async Operations
```rust
// Spawn foreground task
cx.spawn(async move |cx| {
// Async work with entity access
}).detach();
// Spawn background task
cx.background_spawn(async move {
// Heavy computation
}).detach();
```
## Context Hierarchy
```
App (Global)
└─ Window (Per-window)
└─ Context<T> (Per-component)
└─ AsyncApp (In async tasks)
└─ AsyncWindowContext (Async + Window)
```
## Reference Documentation
- **API Reference**: See [api-reference.md](references/api-reference.md)
- Complete context API, methods, conversions
- Entity operations, window operations
- Async contexts, best practices

View File

@@ -0,0 +1,126 @@
---
name: gpui-element
description: Implementing custom elements using GPUI's low-level Element API (vs. high-level Render/RenderOnce APIs). Use when you need maximum control over layout, prepaint, and paint phases for complex, performance-critical custom UI components that cannot be achieved with Render/RenderOnce traits.
---
## When to Use
Use the low-level `Element` trait when:
- Need fine-grained control over layout calculation
- Building complex, performance-critical components
- Implementing custom layout algorithms (masonry, circular, etc.)
- High-level `Render`/`RenderOnce` APIs are insufficient
**Prefer `Render`/`RenderOnce` for:** Simple components, standard layouts, declarative UI
## Quick Start
The `Element` trait provides direct control over three rendering phases:
```rust
impl Element for MyElement {
type RequestLayoutState = MyLayoutState; // Data passed to later phases
type PrepaintState = MyPaintState; // Data for painting
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
// Phase 1: Calculate sizes and positions
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, Self::RequestLayoutState)
{
let layout_id = window.request_layout(
Style { size: size(px(200.), px(100.)), ..default() },
vec![],
cx
);
(layout_id, MyLayoutState { /* ... */ })
}
// Phase 2: Create hitboxes, prepare for painting
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, layout: &mut Self::RequestLayoutState,
window: &mut Window, cx: &mut App) -> Self::PrepaintState
{
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
MyPaintState { hitbox }
}
// Phase 3: Render and handle interactions
fn paint(&mut self, .., bounds: Bounds<Pixels>, layout: &mut Self::RequestLayoutState,
paint_state: &mut Self::PrepaintState, window: &mut Window, cx: &mut App)
{
window.paint_quad(paint_quad(bounds, Corners::all(px(4.)), cx.theme().background));
window.on_mouse_event({
let hitbox = paint_state.hitbox.clone();
move |event: &MouseDownEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
// Handle interaction
cx.stop_propagation();
}
}
});
}
}
// Enable element to be used as child
impl IntoElement for MyElement {
type Element = Self;
fn into_element(self) -> Self::Element { self }
}
```
## Core Concepts
### Three-Phase Rendering
1. **request_layout**: Calculate sizes and positions, return layout ID and state
2. **prepaint**: Create hitboxes, compute final bounds, prepare for painting
3. **paint**: Render element, set up interactions (mouse events, cursor styles)
### State Flow
```
RequestLayoutState → PrepaintState → paint
```
State flows in one direction through associated types, passed as mutable references between phases.
### Key Operations
- **Layout**: `window.request_layout(style, children, cx)` - Create layout node
- **Hitboxes**: `window.insert_hitbox(bounds, behavior)` - Create interaction area
- **Painting**: `window.paint_quad(...)` - Render visual content
- **Events**: `window.on_mouse_event(handler)` - Handle user input
## Reference Documentation
### Complete API Documentation
- **Element Trait API**: See [api-reference.md](references/api-reference.md)
- Associated types, methods, parameters, return values
- Hitbox system, event handling, cursor styles
### Implementation Guides
- **Examples**: See [examples.md](references/examples.md)
- Simple text element with highlighting
- Interactive element with selection
- Complex element with child management
- **Best Practices**: See [best-practices.md](references/best-practices.md)
- State management, performance optimization
- Interaction handling, layout strategies
- Error handling, testing, common pitfalls
- **Common Patterns**: See [patterns.md](references/patterns.md)
- Text rendering, container, interactive, composite, scrollable patterns
- Pattern selection guide
- **Advanced Patterns**: See [advanced-patterns.md](references/advanced-patterns.md)
- Custom layout algorithms (masonry, circular)
- Element composition with traits
- Async updates, memoization, virtual lists

View File

@@ -0,0 +1,705 @@
# Advanced Element Patterns
Advanced techniques and patterns for implementing sophisticated GPUI elements.
## Custom Layout Algorithms
Implementing custom layout algorithms not supported by GPUI's built-in layouts.
### Masonry Layout (Pinterest-Style)
```rust
pub struct MasonryLayout {
id: ElementId,
columns: usize,
gap: Pixels,
children: Vec<AnyElement>,
}
struct MasonryLayoutState {
column_layouts: Vec<Vec<LayoutId>>,
column_heights: Vec<Pixels>,
}
struct MasonryPaintState {
child_bounds: Vec<Bounds<Pixels>>,
}
impl Element for MasonryLayout {
type RequestLayoutState = MasonryLayoutState;
type PrepaintState = MasonryPaintState;
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, MasonryLayoutState) {
// Initialize columns
let mut columns: Vec<Vec<LayoutId>> = vec![Vec::new(); self.columns];
let mut column_heights = vec![px(0.); self.columns];
// Distribute children across columns
for child in &mut self.children {
let (child_layout_id, _) = child.request_layout(
global_id,
inspector_id,
window,
cx
);
let child_size = window.layout_bounds(child_layout_id).size;
// Find shortest column
let min_column_idx = column_heights
.iter()
.enumerate()
.min_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.unwrap()
.0;
// Add child to shortest column
columns[min_column_idx].push(child_layout_id);
column_heights[min_column_idx] += child_size.height + self.gap;
}
// Calculate total layout size
let column_width = px(200.); // Fixed column width
let total_width = column_width * self.columns as f32
+ self.gap * (self.columns - 1) as f32;
let total_height = column_heights.iter()
.max_by(|a, b| a.partial_cmp(b).unwrap())
.copied()
.unwrap_or(px(0.));
let layout_id = window.request_layout(
Style {
size: size(total_width, total_height),
..default()
},
columns.iter().flatten().copied().collect(),
cx
);
(layout_id, MasonryLayoutState {
column_layouts: columns,
column_heights,
})
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
layout_state: &mut MasonryLayoutState,
window: &mut Window,
cx: &mut App
) -> MasonryPaintState {
let column_width = px(200.);
let mut child_bounds = Vec::new();
// Position children in columns
for (col_idx, column) in layout_state.column_layouts.iter().enumerate() {
let x_offset = bounds.left()
+ (column_width + self.gap) * col_idx as f32;
let mut y_offset = bounds.top();
for (child_idx, layout_id) in column.iter().enumerate() {
let child_size = window.layout_bounds(*layout_id).size;
let child_bound = Bounds::new(
point(x_offset, y_offset),
size(column_width, child_size.height)
);
self.children[child_idx].prepaint(
global_id,
inspector_id,
child_bound,
window,
cx
);
child_bounds.push(child_bound);
y_offset += child_size.height + self.gap;
}
}
MasonryPaintState { child_bounds }
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_layout_state: &mut MasonryLayoutState,
paint_state: &mut MasonryPaintState,
window: &mut Window,
cx: &mut App
) {
for (child, bounds) in self.children.iter_mut().zip(&paint_state.child_bounds) {
child.paint(global_id, inspector_id, *bounds, window, cx);
}
}
}
```
### Circular Layout
```rust
pub struct CircularLayout {
id: ElementId,
radius: Pixels,
children: Vec<AnyElement>,
}
impl Element for CircularLayout {
type RequestLayoutState = Vec<LayoutId>;
type PrepaintState = Vec<Bounds<Pixels>>;
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, Vec<LayoutId>) {
let child_layouts: Vec<_> = self.children
.iter_mut()
.map(|child| child.request_layout(global_id, inspector_id, window, cx).0)
.collect();
let diameter = self.radius * 2.;
let layout_id = window.request_layout(
Style {
size: size(diameter, diameter),
..default()
},
child_layouts.clone(),
cx
);
(layout_id, child_layouts)
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
layout_ids: &mut Vec<LayoutId>,
window: &mut Window,
cx: &mut App
) -> Vec<Bounds<Pixels>> {
let center = bounds.center();
let angle_step = 2.0 * std::f32::consts::PI / self.children.len() as f32;
let mut child_bounds = Vec::new();
for (i, (child, layout_id)) in self.children.iter_mut()
.zip(layout_ids.iter())
.enumerate()
{
let angle = angle_step * i as f32;
let child_size = window.layout_bounds(*layout_id).size;
// Position child on circle
let x = center.x + self.radius * angle.cos() - child_size.width / 2.;
let y = center.y + self.radius * angle.sin() - child_size.height / 2.;
let child_bound = Bounds::new(point(x, y), child_size);
child.prepaint(global_id, inspector_id, child_bound, window, cx);
child_bounds.push(child_bound);
}
child_bounds
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_layout_ids: &mut Vec<LayoutId>,
child_bounds: &mut Vec<Bounds<Pixels>>,
window: &mut Window,
cx: &mut App
) {
for (child, bounds) in self.children.iter_mut().zip(child_bounds) {
child.paint(global_id, inspector_id, *bounds, window, cx);
}
}
}
```
## Element Composition with Traits
Create reusable behaviors via traits for element composition.
### Hoverable Trait
```rust
pub trait Hoverable: Element {
fn on_hover<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&mut Window, &mut App) + 'static;
fn on_hover_end<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&mut Window, &mut App) + 'static;
}
// Implementation for custom element
pub struct HoverableElement {
id: ElementId,
content: AnyElement,
hover_handlers: Vec<Box<dyn Fn(&mut Window, &mut App)>>,
hover_end_handlers: Vec<Box<dyn Fn(&mut Window, &mut App)>>,
was_hovered: bool,
}
impl Hoverable for HoverableElement {
fn on_hover<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&mut Window, &mut App) + 'static
{
self.hover_handlers.push(Box::new(f));
self
}
fn on_hover_end<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&mut Window, &mut App) + 'static
{
self.hover_end_handlers.push(Box::new(f));
self
}
}
impl Element for HoverableElement {
type RequestLayoutState = LayoutId;
type PrepaintState = Hitbox;
fn paint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_layout: &mut LayoutId,
hitbox: &mut Hitbox,
window: &mut Window,
cx: &mut App
) {
let is_hovered = hitbox.is_hovered(window);
// Trigger hover events
if is_hovered && !self.was_hovered {
for handler in &self.hover_handlers {
handler(window, cx);
}
} else if !is_hovered && self.was_hovered {
for handler in &self.hover_end_handlers {
handler(window, cx);
}
}
self.was_hovered = is_hovered;
// Paint content
self.content.paint(bounds, window, cx);
}
// ... other methods
}
```
### Clickable Trait
```rust
pub trait Clickable: Element {
fn on_click<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static;
fn on_double_click<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static;
}
pub struct ClickableElement {
id: ElementId,
content: AnyElement,
click_handlers: Vec<Box<dyn Fn(&MouseUpEvent, &mut Window, &mut App)>>,
double_click_handlers: Vec<Box<dyn Fn(&MouseUpEvent, &mut Window, &mut App)>>,
last_click_time: Option<Instant>,
}
impl Clickable for ClickableElement {
fn on_click<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static
{
self.click_handlers.push(Box::new(f));
self
}
fn on_double_click<F>(&mut self, f: F) -> &mut Self
where
F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static
{
self.double_click_handlers.push(Box::new(f));
self
}
}
```
## Async Element Updates
Elements that update based on async operations.
```rust
pub struct AsyncElement {
id: ElementId,
state: Entity<AsyncState>,
loading: bool,
data: Option<String>,
}
pub struct AsyncState {
loading: bool,
data: Option<String>,
}
impl Element for AsyncElement {
type RequestLayoutState = ();
type PrepaintState = Hitbox;
fn paint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_layout: &mut (),
hitbox: &mut Hitbox,
window: &mut Window,
cx: &mut App
) {
// Display loading or data
if self.loading {
// Paint loading indicator
self.paint_loading(bounds, window, cx);
} else if let Some(data) = &self.data {
// Paint data
self.paint_data(data, bounds, window, cx);
}
// Trigger async update on click
window.on_mouse_event({
let state = self.state.clone();
let hitbox = hitbox.clone();
move |event: &MouseUpEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
// Spawn async task
cx.spawn({
let state = state.clone();
async move {
// Perform async operation
let result = fetch_data_async().await;
// Update state on completion
state.update(cx, |state, cx| {
state.loading = false;
state.data = Some(result);
cx.notify();
});
}
}).detach();
// Set loading state immediately
state.update(cx, |state, cx| {
state.loading = true;
cx.notify();
});
cx.stop_propagation();
}
}
});
}
// ... other methods
}
async fn fetch_data_async() -> String {
// Simulate async operation
tokio::time::sleep(Duration::from_secs(1)).await;
"Data loaded!".to_string()
}
```
## Element Memoization
Optimize performance by memoizing expensive element computations.
```rust
pub struct MemoizedElement<T: PartialEq + Clone + 'static> {
id: ElementId,
value: T,
render_fn: Box<dyn Fn(&T) -> AnyElement>,
cached_element: Option<AnyElement>,
last_value: Option<T>,
}
impl<T: PartialEq + Clone + 'static> MemoizedElement<T> {
pub fn new<F>(id: ElementId, value: T, render_fn: F) -> Self
where
F: Fn(&T) -> AnyElement + 'static,
{
Self {
id,
value,
render_fn: Box::new(render_fn),
cached_element: None,
last_value: None,
}
}
}
impl<T: PartialEq + Clone + 'static> Element for MemoizedElement<T> {
type RequestLayoutState = LayoutId;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, LayoutId) {
// Check if value changed
if self.last_value.as_ref() != Some(&self.value) || self.cached_element.is_none() {
// Recompute element
self.cached_element = Some((self.render_fn)(&self.value));
self.last_value = Some(self.value.clone());
}
// Request layout for cached element
let (layout_id, _) = self.cached_element
.as_mut()
.unwrap()
.request_layout(global_id, inspector_id, window, cx);
(layout_id, layout_id)
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_layout_id: &mut LayoutId,
window: &mut Window,
cx: &mut App
) -> () {
self.cached_element
.as_mut()
.unwrap()
.prepaint(global_id, inspector_id, bounds, window, cx);
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_layout_id: &mut LayoutId,
_: &mut (),
window: &mut Window,
cx: &mut App
) {
self.cached_element
.as_mut()
.unwrap()
.paint(global_id, inspector_id, bounds, window, cx);
}
}
// Usage
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
MemoizedElement::new(
ElementId::Name("memoized".into()),
self.expensive_value.clone(),
|value| {
// Expensive rendering function only called when value changes
div().child(format!("Computed: {}", value))
}
)
}
```
## Virtual List Pattern
Efficiently render large lists by only rendering visible items.
```rust
pub struct VirtualList {
id: ElementId,
item_count: usize,
item_height: Pixels,
viewport_height: Pixels,
scroll_offset: Pixels,
render_item: Box<dyn Fn(usize) -> AnyElement>,
}
struct VirtualListState {
visible_range: Range<usize>,
visible_item_layouts: Vec<LayoutId>,
}
impl Element for VirtualList {
type RequestLayoutState = VirtualListState;
type PrepaintState = Hitbox;
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, VirtualListState) {
// Calculate visible range
let start_idx = (self.scroll_offset / self.item_height).floor() as usize;
let end_idx = ((self.scroll_offset + self.viewport_height) / self.item_height)
.ceil() as usize;
let visible_range = start_idx..end_idx.min(self.item_count);
// Request layout only for visible items
let visible_item_layouts: Vec<_> = visible_range.clone()
.map(|i| {
let mut item = (self.render_item)(i);
item.request_layout(global_id, inspector_id, window, cx).0
})
.collect();
let total_height = self.item_height * self.item_count as f32;
let layout_id = window.request_layout(
Style {
size: size(relative(1.0), self.viewport_height),
overflow: Overflow::Hidden,
..default()
},
visible_item_layouts.clone(),
cx
);
(layout_id, VirtualListState {
visible_range,
visible_item_layouts,
})
}
fn prepaint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
state: &mut VirtualListState,
window: &mut Window,
_cx: &mut App
) -> Hitbox {
// Prepaint visible items at correct positions
for (i, layout_id) in state.visible_item_layouts.iter().enumerate() {
let item_idx = state.visible_range.start + i;
let y = item_idx as f32 * self.item_height - self.scroll_offset;
let item_bounds = Bounds::new(
point(bounds.left(), bounds.top() + y),
size(bounds.width(), self.item_height)
);
// Prepaint if visible
if item_bounds.intersects(&bounds) {
// Prepaint item...
}
}
window.insert_hitbox(bounds, HitboxBehavior::Normal)
}
fn paint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
state: &mut VirtualListState,
hitbox: &mut Hitbox,
window: &mut Window,
cx: &mut App
) {
// Paint visible items
for (i, _layout_id) in state.visible_item_layouts.iter().enumerate() {
let item_idx = state.visible_range.start + i;
let y = item_idx as f32 * self.item_height - self.scroll_offset;
let item_bounds = Bounds::new(
point(bounds.left(), bounds.top() + y),
size(bounds.width(), self.item_height)
);
if item_bounds.intersects(&bounds) {
let mut item = (self.render_item)(item_idx);
item.paint(item_bounds, window, cx);
}
}
// Handle scroll
window.on_mouse_event({
let hitbox = hitbox.clone();
let total_height = self.item_height * self.item_count as f32;
move |event: &ScrollWheelEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
self.scroll_offset -= event.delta.y;
self.scroll_offset = self.scroll_offset
.max(px(0.))
.min(total_height - self.viewport_height);
cx.notify();
cx.stop_propagation();
}
}
});
}
}
// Usage: Efficiently render 10,000 items
let virtual_list = VirtualList {
id: ElementId::Name("large-list".into()),
item_count: 10_000,
item_height: px(40.),
viewport_height: px(400.),
scroll_offset: px(0.),
render_item: Box::new(|index| {
div().child(format!("Item {}", index))
}),
};
```
These advanced patterns enable sophisticated element implementations while maintaining performance and code quality.

View File

@@ -0,0 +1,477 @@
# Element API Reference
Complete API documentation for GPUI's low-level Element trait.
## Element Trait Structure
The `Element` trait requires implementing three associated types and five methods:
```rust
pub trait Element: 'static + IntoElement {
type RequestLayoutState: 'static;
type PrepaintState: 'static;
fn id(&self) -> Option<ElementId>;
fn source_location(&self) -> Option<&'static std::panic::Location<'static>>;
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState);
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState;
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
);
}
```
## Associated Types
### RequestLayoutState
Data passed from `request_layout` to `prepaint` and `paint` phases.
**Usage:**
- Store layout calculations (styled text, child layout IDs)
- Cache expensive computations
- Pass child state between phases
**Examples:**
```rust
// Simple: no state needed
type RequestLayoutState = ();
// Single value
type RequestLayoutState = StyledText;
// Multiple values
type RequestLayoutState = (StyledText, Vec<ChildLayout>);
// Complex struct
pub struct MyLayoutState {
pub styled_text: StyledText,
pub child_layouts: Vec<(LayoutId, ChildState)>,
pub computed_bounds: Bounds<Pixels>,
}
type RequestLayoutState = MyLayoutState;
```
### PrepaintState
Data passed from `prepaint` to `paint` phase.
**Usage:**
- Store hitboxes for interaction
- Cache visual bounds
- Store prepaint results
**Examples:**
```rust
// Simple: just a hitbox
type PrepaintState = Hitbox;
// Optional hitbox
type PrepaintState = Option<Hitbox>;
// Multiple values
type PrepaintState = (Hitbox, Vec<Bounds<Pixels>>);
// Complex struct
pub struct MyPaintState {
pub hitbox: Hitbox,
pub child_bounds: Vec<Bounds<Pixels>>,
pub visible_range: Range<usize>,
}
type PrepaintState = MyPaintState;
```
## Methods
### id()
Returns optional unique identifier for debugging and inspection.
```rust
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
// Or if no ID needed
fn id(&self) -> Option<ElementId> {
None
}
```
### source_location()
Returns source location for debugging. Usually returns `None` unless debugging is needed.
```rust
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
```
### request_layout()
Calculates sizes and positions for the element tree.
**Parameters:**
- `global_id`: Global element identifier (optional)
- `inspector_id`: Inspector element identifier (optional)
- `window`: Mutable window reference
- `cx`: Mutable app context
**Returns:**
- `(LayoutId, Self::RequestLayoutState)`: Layout ID and state for next phases
**Responsibilities:**
1. Calculate child layouts by calling `child.request_layout()`
2. Create own layout using `window.request_layout()`
3. Return layout ID and state to pass to next phases
**Example:**
```rust
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
// 1. Calculate child layouts
let child_layout_id = self.child.request_layout(
global_id,
inspector_id,
window,
cx
).0;
// 2. Create own layout
let layout_id = window.request_layout(
Style {
size: size(px(200.), px(100.)),
..default()
},
vec![child_layout_id],
cx
);
// 3. Return layout ID and state
(layout_id, MyLayoutState { child_layout_id })
}
```
### prepaint()
Prepares for painting by creating hitboxes and computing final bounds.
**Parameters:**
- `global_id`: Global element identifier (optional)
- `inspector_id`: Inspector element identifier (optional)
- `bounds`: Final bounds calculated by layout engine
- `request_layout`: Mutable reference to layout state
- `window`: Mutable window reference
- `cx`: Mutable app context
**Returns:**
- `Self::PrepaintState`: State for paint phase
**Responsibilities:**
1. Compute final child bounds based on layout bounds
2. Call `child.prepaint()` for all children
3. Create hitboxes using `window.insert_hitbox()`
4. Return state for paint phase
**Example:**
```rust
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
// 1. Compute child bounds
let child_bounds = bounds; // or calculated subset
// 2. Prepaint children
self.child.prepaint(
global_id,
inspector_id,
child_bounds,
&mut request_layout.child_state,
window,
cx
);
// 3. Create hitboxes
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
// 4. Return paint state
MyPaintState { hitbox }
}
```
### paint()
Renders the element and handles interactions.
**Parameters:**
- `global_id`: Global element identifier (optional)
- `inspector_id`: Inspector element identifier (optional)
- `bounds`: Final bounds for rendering
- `request_layout`: Mutable reference to layout state
- `prepaint`: Mutable reference to prepaint state
- `window`: Mutable window reference
- `cx`: Mutable app context
**Responsibilities:**
1. Paint children first (bottom to top)
2. Paint own content (backgrounds, borders, etc.)
3. Set up interactions (mouse events, cursor styles)
**Example:**
```rust
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
// 1. Paint children first
self.child.paint(
global_id,
inspector_id,
child_bounds,
&mut request_layout.child_state,
&mut prepaint.child_paint_state,
window,
cx
);
// 2. Paint own content
window.paint_quad(paint_quad(
bounds,
Corners::all(px(4.)),
cx.theme().background,
));
// 3. Set up interactions
window.on_mouse_event({
let hitbox = prepaint.hitbox.clone();
move |event: &MouseDownEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
// Handle click
cx.stop_propagation();
}
}
});
window.set_cursor_style(CursorStyle::PointingHand, &prepaint.hitbox);
}
```
## IntoElement Integration
Elements must also implement `IntoElement` to be used as children:
```rust
impl IntoElement for MyElement {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
```
This allows your custom element to be used directly in the element tree:
```rust
div()
.child(MyElement::new()) // Works because of IntoElement
```
## Common Parameters
### Global and Inspector IDs
Both are optional identifiers used for debugging and inspection:
- `global_id`: Unique identifier across entire app
- `inspector_id`: Identifier for dev tools/inspector
Usually passed through to children without modification.
### Window and Context
- `window: &mut Window`: Window-specific operations (painting, hitboxes, events)
- `cx: &mut App`: App-wide operations (spawning tasks, accessing globals)
## Layout System Integration
### window.request_layout()
Creates a layout node with specified style and children:
```rust
let layout_id = window.request_layout(
Style {
size: size(px(200.), px(100.)),
flex: Flex::Column,
gap: px(8.),
..default()
},
vec![child1_layout_id, child2_layout_id],
cx
);
```
### Bounds<Pixels>
Represents rectangular region:
```rust
pub struct Bounds<T> {
pub origin: Point<T>,
pub size: Size<T>,
}
// Create bounds
let bounds = Bounds::new(
point(px(10.), px(20.)),
size(px(100.), px(50.))
);
// Access properties
bounds.left() // origin.x
bounds.top() // origin.y
bounds.right() // origin.x + size.width
bounds.bottom() // origin.y + size.height
bounds.center() // center point
```
## Hitbox System
### Creating Hitboxes
```rust
// Normal hitbox (blocks events)
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
// Transparent hitbox (passes events through)
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Transparent);
```
### Using Hitboxes
```rust
// Check if hovered
if hitbox.is_hovered(window) {
// ...
}
// Set cursor style
window.set_cursor_style(CursorStyle::PointingHand, &hitbox);
// Use in event handlers
window.on_mouse_event(move |event, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
// Handle event
}
});
```
## Event Handling
### Mouse Events
```rust
// Mouse down
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
if phase.bubble() && bounds.contains(&event.position) {
// Handle mouse down
cx.stop_propagation(); // Prevent bubbling
}
});
// Mouse up
window.on_mouse_event(move |event: &MouseUpEvent, phase, window, cx| {
// Handle mouse up
});
// Mouse move
window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| {
// Handle mouse move
});
// Scroll
window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| {
// Handle scroll
});
```
### Event Phase
Events go through two phases:
- **Capture**: Top-down (parent → child)
- **Bubble**: Bottom-up (child → parent)
```rust
move |event, phase, window, cx| {
if phase.capture() {
// Handle in capture phase
} else if phase.bubble() {
// Handle in bubble phase
}
cx.stop_propagation(); // Stop event from continuing
}
```
## Cursor Styles
Available cursor styles:
```rust
CursorStyle::Arrow
CursorStyle::IBeam // Text selection
CursorStyle::PointingHand // Clickable
CursorStyle::ResizeLeft
CursorStyle::ResizeRight
CursorStyle::ResizeUp
CursorStyle::ResizeDown
CursorStyle::ResizeLeftRight
CursorStyle::ResizeUpDown
CursorStyle::Crosshair
CursorStyle::OperationNotAllowed
```
Usage:
```rust
window.set_cursor_style(CursorStyle::PointingHand, &hitbox);
```

View File

@@ -0,0 +1,546 @@
# Element Best Practices
Guidelines and best practices for implementing high-quality GPUI elements.
## State Management
### Using Associated Types Effectively
**Good:** Use associated types to pass meaningful data between phases
```rust
// Good: Structured state with type safety
type RequestLayoutState = (StyledText, Vec<ChildLayout>);
type PrepaintState = (Hitbox, Vec<ChildBounds>);
```
**Bad:** Using empty state when you need data
```rust
// Bad: No state when you need to pass data
type RequestLayoutState = ();
type PrepaintState = ();
// Now you can't pass layout info to paint phase!
```
### Managing Complex State
For elements with complex state, create dedicated structs:
```rust
// Good: Dedicated struct for complex state
pub struct TextElementState {
pub styled_text: StyledText,
pub text_layout: TextLayout,
pub child_states: Vec<ChildState>,
}
type RequestLayoutState = TextElementState;
```
**Benefits:**
- Clear documentation of state structure
- Easy to extend
- Type-safe access
### State Lifecycle
**Golden Rule:** State flows in one direction through the phases
```
request_layout → RequestLayoutState →
prepaint → PrepaintState →
paint
```
**Don't:**
- Store state in the element struct that should be in associated types
- Try to mutate element state in paint phase (use `cx.notify()` to schedule re-render)
- Pass mutable references across phase boundaries
## Performance Considerations
### Minimize Allocations in Paint Phase
**Critical:** Paint phase is called every frame during animations. Minimize allocations.
**Good:** Pre-allocate in `request_layout` or `prepaint`
```rust
impl Element for MyElement {
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, Vec<StyledText>)
{
// Allocate once during layout
let styled_texts = self.children
.iter()
.map(|child| StyledText::new(child.text.clone()))
.collect();
(layout_id, styled_texts)
}
fn paint(&mut self, .., styled_texts: &mut Vec<StyledText>, ..) {
// Just use pre-allocated styled_texts
for text in styled_texts {
text.paint(..);
}
}
}
```
**Bad:** Allocate in `paint` phase
```rust
fn paint(&mut self, ..) {
// Bad: Allocation in paint phase!
let styled_texts: Vec<_> = self.children
.iter()
.map(|child| StyledText::new(child.text.clone()))
.collect();
}
```
### Cache Expensive Computations
Use memoization for expensive operations:
```rust
pub struct CachedElement {
// Cache key
last_text: Option<SharedString>,
last_width: Option<Pixels>,
// Cached result
cached_layout: Option<TextLayout>,
}
impl Element for CachedElement {
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, TextLayout)
{
let current_width = window.bounds().width();
// Check if cache is valid
if self.last_text.as_ref() != Some(&self.text)
|| self.last_width != Some(current_width)
|| self.cached_layout.is_none()
{
// Recompute expensive layout
self.cached_layout = Some(self.compute_text_layout(current_width));
self.last_text = Some(self.text.clone());
self.last_width = Some(current_width);
}
// Use cached layout
let layout = self.cached_layout.as_ref().unwrap();
(layout_id, layout.clone())
}
}
```
### Lazy Child Rendering
Only render visible children in scrollable containers:
```rust
fn paint(&mut self, .., bounds: Bounds<Pixels>, paint_state: &mut Self::PrepaintState, ..) {
for (i, child) in self.children.iter_mut().enumerate() {
let child_bounds = paint_state.child_bounds[i];
// Only paint visible children
if self.is_visible(&child_bounds, &bounds) {
child.paint(..);
}
}
}
fn is_visible(&self, child_bounds: &Bounds<Pixels>, container_bounds: &Bounds<Pixels>) -> bool {
child_bounds.bottom() >= container_bounds.top() &&
child_bounds.top() <= container_bounds.bottom()
}
```
## Interaction Handling
### Proper Event Bubbling
Always check phase and bounds before handling events:
```rust
fn paint(&mut self, .., window: &mut Window, cx: &mut App) {
window.on_mouse_event({
let hitbox = self.hitbox.clone();
move |event: &MouseDownEvent, phase, window, cx| {
// Check phase first
if !phase.bubble() {
return;
}
// Check if event is within bounds
if !hitbox.is_hovered(window) {
return;
}
// Handle event
self.handle_click(event);
// Stop propagation if handled
cx.stop_propagation();
}
});
}
```
**Don't forget:**
- Check `phase.bubble()` or `phase.capture()` as appropriate
- Check hitbox hover state or bounds
- Call `cx.stop_propagation()` if you handle the event
### Hitbox Management
Create hitboxes in `prepaint` phase, not `paint`:
**Good:**
```rust
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, window: &mut Window, ..) -> Hitbox {
// Create hitbox in prepaint
window.insert_hitbox(bounds, HitboxBehavior::Normal)
}
fn paint(&mut self, .., hitbox: &mut Hitbox, window: &mut Window, ..) {
// Use hitbox in paint
window.set_cursor_style(CursorStyle::PointingHand, hitbox);
}
```
**Hitbox Behaviors:**
```rust
// Normal: Blocks events from passing through
HitboxBehavior::Normal
// Transparent: Allows events to pass through to elements below
HitboxBehavior::Transparent
```
### Cursor Style Guidelines
Set appropriate cursor styles for interactivity cues:
```rust
// Text selection
window.set_cursor_style(CursorStyle::IBeam, &hitbox);
// Clickable elements (desktop convention: use default, not pointing hand)
window.set_cursor_style(CursorStyle::Arrow, &hitbox);
// Links (web convention: use pointing hand)
window.set_cursor_style(CursorStyle::PointingHand, &hitbox);
// Resizable edges
window.set_cursor_style(CursorStyle::ResizeLeftRight, &hitbox);
```
**Desktop vs Web Convention:**
- Desktop apps: Use `Arrow` for buttons
- Web apps: Use `PointingHand` for links only
## Layout Strategies
### Fixed Size Elements
For elements with known, unchanging size:
```rust
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App) -> (LayoutId, ()) {
let layout_id = window.request_layout(
Style {
size: size(px(200.), px(100.)),
..default()
},
vec![], // No children
cx
);
(layout_id, ())
}
```
### Content-Based Sizing
For elements sized by their content:
```rust
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, Size<Pixels>)
{
// Measure content
let text_bounds = self.measure_text(window);
let padding = px(16.);
let layout_id = window.request_layout(
Style {
size: size(
text_bounds.width() + padding * 2.,
text_bounds.height() + padding * 2.,
),
..default()
},
vec![],
cx
);
(layout_id, text_bounds)
}
```
### Flexible Layouts
For elements that adapt to available space:
```rust
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, Vec<LayoutId>)
{
let mut child_layout_ids = Vec::new();
for child in &mut self.children {
let (layout_id, _) = child.request_layout(window, cx);
child_layout_ids.push(layout_id);
}
let layout_id = window.request_layout(
Style {
flex_direction: FlexDirection::Row,
gap: px(8.),
size: Size {
width: relative(1.0), // Fill parent width
height: auto(), // Auto height
},
..default()
},
child_layout_ids.clone(),
cx
);
(layout_id, child_layout_ids)
}
```
## Error Handling
### Graceful Degradation
Handle errors gracefully, don't panic:
```rust
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, Option<TextLayout>)
{
// Try to create styled text
match StyledText::new(self.text.clone()).request_layout(None, None, window, cx) {
Ok((layout_id, text_layout)) => {
(layout_id, Some(text_layout))
}
Err(e) => {
// Log error
eprintln!("Failed to layout text: {}", e);
// Fallback to simple text
let fallback_text = StyledText::new("(Error loading text)".into());
let (layout_id, _) = fallback_text.request_layout(None, None, window, cx);
(layout_id, None)
}
}
}
```
### Defensive Bounds Checking
Always validate bounds and indices:
```rust
fn paint_selection(&self, selection: &Selection, text_layout: &TextLayout, ..) {
// Validate selection bounds
let start = selection.start.min(self.text.len());
let end = selection.end.min(self.text.len());
if start >= end {
return; // Invalid selection
}
let rects = text_layout.rects_for_range(start..end);
// Paint selection...
}
```
## Testing Element Implementations
### Layout Tests
Test that layout calculations are correct:
```rust
#[cfg(test)]
mod tests {
use super::*;
use gpui::TestAppContext;
#[gpui::test]
fn test_element_layout(cx: &mut TestAppContext) {
cx.update(|cx| {
let mut window = cx.open_window(Default::default(), |_, _| ()).unwrap();
window.update(cx, |window, cx| {
let mut element = MyElement::new();
let (layout_id, layout_state) = element.request_layout(
None,
None,
window,
cx
);
// Assert layout properties
let bounds = window.layout_bounds(layout_id);
assert_eq!(bounds.size.width, px(200.));
assert_eq!(bounds.size.height, px(100.));
});
});
}
}
```
### Interaction Tests
Test that interactions work correctly:
```rust
#[gpui::test]
fn test_element_click(cx: &mut TestAppContext) {
cx.update(|cx| {
let mut window = cx.open_window(Default::default(), |_, cx| {
cx.new(|_| MyElement::new())
}).unwrap();
window.update(cx, |window, cx| {
let view = window.root_view().unwrap();
// Simulate click
let position = point(px(10.), px(10.));
window.dispatch_event(MouseDownEvent {
position,
button: MouseButton::Left,
modifiers: Modifiers::default(),
});
// Assert element responded
view.read(cx).assert_clicked();
});
});
}
```
## Common Pitfalls
### ❌ Storing Layout State in Element Struct
**Bad:**
```rust
pub struct MyElement {
id: ElementId,
// Bad: This should be in RequestLayoutState
cached_layout: Option<TextLayout>,
}
```
**Good:**
```rust
pub struct MyElement {
id: ElementId,
text: SharedString,
}
type RequestLayoutState = TextLayout; // Good: State in associated type
```
### ❌ Mutating Element in Paint Phase
**Bad:**
```rust
fn paint(&mut self, ..) {
self.counter += 1; // Bad: Mutating element in paint
}
```
**Good:**
```rust
fn paint(&mut self, .., window: &mut Window, cx: &mut App) {
window.on_mouse_event(move |event, phase, window, cx| {
if phase.bubble() {
self.counter += 1;
cx.notify(); // Schedule re-render
}
});
}
```
### ❌ Creating Hitboxes in Paint Phase
**Bad:**
```rust
fn paint(&mut self, .., bounds: Bounds<Pixels>, window: &mut Window, ..) {
// Bad: Creating hitbox in paint
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
}
```
**Good:**
```rust
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, window: &mut Window, ..) -> Hitbox {
// Good: Creating hitbox in prepaint
window.insert_hitbox(bounds, HitboxBehavior::Normal)
}
```
### ❌ Ignoring Event Phase
**Bad:**
```rust
window.on_mouse_event(move |event, phase, window, cx| {
// Bad: Not checking phase
self.handle_click(event);
});
```
**Good:**
```rust
window.on_mouse_event(move |event, phase, window, cx| {
// Good: Checking phase
if !phase.bubble() {
return;
}
self.handle_click(event);
});
```
## Performance Checklist
Before shipping an element implementation, verify:
- [ ] No allocations in `paint` phase (except event handlers)
- [ ] Expensive computations are cached/memoized
- [ ] Only visible children are rendered in scrollable containers
- [ ] Hitboxes created in `prepaint`, not `paint`
- [ ] Event handlers check phase and bounds
- [ ] Layout state is passed through associated types, not stored in element
- [ ] Element implements proper error handling with fallbacks
- [ ] Tests cover layout calculations and interactions

View File

@@ -0,0 +1,632 @@
# Element Implementation Examples
Complete examples of implementing custom elements for various scenarios.
## Table of Contents
1. [Simple Text Element](#simple-text-element)
2. [Interactive Element with Selection](#interactive-element-with-selection)
3. [Complex Element with Child Management](#complex-element-with-child-management)
## Simple Text Element
A basic text element with syntax highlighting support.
```rust
pub struct SimpleText {
id: ElementId,
text: SharedString,
highlights: Vec<(Range<usize>, HighlightStyle)>,
}
impl IntoElement for SimpleText {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for SimpleText {
type RequestLayoutState = StyledText;
type PrepaintState = Hitbox;
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, Self::RequestLayoutState) {
// Create styled text with highlights
let mut runs = Vec::new();
let mut ix = 0;
for (range, highlight) in &self.highlights {
// Add unstyled text before highlight
if ix < range.start {
runs.push(window.text_style().to_run(range.start - ix));
}
// Add highlighted text
runs.push(
window.text_style()
.highlight(*highlight)
.to_run(range.len())
);
ix = range.end;
}
// Add remaining unstyled text
if ix < self.text.len() {
runs.push(window.text_style().to_run(self.text.len() - ix));
}
let styled_text = StyledText::new(self.text.clone()).with_runs(runs);
let (layout_id, _) = styled_text.request_layout(
global_id,
inspector_id,
window,
cx
);
(layout_id, styled_text)
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
styled_text: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App
) -> Self::PrepaintState {
// Prepaint the styled text
styled_text.prepaint(
global_id,
inspector_id,
bounds,
&mut (),
window,
cx
);
// Create hitbox for interaction
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
hitbox
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
styled_text: &mut Self::RequestLayoutState,
hitbox: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App
) {
// Paint the styled text
styled_text.paint(
global_id,
inspector_id,
bounds,
&mut (),
&mut (),
window,
cx
);
// Set cursor style for text
window.set_cursor_style(CursorStyle::IBeam, hitbox);
}
}
```
## Interactive Element with Selection
A text element that supports text selection via mouse interaction.
```rust
#[derive(Clone)]
pub struct Selection {
pub start: usize,
pub end: usize,
}
pub struct SelectableText {
id: ElementId,
text: SharedString,
selectable: bool,
selection: Option<Selection>,
}
impl IntoElement for SelectableText {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for SelectableText {
type RequestLayoutState = TextLayout;
type PrepaintState = Option<Hitbox>;
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, Self::RequestLayoutState) {
let styled_text = StyledText::new(self.text.clone());
let (layout_id, _) = styled_text.request_layout(
global_id,
inspector_id,
window,
cx
);
// Extract text layout for selection painting
let text_layout = styled_text.layout().clone();
(layout_id, text_layout)
}
fn prepaint(
&mut self,
_global_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_text_layout: &mut Self::RequestLayoutState,
window: &mut Window,
_cx: &mut App
) -> Self::PrepaintState {
// Only create hitbox if selectable
if self.selectable {
Some(window.insert_hitbox(bounds, HitboxBehavior::Normal))
} else {
None
}
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
text_layout: &mut Self::RequestLayoutState,
hitbox: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App
) {
// Paint text
let styled_text = StyledText::new(self.text.clone());
styled_text.paint(
global_id,
inspector_id,
bounds,
&mut (),
&mut (),
window,
cx
);
// Paint selection if any
if let Some(selection) = &self.selection {
Self::paint_selection(selection, text_layout, &bounds, window, cx);
}
// Handle mouse events for selection
if let Some(hitbox) = hitbox {
window.set_cursor_style(CursorStyle::IBeam, hitbox);
// Mouse down to start selection
window.on_mouse_event({
let bounds = bounds.clone();
move |event: &MouseDownEvent, phase, window, cx| {
if bounds.contains(&event.position) && phase.bubble() {
// Start selection at mouse position
let char_index = Self::position_to_index(
event.position,
&bounds,
text_layout
);
self.selection = Some(Selection {
start: char_index,
end: char_index,
});
cx.notify();
cx.stop_propagation();
}
}
});
// Mouse drag to extend selection
window.on_mouse_event({
let bounds = bounds.clone();
move |event: &MouseMoveEvent, phase, window, cx| {
if let Some(selection) = &mut self.selection {
if phase.bubble() {
let char_index = Self::position_to_index(
event.position,
&bounds,
text_layout
);
selection.end = char_index;
cx.notify();
}
}
}
});
}
}
}
impl SelectableText {
fn paint_selection(
selection: &Selection,
text_layout: &TextLayout,
bounds: &Bounds<Pixels>,
window: &mut Window,
cx: &mut App
) {
// Calculate selection bounds from text layout
let selection_rects = text_layout.rects_for_range(
selection.start..selection.end
);
// Paint selection background
for rect in selection_rects {
window.paint_quad(paint_quad(
Bounds::new(
point(bounds.left() + rect.origin.x, bounds.top() + rect.origin.y),
rect.size
),
Corners::default(),
cx.theme().selection_background,
));
}
}
fn position_to_index(
position: Point<Pixels>,
bounds: &Bounds<Pixels>,
text_layout: &TextLayout
) -> usize {
// Convert screen position to character index
let relative_pos = point(
position.x - bounds.left(),
position.y - bounds.top()
);
text_layout.index_for_position(relative_pos)
}
}
```
## Complex Element with Child Management
A container element that manages multiple children with scrolling support.
```rust
pub struct ComplexElement {
id: ElementId,
children: Vec<Box<dyn Element<RequestLayoutState = (), PrepaintState = ()>>>,
scrollable: bool,
scroll_offset: Point<Pixels>,
}
struct ComplexLayoutState {
child_layouts: Vec<LayoutId>,
total_height: Pixels,
}
struct ComplexPaintState {
child_bounds: Vec<Bounds<Pixels>>,
hitbox: Hitbox,
}
impl IntoElement for ComplexElement {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for ComplexElement {
type RequestLayoutState = ComplexLayoutState;
type PrepaintState = ComplexPaintState;
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App
) -> (LayoutId, Self::RequestLayoutState) {
let mut child_layouts = Vec::new();
let mut total_height = px(0.);
// Request layout for all children
for child in &mut self.children {
let (child_layout_id, _) = child.request_layout(
global_id,
inspector_id,
window,
cx
);
child_layouts.push(child_layout_id);
// Get child size from layout
let child_size = window.layout_bounds(child_layout_id).size();
total_height += child_size.height;
}
// Create container layout
let layout_id = window.request_layout(
Style {
flex_direction: FlexDirection::Column,
gap: px(8.),
size: Size {
width: relative(1.0),
height: if self.scrollable {
// Fixed height for scrollable
px(400.)
} else {
// Auto height for non-scrollable
total_height
},
},
..default()
},
child_layouts.clone(),
cx
);
(layout_id, ComplexLayoutState {
child_layouts,
total_height,
})
}
fn prepaint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
layout_state: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App
) -> Self::PrepaintState {
let mut child_bounds = Vec::new();
let mut y_offset = self.scroll_offset.y;
// Calculate child bounds and prepaint children
for (child, layout_id) in self.children.iter_mut()
.zip(&layout_state.child_layouts)
{
let child_size = window.layout_bounds(*layout_id).size();
let child_bound = Bounds::new(
point(bounds.left(), bounds.top() + y_offset),
child_size
);
// Only prepaint visible children
if self.is_visible(&child_bound, &bounds) {
child.prepaint(
global_id,
inspector_id,
child_bound,
&mut (),
window,
cx
);
}
child_bounds.push(child_bound);
y_offset += child_size.height + px(8.); // gap
}
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
ComplexPaintState {
child_bounds,
hitbox,
}
}
fn paint(
&mut self,
global_id: Option<&GlobalElementId>,
inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
layout_state: &mut Self::RequestLayoutState,
paint_state: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App
) {
// Paint background
window.paint_quad(paint_quad(
bounds,
Corners::all(px(4.)),
cx.theme().background,
));
// Paint visible children only
for (i, child) in self.children.iter_mut().enumerate() {
let child_bounds = paint_state.child_bounds[i];
if self.is_visible(&child_bounds, &bounds) {
child.paint(
global_id,
inspector_id,
child_bounds,
&mut (),
&mut (),
window,
cx
);
}
}
// Paint scrollbar if scrollable
if self.scrollable {
self.paint_scrollbar(bounds, layout_state, window, cx);
}
// Handle scroll events
if self.scrollable {
window.on_mouse_event({
let hitbox = paint_state.hitbox.clone();
let total_height = layout_state.total_height;
let visible_height = bounds.size.height;
move |event: &ScrollWheelEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
// Update scroll offset
self.scroll_offset.y -= event.delta.y;
// Clamp scroll offset
let max_scroll = (total_height - visible_height).max(px(0.));
self.scroll_offset.y = self.scroll_offset.y
.max(px(0.))
.min(max_scroll);
cx.notify();
cx.stop_propagation();
}
}
});
}
}
}
impl ComplexElement {
fn is_visible(&self, child_bounds: &Bounds<Pixels>, container_bounds: &Bounds<Pixels>) -> bool {
// Check if child is within visible area
child_bounds.bottom() >= container_bounds.top() &&
child_bounds.top() <= container_bounds.bottom()
}
fn paint_scrollbar(
&self,
bounds: Bounds<Pixels>,
layout_state: &ComplexLayoutState,
window: &mut Window,
cx: &mut App
) {
let scrollbar_width = px(8.);
let visible_height = bounds.size.height;
let total_height = layout_state.total_height;
if total_height <= visible_height {
return; // No need for scrollbar
}
// Calculate scrollbar position and size
let scroll_ratio = self.scroll_offset.y / (total_height - visible_height);
let thumb_height = (visible_height / total_height) * visible_height;
let thumb_y = scroll_ratio * (visible_height - thumb_height);
// Paint scrollbar track
let track_bounds = Bounds::new(
point(bounds.right() - scrollbar_width, bounds.top()),
size(scrollbar_width, visible_height)
);
window.paint_quad(paint_quad(
track_bounds,
Corners::default(),
cx.theme().scrollbar_track,
));
// Paint scrollbar thumb
let thumb_bounds = Bounds::new(
point(bounds.right() - scrollbar_width, bounds.top() + thumb_y),
size(scrollbar_width, thumb_height)
);
window.paint_quad(paint_quad(
thumb_bounds,
Corners::all(px(4.)),
cx.theme().scrollbar_thumb,
));
}
}
```
## Usage Examples
### Using SimpleText
```rust
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.child(SimpleText {
id: ElementId::Name("code-text".into()),
text: "fn main() { println!(\"Hello\"); }".into(),
highlights: vec![
(0..2, HighlightStyle::keyword()),
(3..7, HighlightStyle::function()),
],
})
}
```
### Using SelectableText
```rust
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.child(SelectableText {
id: ElementId::Name("selectable-text".into()),
text: "Select this text with your mouse".into(),
selectable: true,
selection: self.current_selection.clone(),
})
}
```
### Using ComplexElement
```rust
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let children: Vec<Box<dyn Element<_, _>>> = self.items
.iter()
.map(|item| Box::new(div().child(item.name.clone())) as Box<_>)
.collect();
div()
.child(ComplexElement {
id: ElementId::Name("scrollable-list".into()),
children,
scrollable: true,
scroll_offset: self.scroll_offset,
})
}
```

View File

@@ -0,0 +1,509 @@
# Common Element Patterns
Reusable patterns for implementing common element types in GPUI.
## Text Rendering Elements
Elements that display and manipulate text content.
### Pattern Characteristics
- Use `StyledText` for text layout and rendering
- Handle text selection in `paint` phase with hitbox interaction
- Create hitboxes for text interaction in `prepaint`
- Support text highlighting and custom styling via runs
### Implementation Template
```rust
pub struct TextElement {
id: ElementId,
text: SharedString,
style: TextStyle,
}
impl Element for TextElement {
type RequestLayoutState = StyledText;
type PrepaintState = Hitbox;
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, StyledText)
{
let styled_text = StyledText::new(self.text.clone())
.with_style(self.style);
let (layout_id, _) = styled_text.request_layout(None, None, window, cx);
(layout_id, styled_text)
}
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, styled_text: &mut StyledText,
window: &mut Window, cx: &mut App) -> Hitbox
{
styled_text.prepaint(None, None, bounds, &mut (), window, cx);
window.insert_hitbox(bounds, HitboxBehavior::Normal)
}
fn paint(&mut self, .., bounds: Bounds<Pixels>, styled_text: &mut StyledText,
hitbox: &mut Hitbox, window: &mut Window, cx: &mut App)
{
styled_text.paint(None, None, bounds, &mut (), &mut (), window, cx);
window.set_cursor_style(CursorStyle::IBeam, hitbox);
}
}
```
### Use Cases
- Code editors with syntax highlighting
- Rich text displays
- Labels with custom formatting
- Selectable text areas
## Container Elements
Elements that manage and layout child elements.
### Pattern Characteristics
- Manage child element layouts and positions
- Handle scrolling and clipping when needed
- Implement flex/grid-like layouts
- Coordinate child interactions and event delegation
### Implementation Template
```rust
pub struct ContainerElement {
id: ElementId,
children: Vec<AnyElement>,
direction: FlexDirection,
gap: Pixels,
}
impl Element for ContainerElement {
type RequestLayoutState = Vec<LayoutId>;
type PrepaintState = Vec<Bounds<Pixels>>;
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, Vec<LayoutId>)
{
let child_layout_ids: Vec<_> = self.children
.iter_mut()
.map(|child| child.request_layout(window, cx).0)
.collect();
let layout_id = window.request_layout(
Style {
flex_direction: self.direction,
gap: self.gap,
..default()
},
child_layout_ids.clone(),
cx
);
(layout_id, child_layout_ids)
}
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, layout_ids: &mut Vec<LayoutId>,
window: &mut Window, cx: &mut App) -> Vec<Bounds<Pixels>>
{
let mut child_bounds = Vec::new();
for (child, layout_id) in self.children.iter_mut().zip(layout_ids.iter()) {
let child_bound = window.layout_bounds(*layout_id);
child.prepaint(child_bound, window, cx);
child_bounds.push(child_bound);
}
child_bounds
}
fn paint(&mut self, .., child_bounds: &mut Vec<Bounds<Pixels>>,
window: &mut Window, cx: &mut App)
{
for (child, bounds) in self.children.iter_mut().zip(child_bounds.iter()) {
child.paint(*bounds, window, cx);
}
}
}
```
### Use Cases
- Panels and split views
- List containers
- Grid layouts
- Tab containers
## Interactive Elements
Elements that respond to user input (mouse, keyboard, touch).
### Pattern Characteristics
- Create appropriate hitboxes for interaction areas
- Handle mouse/keyboard/touch events properly
- Manage focus and cursor styles
- Support hover, active, and disabled states
### Implementation Template
```rust
pub struct InteractiveElement {
id: ElementId,
content: AnyElement,
on_click: Option<Box<dyn Fn(&MouseUpEvent, &mut Window, &mut App)>>,
hover_style: Option<Style>,
}
impl Element for InteractiveElement {
type RequestLayoutState = LayoutId;
type PrepaintState = (Hitbox, bool); // hitbox and is_hovered
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, LayoutId)
{
let (content_layout, _) = self.content.request_layout(window, cx);
(content_layout, content_layout)
}
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, content_layout: &mut LayoutId,
window: &mut Window, cx: &mut App) -> (Hitbox, bool)
{
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
let is_hovered = hitbox.is_hovered(window);
self.content.prepaint(bounds, window, cx);
(hitbox, is_hovered)
}
fn paint(&mut self, .., bounds: Bounds<Pixels>, content_layout: &mut LayoutId,
prepaint: &mut (Hitbox, bool), window: &mut Window, cx: &mut App)
{
let (hitbox, is_hovered) = prepaint;
// Paint hover background if hovered
if *is_hovered {
if let Some(hover_style) = &self.hover_style {
window.paint_quad(paint_quad(
bounds,
Corners::all(px(4.)),
hover_style.background_color.unwrap_or(cx.theme().hover),
));
}
}
// Paint content
self.content.paint(bounds, window, cx);
// Handle click
if let Some(on_click) = self.on_click.as_ref() {
window.on_mouse_event({
let on_click = on_click.clone();
let hitbox = hitbox.clone();
move |event: &MouseUpEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
on_click(event, window, cx);
cx.stop_propagation();
}
}
});
}
// Set cursor style
window.set_cursor_style(CursorStyle::PointingHand, hitbox);
}
}
```
### Use Cases
- Buttons
- Links
- Clickable cards
- Drag handles
- Menu items
## Composite Elements
Elements that combine multiple child elements with complex coordination.
### Pattern Characteristics
- Combine multiple child elements with different types
- Manage complex state across children
- Coordinate animations and transitions
- Handle focus delegation between children
### Implementation Template
```rust
pub struct CompositeElement {
id: ElementId,
header: AnyElement,
content: AnyElement,
footer: Option<AnyElement>,
}
struct CompositeLayoutState {
header_layout: LayoutId,
content_layout: LayoutId,
footer_layout: Option<LayoutId>,
}
struct CompositePaintState {
header_bounds: Bounds<Pixels>,
content_bounds: Bounds<Pixels>,
footer_bounds: Option<Bounds<Pixels>>,
}
impl Element for CompositeElement {
type RequestLayoutState = CompositeLayoutState;
type PrepaintState = CompositePaintState;
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, CompositeLayoutState)
{
let (header_layout, _) = self.header.request_layout(window, cx);
let (content_layout, _) = self.content.request_layout(window, cx);
let footer_layout = self.footer.as_mut()
.map(|f| f.request_layout(window, cx).0);
let mut children = vec![header_layout, content_layout];
if let Some(footer) = footer_layout {
children.push(footer);
}
let layout_id = window.request_layout(
Style {
flex_direction: FlexDirection::Column,
size: Size {
width: relative(1.0),
height: auto(),
},
..default()
},
children,
cx
);
(layout_id, CompositeLayoutState {
header_layout,
content_layout,
footer_layout,
})
}
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, layout: &mut CompositeLayoutState,
window: &mut Window, cx: &mut App) -> CompositePaintState
{
let header_bounds = window.layout_bounds(layout.header_layout);
let content_bounds = window.layout_bounds(layout.content_layout);
let footer_bounds = layout.footer_layout
.map(|id| window.layout_bounds(id));
self.header.prepaint(header_bounds, window, cx);
self.content.prepaint(content_bounds, window, cx);
if let (Some(footer), Some(bounds)) = (&mut self.footer, footer_bounds) {
footer.prepaint(bounds, window, cx);
}
CompositePaintState {
header_bounds,
content_bounds,
footer_bounds,
}
}
fn paint(&mut self, .., paint_state: &mut CompositePaintState,
window: &mut Window, cx: &mut App)
{
self.header.paint(paint_state.header_bounds, window, cx);
self.content.paint(paint_state.content_bounds, window, cx);
if let (Some(footer), Some(bounds)) = (&mut self.footer, paint_state.footer_bounds) {
footer.paint(bounds, window, cx);
}
}
}
```
### Use Cases
- Dialog boxes (header + content + footer)
- Cards with multiple sections
- Form layouts
- Panels with toolbars
## Scrollable Elements
Elements with scrollable content areas.
### Pattern Characteristics
- Manage scroll state (offset, velocity)
- Handle scroll events (wheel, drag, touch)
- Paint scrollbars (track and thumb)
- Clip content to visible area
### Implementation Template
```rust
pub struct ScrollableElement {
id: ElementId,
content: AnyElement,
scroll_offset: Point<Pixels>,
content_size: Size<Pixels>,
}
struct ScrollPaintState {
hitbox: Hitbox,
visible_bounds: Bounds<Pixels>,
}
impl Element for ScrollableElement {
type RequestLayoutState = (LayoutId, Size<Pixels>);
type PrepaintState = ScrollPaintState;
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
-> (LayoutId, (LayoutId, Size<Pixels>))
{
let (content_layout, _) = self.content.request_layout(window, cx);
let content_size = window.layout_bounds(content_layout).size;
let layout_id = window.request_layout(
Style {
size: Size {
width: relative(1.0),
height: px(400.), // Fixed viewport height
},
overflow: Overflow::Hidden,
..default()
},
vec![content_layout],
cx
);
(layout_id, (content_layout, content_size))
}
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, layout: &mut (LayoutId, Size<Pixels>),
window: &mut Window, cx: &mut App) -> ScrollPaintState
{
let (content_layout, content_size) = layout;
// Calculate content bounds with scroll offset
let content_bounds = Bounds::new(
point(bounds.left(), bounds.top() - self.scroll_offset.y),
*content_size
);
self.content.prepaint(content_bounds, window, cx);
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
ScrollPaintState {
hitbox,
visible_bounds: bounds,
}
}
fn paint(&mut self, .., layout: &mut (LayoutId, Size<Pixels>),
paint_state: &mut ScrollPaintState, window: &mut Window, cx: &mut App)
{
let (_, content_size) = layout;
// Paint content
self.content.paint(paint_state.visible_bounds, window, cx);
// Paint scrollbar
self.paint_scrollbar(paint_state.visible_bounds, *content_size, window, cx);
// Handle scroll events
window.on_mouse_event({
let hitbox = paint_state.hitbox.clone();
let content_height = content_size.height;
let visible_height = paint_state.visible_bounds.size.height;
move |event: &ScrollWheelEvent, phase, window, cx| {
if hitbox.is_hovered(window) && phase.bubble() {
// Update scroll offset
self.scroll_offset.y -= event.delta.y;
// Clamp to valid range
let max_scroll = (content_height - visible_height).max(px(0.));
self.scroll_offset.y = self.scroll_offset.y
.max(px(0.))
.min(max_scroll);
cx.notify();
cx.stop_propagation();
}
}
});
}
}
impl ScrollableElement {
fn paint_scrollbar(
&self,
bounds: Bounds<Pixels>,
content_size: Size<Pixels>,
window: &mut Window,
cx: &mut App
) {
let visible_height = bounds.size.height;
let content_height = content_size.height;
if content_height <= visible_height {
return; // No scrollbar needed
}
let scrollbar_width = px(8.);
// Calculate thumb position and size
let scroll_ratio = self.scroll_offset.y / (content_height - visible_height);
let thumb_height = (visible_height / content_height) * visible_height;
let thumb_y = scroll_ratio * (visible_height - thumb_height);
// Paint track
window.paint_quad(paint_quad(
Bounds::new(
point(bounds.right() - scrollbar_width, bounds.top()),
size(scrollbar_width, visible_height)
),
Corners::default(),
cx.theme().scrollbar_track,
));
// Paint thumb
window.paint_quad(paint_quad(
Bounds::new(
point(bounds.right() - scrollbar_width, bounds.top() + thumb_y),
size(scrollbar_width, thumb_height)
),
Corners::all(px(4.)),
cx.theme().scrollbar_thumb,
));
}
}
```
### Use Cases
- Scrollable lists
- Code editors with large files
- Long-form text content
- Image galleries
## Pattern Selection Guide
| Need | Pattern | Complexity |
|------|---------|------------|
| Display styled text | Text Rendering | Low |
| Layout multiple children | Container | Low-Medium |
| Handle clicks/hovers | Interactive | Medium |
| Complex multi-part UI | Composite | Medium-High |
| Large content with scrolling | Scrollable | High |
Choose the simplest pattern that meets your requirements, then extend as needed.

View File

@@ -0,0 +1,168 @@
---
name: gpui-entity
description: Entity management and state handling in GPUI. Use when working with entities, managing component state, coordinating between components, handling async operations with state updates, or implementing reactive patterns. Entities provide safe concurrent access to application state.
---
## Overview
An `Entity<T>` is a handle to state of type `T`, providing safe access and updates.
**Key Methods:**
- `entity.read(cx)``&T` - Read-only access
- `entity.read_with(cx, |state, cx| ...)``R` - Read with closure
- `entity.update(cx, |state, cx| ...)``R` - Mutable update
- `entity.downgrade()``WeakEntity<T>` - Create weak reference
- `entity.entity_id()``EntityId` - Unique identifier
**Entity Types:**
- **`Entity<T>`**: Strong reference (increases ref count)
- **`WeakEntity<T>`**: Weak reference (doesn't prevent cleanup, returns `Result`)
## Quick Start
### Creating and Using Entities
```rust
// Create entity
let counter = cx.new(|cx| Counter { count: 0 });
// Read state
let count = counter.read(cx).count;
// Update state
counter.update(cx, |state, cx| {
state.count += 1;
cx.notify(); // Trigger re-render
});
// Weak reference (for closures/callbacks)
let weak = counter.downgrade();
let _ = weak.update(cx, |state, cx| {
state.count += 1;
cx.notify();
});
```
### In Components
```rust
struct MyComponent {
shared_state: Entity<SharedData>,
}
impl MyComponent {
fn new(cx: &mut App) -> Entity<Self> {
let shared = cx.new(|_| SharedData::default());
cx.new(|cx| Self {
shared_state: shared,
})
}
fn update_shared(&mut self, cx: &mut Context<Self>) {
self.shared_state.update(cx, |state, cx| {
state.value = 42;
cx.notify();
});
}
}
```
### Async Operations
```rust
impl MyComponent {
fn fetch_data(&mut self, cx: &mut Context<Self>) {
let weak_self = cx.entity().downgrade();
cx.spawn(async move |cx| {
let data = fetch_from_api().await;
// Update entity safely
let _ = weak_self.update(cx, |state, cx| {
state.data = Some(data);
cx.notify();
});
}).detach();
}
}
```
## Core Principles
### Always Use Weak References in Closures
```rust
// ✅ Good: Weak reference prevents retain cycles
let weak = cx.entity().downgrade();
callback(move || {
let _ = weak.update(cx, |state, cx| cx.notify());
});
// ❌ Bad: Strong reference may cause memory leak
let strong = cx.entity();
callback(move || {
strong.update(cx, |state, cx| cx.notify());
});
```
### Use Inner Context
```rust
// ✅ Good: Use inner cx from closure
entity.update(cx, |state, inner_cx| {
inner_cx.notify(); // Correct
});
// ❌ Bad: Use outer cx (multiple borrow error)
entity.update(cx, |state, inner_cx| {
cx.notify(); // Wrong!
});
```
### Avoid Nested Updates
```rust
// ✅ Good: Sequential updates
entity1.update(cx, |state, cx| { /* ... */ });
entity2.update(cx, |state, cx| { /* ... */ });
// ❌ Bad: Nested updates (may panic)
entity1.update(cx, |_, cx| {
entity2.update(cx, |_, cx| { /* ... */ });
});
```
## Common Use Cases
1. **Component State**: Internal state that needs reactivity
2. **Shared State**: State shared between multiple components
3. **Parent-Child**: Coordinating between related components (use weak refs)
4. **Async State**: Managing state that changes from async operations
5. **Observations**: Reacting to changes in other entities
## Reference Documentation
### Complete API Documentation
- **Entity API**: See [api-reference.md](references/api-reference.md)
- Entity types, methods, lifecycle
- Context methods, async operations
- Error handling, type conversions
### Implementation Guides
- **Patterns**: See [patterns.md](references/patterns.md)
- Model-view separation, state management
- Cross-entity communication, async operations
- Observer pattern, event subscription
- Pattern selection guide
- **Best Practices**: See [best-practices.md](references/best-practices.md)
- Avoiding common pitfalls, memory leaks
- Performance optimization, batching updates
- Lifecycle management, cleanup
- Async best practices, testing
- **Advanced Patterns**: See [advanced.md](references/advanced.md)
- Entity collections, registry pattern
- Debounced/throttled updates, state machines
- Entity snapshots, transactions, pools

View File

@@ -0,0 +1,528 @@
# Advanced Entity Patterns
Advanced techniques for sophisticated entity management scenarios.
## Entity Collections Management
### Dynamic Collection with Cleanup
```rust
struct EntityCollection<T> {
strong_refs: Vec<Entity<T>>,
weak_refs: Vec<WeakEntity<T>>,
}
impl<T> EntityCollection<T> {
fn new() -> Self {
Self {
strong_refs: Vec::new(),
weak_refs: Vec::new(),
}
}
fn add(&mut self, entity: Entity<T>, cx: &mut App) {
self.strong_refs.push(entity.clone());
self.weak_refs.push(entity.downgrade());
}
fn remove(&mut self, entity_id: EntityId, cx: &mut App) {
self.strong_refs.retain(|e| e.entity_id() != entity_id);
self.weak_refs.retain(|w| {
w.upgrade()
.map(|e| e.entity_id() != entity_id)
.unwrap_or(false)
});
}
fn cleanup_invalid(&mut self, cx: &mut App) {
self.weak_refs.retain(|weak| weak.upgrade().is_some());
}
fn for_each<F>(&self, cx: &mut App, mut f: F)
where
F: FnMut(&Entity<T>, &mut App),
{
for entity in &self.strong_refs {
f(entity, cx);
}
}
fn for_each_weak<F>(&mut self, cx: &mut App, mut f: F)
where
F: FnMut(Entity<T>, &mut App),
{
self.weak_refs.retain(|weak| {
if let Some(entity) = weak.upgrade() {
f(entity, cx);
true
} else {
false // Remove invalid weak references
}
});
}
}
```
### Entity Registry Pattern
```rust
use std::collections::HashMap;
struct EntityRegistry<T> {
entities: HashMap<EntityId, WeakEntity<T>>,
}
impl<T> EntityRegistry<T> {
fn new() -> Self {
Self {
entities: HashMap::new(),
}
}
fn register(&mut self, entity: &Entity<T>) {
self.entities.insert(entity.entity_id(), entity.downgrade());
}
fn unregister(&mut self, entity_id: EntityId) {
self.entities.remove(&entity_id);
}
fn get(&self, entity_id: EntityId) -> Option<Entity<T>> {
self.entities.get(&entity_id)?.upgrade()
}
fn cleanup(&mut self) {
self.entities.retain(|_, weak| weak.upgrade().is_some());
}
fn count(&self) -> usize {
self.entities.len()
}
fn all_entities(&self) -> Vec<Entity<T>> {
self.entities
.values()
.filter_map(|weak| weak.upgrade())
.collect()
}
}
```
## Conditional Update Patterns
### Debounced Updates
```rust
use std::time::{Duration, Instant};
struct DebouncedEntity<T> {
entity: Entity<T>,
last_update: Instant,
debounce_duration: Duration,
pending_update: Option<Box<dyn FnOnce(&mut T, &mut Context<T>)>>,
}
impl<T: 'static> DebouncedEntity<T> {
fn new(entity: Entity<T>, debounce_ms: u64) -> Self {
Self {
entity,
last_update: Instant::now(),
debounce_duration: Duration::from_millis(debounce_ms),
pending_update: None,
}
}
fn update<F>(&mut self, cx: &mut App, update_fn: F)
where
F: FnOnce(&mut T, &mut Context<T>) + 'static,
{
let now = Instant::now();
let elapsed = now.duration_since(self.last_update);
if elapsed >= self.debounce_duration {
// Execute immediately
self.entity.update(cx, update_fn);
self.last_update = now;
self.pending_update = None;
} else {
// Store for later
self.pending_update = Some(Box::new(update_fn));
// Schedule execution
let entity = self.entity.clone();
let delay = self.debounce_duration - elapsed;
cx.spawn(async move |cx| {
tokio::time::sleep(delay).await;
if let Some(update) = self.pending_update.take() {
entity.update(cx, |state, inner_cx| {
update(state, inner_cx);
});
}
}).detach();
}
}
}
```
### Throttled Updates
```rust
struct ThrottledEntity<T> {
entity: Entity<T>,
last_update: Instant,
throttle_duration: Duration,
}
impl<T: 'static> ThrottledEntity<T> {
fn new(entity: Entity<T>, throttle_ms: u64) -> Self {
Self {
entity,
last_update: Instant::now(),
throttle_duration: Duration::from_millis(throttle_ms),
}
}
fn try_update<F>(&mut self, cx: &mut App, update_fn: F) -> bool
where
F: FnOnce(&mut T, &mut Context<T>),
{
let now = Instant::now();
let elapsed = now.duration_since(self.last_update);
if elapsed >= self.throttle_duration {
self.entity.update(cx, update_fn);
self.last_update = now;
true
} else {
false // Update throttled
}
}
}
```
## Entity State Machine Pattern
```rust
enum AppState {
Idle,
Loading,
Loaded(String),
Error(String),
}
struct StateMachine {
state: AppState,
}
impl StateMachine {
fn new() -> Self {
Self {
state: AppState::Idle,
}
}
fn start_loading(&mut self, cx: &mut Context<Self>) {
if matches!(self.state, AppState::Idle | AppState::Error(_)) {
self.state = AppState::Loading;
cx.notify();
let weak_entity = cx.entity().downgrade();
cx.spawn(async move |cx| {
let result = perform_load().await;
let _ = weak_entity.update(cx, |state, cx| {
match result {
Ok(data) => state.on_load_success(data, cx),
Err(e) => state.on_load_error(e.to_string(), cx),
}
});
}).detach();
}
}
fn on_load_success(&mut self, data: String, cx: &mut Context<Self>) {
if matches!(self.state, AppState::Loading) {
self.state = AppState::Loaded(data);
cx.notify();
}
}
fn on_load_error(&mut self, error: String, cx: &mut Context<Self>) {
if matches!(self.state, AppState::Loading) {
self.state = AppState::Error(error);
cx.notify();
}
}
fn reset(&mut self, cx: &mut Context<Self>) {
self.state = AppState::Idle;
cx.notify();
}
}
async fn perform_load() -> Result<String, anyhow::Error> {
// Actual load implementation
Ok("Data".to_string())
}
```
## Entity Proxy Pattern
```rust
struct EntityProxy<T> {
entity: WeakEntity<T>,
}
impl<T> EntityProxy<T> {
fn new(entity: &Entity<T>) -> Self {
Self {
entity: entity.downgrade(),
}
}
fn with<F, R>(&self, cx: &mut App, f: F) -> Result<R, anyhow::Error>
where
F: FnOnce(&T, &App) -> R,
{
self.entity.read_with(cx, f)
}
fn update<F, R>(&self, cx: &mut App, f: F) -> Result<R, anyhow::Error>
where
F: FnOnce(&mut T, &mut Context<T>) -> R,
{
self.entity.update(cx, f)
}
fn is_valid(&self, cx: &App) -> bool {
self.entity.upgrade().is_some()
}
}
```
## Cascading Updates Pattern
```rust
struct CascadingUpdater {
entities: Vec<WeakEntity<UpdateTarget>>,
}
impl CascadingUpdater {
fn new() -> Self {
Self {
entities: Vec::new(),
}
}
fn add_target(&mut self, entity: &Entity<UpdateTarget>) {
self.entities.push(entity.downgrade());
}
fn cascade_update<F>(&mut self, cx: &mut App, update_fn: F)
where
F: Fn(&mut UpdateTarget, &mut Context<UpdateTarget>) + Clone,
{
// Update all entities in sequence
self.entities.retain(|weak| {
if let Ok(_) = weak.update(cx, |state, inner_cx| {
update_fn.clone()(state, inner_cx);
}) {
true // Keep valid entity
} else {
false // Remove invalid entity
}
});
}
}
struct UpdateTarget {
value: i32,
}
```
## Entity Snapshot Pattern
```rust
use serde::{Serialize, Deserialize};
#[derive(Clone, Serialize, Deserialize)]
struct EntitySnapshot {
data: String,
timestamp: u64,
}
struct SnapshotableEntity {
data: String,
snapshots: Vec<EntitySnapshot>,
}
impl SnapshotableEntity {
fn new(data: String) -> Self {
Self {
data,
snapshots: Vec::new(),
}
}
fn take_snapshot(&mut self, cx: &mut Context<Self>) {
let snapshot = EntitySnapshot {
data: self.data.clone(),
timestamp: current_timestamp(),
};
self.snapshots.push(snapshot);
cx.notify();
}
fn restore_snapshot(&mut self, index: usize, cx: &mut Context<Self>) -> Result<(), String> {
if let Some(snapshot) = self.snapshots.get(index) {
self.data = snapshot.data.clone();
cx.notify();
Ok(())
} else {
Err("Invalid snapshot index".to_string())
}
}
fn clear_old_snapshots(&mut self, keep_last: usize, cx: &mut Context<Self>) {
if self.snapshots.len() > keep_last {
self.snapshots.drain(0..self.snapshots.len() - keep_last);
cx.notify();
}
}
}
fn current_timestamp() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
}
```
## Entity Transaction Pattern
```rust
struct Transaction<T> {
entity: Entity<T>,
original_state: Option<T>,
}
impl<T: Clone> Transaction<T> {
fn begin(entity: Entity<T>, cx: &mut App) -> Self {
let original_state = entity.read(cx).clone();
Self {
entity,
original_state: Some(original_state),
}
}
fn update<F>(&mut self, cx: &mut App, update_fn: F)
where
F: FnOnce(&mut T, &mut Context<T>),
{
self.entity.update(cx, update_fn);
}
fn commit(mut self, cx: &mut App) {
self.original_state = None; // Don't rollback
self.entity.update(cx, |_, cx| {
cx.notify();
});
}
fn rollback(mut self, cx: &mut App) {
if let Some(original) = self.original_state.take() {
self.entity.update(cx, |state, cx| {
*state = original;
cx.notify();
});
}
}
}
impl<T> Drop for Transaction<T> {
fn drop(&mut self) {
// Auto-rollback if not committed
if self.original_state.is_some() {
eprintln!("Warning: Transaction dropped without commit");
}
}
}
// Usage
fn perform_transaction(entity: Entity<MyState>, cx: &mut App) -> Result<(), String> {
let mut tx = Transaction::begin(entity, cx);
tx.update(cx, |state, cx| {
state.value = 42;
});
if validate_state(&tx.entity, cx)? {
tx.commit(cx);
Ok(())
} else {
tx.rollback(cx);
Err("Validation failed".to_string())
}
}
```
## Entity Pool Pattern
```rust
struct EntityPool<T> {
available: Vec<Entity<T>>,
in_use: Vec<WeakEntity<T>>,
factory: Box<dyn Fn(&mut App) -> Entity<T>>,
}
impl<T: 'static> EntityPool<T> {
fn new<F>(factory: F) -> Self
where
F: Fn(&mut App) -> Entity<T> + 'static,
{
Self {
available: Vec::new(),
in_use: Vec::new(),
factory: Box::new(factory),
}
}
fn acquire(&mut self, cx: &mut App) -> Entity<T> {
let entity = if let Some(entity) = self.available.pop() {
entity
} else {
(self.factory)(cx)
};
self.in_use.push(entity.downgrade());
entity
}
fn release(&mut self, entity: Entity<T>, cx: &mut App) {
// Reset entity state if needed
entity.update(cx, |state, cx| {
// Reset logic here
cx.notify();
});
self.available.push(entity);
self.cleanup_in_use();
}
fn cleanup_in_use(&mut self) {
self.in_use.retain(|weak| weak.upgrade().is_some());
}
fn pool_size(&self) -> (usize, usize) {
(self.available.len(), self.in_use.len())
}
}
```
These advanced patterns provide powerful abstractions for managing complex entity scenarios while maintaining code quality and performance.

View 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
});
});
```

View File

@@ -0,0 +1,484 @@
# Entity Best Practices
Guidelines and best practices for effective entity management in GPUI.
## Avoiding Common Pitfalls
### Avoid Entity Borrowing Conflicts
**Problem:** Nested updates can cause borrow checker panics.
```rust
// ❌ Bad: Nested updates can panic
entity1.update(cx, |_, cx| {
entity2.update(cx, |_, cx| {
// This may panic if entities are related
});
});
```
**Solution:** Perform updates sequentially.
```rust
// ✅ Good: Sequential updates
entity1.update(cx, |state1, cx| {
// Update entity1
state1.value = 42;
cx.notify();
});
entity2.update(cx, |state2, cx| {
// Update entity2
state2.value = 100;
cx.notify();
});
```
### Use Weak References in Closures
**Problem:** Strong references in closures can create retain cycles and memory leaks.
```rust
// ❌ Bad: Strong reference creates retain cycle
impl MyComponent {
fn setup_callback(&mut self, cx: &mut Context<Self>) {
let entity = cx.entity(); // Strong reference
some_callback(move || {
entity.update(cx, |state, cx| {
// This closure holds a strong reference
// If the closure itself is retained by the entity, memory leak!
cx.notify();
});
});
}
}
```
**Solution:** Use weak references in closures.
```rust
// ✅ Good: Weak reference prevents retain cycle
impl MyComponent {
fn setup_callback(&mut self, cx: &mut Context<Self>) {
let weak_entity = cx.entity().downgrade(); // Weak reference
some_callback(move || {
// Safe: weak reference doesn't prevent cleanup
let _ = weak_entity.update(cx, |state, cx| {
cx.notify();
});
});
}
}
```
### Use Inner Context in Closures
**Problem:** Using outer context causes multiple borrow errors.
```rust
// ❌ Bad: Using outer cx causes borrow issues
entity.update(cx, |state, inner_cx| {
cx.notify(); // Wrong! Using outer cx
cx.spawn(...); // Multiple borrow error
});
```
**Solution:** Always use the inner context provided to the closure.
```rust
// ✅ Good: Use inner cx
entity.update(cx, |state, inner_cx| {
inner_cx.notify(); // Correct
inner_cx.spawn(...); // Works fine
});
```
### Entity as Props - Use Weak References
**Problem:** Strong entity references in props can create ownership issues.
```rust
// ❌ Questionable: Strong reference in child
struct ChildComponent {
parent: Entity<ParentComponent>, // Strong reference
}
```
**Better:** Use weak references for parent relationships.
```rust
// ✅ Good: Weak reference prevents issues
struct ChildComponent {
parent: WeakEntity<ParentComponent>, // Weak reference
}
impl ChildComponent {
fn notify_parent(&mut self, cx: &mut Context<Self>) {
// Check if parent still exists
if let Ok(_) = self.parent.update(cx, |parent_state, cx| {
// Update parent
cx.notify();
}) {
// Parent successfully updated
}
}
}
```
## Performance Optimization
### Minimize cx.notify() Calls
Each `cx.notify()` triggers a re-render. Batch updates when possible.
```rust
// ❌ Bad: Multiple notifications
impl MyComponent {
fn update_multiple_fields(&mut self, cx: &mut Context<Self>) {
self.field1 = new_value1;
cx.notify(); // Unnecessary intermediate notification
self.field2 = new_value2;
cx.notify(); // Unnecessary intermediate notification
self.field3 = new_value3;
cx.notify();
}
}
```
```rust
// ✅ Good: Single notification after all updates
impl MyComponent {
fn update_multiple_fields(&mut self, cx: &mut Context<Self>) {
self.field1 = new_value1;
self.field2 = new_value2;
self.field3 = new_value3;
cx.notify(); // Single notification
}
}
```
### Conditional Updates
Only notify when state actually changes.
```rust
impl MyComponent {
fn set_value(&mut self, new_value: i32, cx: &mut Context<Self>) {
if self.value != new_value {
self.value = new_value;
cx.notify(); // Only notify if changed
}
}
}
```
### Use read_with for Complex Operations
Prefer `read_with` over separate `read` calls.
```rust
// ❌ Less efficient: Multiple borrows
let state_ref = entity.read(cx);
let value1 = state_ref.field1;
let value2 = state_ref.field2;
// state_ref borrowed for entire scope
// ✅ More efficient: Single borrow with closure
let (value1, value2) = entity.read_with(cx, |state, cx| {
(state.field1, state.field2)
});
```
### Avoid Excessive Entity Creation
Creating entities has overhead. Reuse when appropriate.
```rust
// ❌ Bad: Creating entity per item in render
impl Render for MyList {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div().children(
self.items.iter().map(|item| {
// Don't create entities in render!
let entity = cx.new(|_| item.clone());
ItemView { entity }
})
)
}
}
```
```rust
// ✅ Good: Create entities once, reuse
struct MyList {
item_entities: Vec<Entity<Item>>,
}
impl MyList {
fn add_item(&mut self, item: Item, cx: &mut Context<Self>) {
let entity = cx.new(|_| item);
self.item_entities.push(entity);
cx.notify();
}
}
```
## Entity Lifecycle Management
### Clean Up Weak References
Periodically clean up invalid weak references from collections.
```rust
struct Container {
weak_children: Vec<WeakEntity<Child>>,
}
impl Container {
fn cleanup_invalid_children(&mut self, cx: &mut Context<Self>) {
// Remove weak references that are no longer valid
let before_count = self.weak_children.len();
self.weak_children.retain(|weak| weak.upgrade().is_some());
let after_count = self.weak_children.len();
if before_count != after_count {
cx.notify(); // Notify if list changed
}
}
}
```
### Entity Cloning and Sharing
Understand that cloning `Entity<T>` increases reference count.
```rust
// Each clone increases the reference count
let entity1: Entity<MyState> = cx.new(|_| MyState::default());
let entity2 = entity1.clone(); // Reference count: 2
let entity3 = entity1.clone(); // Reference count: 3
// Entity is dropped only when all references are dropped
drop(entity1); // Reference count: 2
drop(entity2); // Reference count: 1
drop(entity3); // Reference count: 0, entity is deallocated
```
### Proper Resource Cleanup
Implement cleanup in `Drop` or explicit cleanup methods.
```rust
struct ManagedResource {
handle: Option<FileHandle>,
}
impl ManagedResource {
fn close(&mut self, cx: &mut Context<Self>) {
if let Some(handle) = self.handle.take() {
// Explicit cleanup
handle.close();
cx.notify();
}
}
}
impl Drop for ManagedResource {
fn drop(&mut self) {
// Automatic cleanup when entity is dropped
if let Some(handle) = self.handle.take() {
handle.close();
}
}
}
```
## Entity Observation Best Practices
### Detach Subscriptions Appropriately
Call `.detach()` on subscriptions you want to keep alive.
```rust
impl MyComponent {
fn new(other_entity: Entity<OtherComponent>, cx: &mut App) -> Entity<Self> {
cx.new(|cx| {
// Observer will live as long as both entities exist
cx.observe(&other_entity, |this, observed, cx| {
// Handle changes
cx.notify();
}).detach(); // Important: detach to make permanent
Self { /* fields */ }
})
}
}
```
### Avoid Observation Cycles
Don't create mutual observation between entities.
```rust
// ❌ Bad: Mutual observation can cause infinite loops
entity1.update(cx, |_, cx| {
cx.observe(&entity2, |_, _, cx| {
cx.notify(); // May trigger entity2's observer
}).detach();
});
entity2.update(cx, |_, cx| {
cx.observe(&entity1, |_, _, cx| {
cx.notify(); // May trigger entity1's observer → infinite loop
}).detach();
});
```
## Async Best Practices
### Always Use Weak References in Async Tasks
```rust
// ✅ Good: Weak reference in spawned task
impl MyComponent {
fn fetch_data(&mut self, cx: &mut Context<Self>) {
let weak_entity = cx.entity().downgrade();
cx.spawn(async move |cx| {
let data = fetch_from_api().await;
// Entity may have been dropped during fetch
let _ = weak_entity.update(cx, |state, cx| {
state.data = Some(data);
cx.notify();
});
}).detach();
}
}
```
### Handle Async Errors Gracefully
```rust
impl MyComponent {
fn fetch_data(&mut self, cx: &mut Context<Self>) {
let weak_entity = cx.entity().downgrade();
cx.spawn(async move |cx| {
match fetch_from_api().await {
Ok(data) => {
let _ = weak_entity.update(cx, |state, cx| {
state.data = Some(data);
state.error = None;
cx.notify();
});
}
Err(e) => {
let _ = weak_entity.update(cx, |state, cx| {
state.error = Some(e.to_string());
cx.notify();
});
}
}
}).detach();
}
}
```
### Cancellation Patterns
Implement cancellation for long-running tasks.
```rust
struct DataFetcher {
current_task: Option<Task<()>>,
data: Option<String>,
}
impl DataFetcher {
fn fetch_data(&mut self, url: String, cx: &mut Context<Self>) {
// Cancel previous task
self.current_task = None; // Dropping task cancels it
let weak_entity = cx.entity().downgrade();
let task = cx.spawn(async move |cx| {
let data = fetch_from_url(&url).await?;
let _ = weak_entity.update(cx, |state, cx| {
state.data = Some(data);
cx.notify();
});
Ok::<(), anyhow::Error>(())
});
self.current_task = Some(task);
}
}
```
## Testing Best Practices
### Use TestAppContext for Entity Tests
```rust
#[cfg(test)]
mod tests {
use super::*;
use gpui::TestAppContext;
#[gpui::test]
fn test_entity_update(cx: &mut TestAppContext) {
let entity = cx.new(|_| MyState { count: 0 });
entity.update(cx, |state, cx| {
state.count += 1;
assert_eq!(state.count, 1);
});
let count = entity.read(cx).count;
assert_eq!(count, 1);
}
}
```
### Test Entity Observation
```rust
#[gpui::test]
fn test_entity_observation(cx: &mut TestAppContext) {
let observed = cx.new(|_| MyState { value: 0 });
let observer = cx.new(|cx| Observer::new(observed.clone(), cx));
// Update observed entity
observed.update(cx, |state, cx| {
state.value = 42;
cx.notify();
});
// Verify observer was notified
observer.read(cx).assert_observed();
}
```
## Performance Checklist
Before shipping entity-based code, verify:
- [ ] No strong references in closures/callbacks (use `WeakEntity`)
- [ ] No nested entity updates (use sequential updates)
- [ ] Using inner `cx` in update closures
- [ ] Batching updates before calling `cx.notify()`
- [ ] Cleaning up invalid weak references periodically
- [ ] Using `read_with` for complex read operations
- [ ] Properly detaching subscriptions and observers
- [ ] Using weak references in async tasks
- [ ] No observation cycles between entities
- [ ] Proper error handling in async operations
- [ ] Resource cleanup in `Drop` or explicit methods
- [ ] Tests cover entity lifecycle and interactions

View File

@@ -0,0 +1,579 @@
# Entity Patterns
Common patterns and use cases for entity management in GPUI.
## Application Scenarios
### Model-View Separation
Separate business logic (model) from UI (view) using entities.
```rust
struct CounterModel {
count: usize,
listeners: Vec<Box<dyn Fn(usize)>>,
}
struct CounterView {
model: Entity<CounterModel>,
}
impl CounterModel {
fn increment(&mut self, cx: &mut Context<Self>) {
self.count += 1;
// Notify listeners
for listener in &self.listeners {
listener(self.count);
}
cx.notify();
}
fn decrement(&mut self, cx: &mut Context<Self>) {
if self.count > 0 {
self.count -= 1;
cx.notify();
}
}
}
impl CounterView {
fn new(cx: &mut App) -> Entity<Self> {
let model = cx.new(|_cx| CounterModel {
count: 0,
listeners: Vec::new(),
});
cx.new(|cx| Self { model })
}
fn increment_count(&mut self, cx: &mut Context<Self>) {
self.model.update(cx, |model, cx| {
model.increment(cx);
});
}
}
impl Render for CounterView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let count = self.model.read(cx).count;
div()
.child(format!("Count: {}", count))
.child(
Button::new("increment")
.label("Increment")
.on_click(cx.listener(|this, _, cx| {
this.increment_count(cx);
}))
)
}
}
```
### Component State Management
Managing complex component state with entities.
```rust
struct TodoList {
todos: Vec<Todo>,
filter: TodoFilter,
next_id: usize,
}
struct Todo {
id: usize,
text: String,
completed: bool,
}
enum TodoFilter {
All,
Active,
Completed,
}
impl TodoList {
fn new() -> Self {
Self {
todos: Vec::new(),
filter: TodoFilter::All,
next_id: 0,
}
}
fn add_todo(&mut self, text: String, cx: &mut Context<Self>) {
self.todos.push(Todo {
id: self.next_id,
text,
completed: false,
});
self.next_id += 1;
cx.notify();
}
fn toggle_todo(&mut self, id: usize, cx: &mut Context<Self>) {
if let Some(todo) = self.todos.iter_mut().find(|t| t.id == id) {
todo.completed = !todo.completed;
cx.notify();
}
}
fn remove_todo(&mut self, id: usize, cx: &mut Context<Self>) {
self.todos.retain(|t| t.id != id);
cx.notify();
}
fn set_filter(&mut self, filter: TodoFilter, cx: &mut Context<Self>) {
self.filter = filter;
cx.notify();
}
fn visible_todos(&self) -> impl Iterator<Item = &Todo> {
self.todos.iter().filter(move |todo| match self.filter {
TodoFilter::All => true,
TodoFilter::Active => !todo.completed,
TodoFilter::Completed => todo.completed,
})
}
}
```
### Cross-Entity Communication
Coordinating state between parent and child entities.
```rust
struct ParentComponent {
child_entities: Vec<Entity<ChildComponent>>,
global_message: String,
}
struct ChildComponent {
id: usize,
message: String,
parent: WeakEntity<ParentComponent>,
}
impl ParentComponent {
fn new(cx: &mut App) -> Entity<Self> {
cx.new(|cx| Self {
child_entities: Vec::new(),
global_message: String::new(),
})
}
fn add_child(&mut self, cx: &mut Context<Self>) {
let parent_weak = cx.entity().downgrade();
let child_id = self.child_entities.len();
let child = cx.new(|cx| ChildComponent {
id: child_id,
message: String::new(),
parent: parent_weak,
});
self.child_entities.push(child);
cx.notify();
}
fn broadcast_message(&mut self, message: String, cx: &mut Context<Self>) {
self.global_message = message.clone();
// Update all children
for child in &self.child_entities {
child.update(cx, |child_state, cx| {
child_state.message = message.clone();
cx.notify();
});
}
cx.notify();
}
}
impl ChildComponent {
fn notify_parent(&mut self, message: String, cx: &mut Context<Self>) {
if let Ok(_) = self.parent.update(cx, |parent_state, cx| {
parent_state.global_message = format!("Child {}: {}", self.id, message);
cx.notify();
}) {
// Parent successfully notified
}
}
}
```
### Async Operations with Entities
Managing async state updates.
```rust
struct DataLoader {
loading: bool,
data: Option<String>,
error: Option<String>,
}
impl DataLoader {
fn new() -> Self {
Self {
loading: false,
data: None,
error: None,
}
}
fn load_data(&mut self, cx: &mut Context<Self>) {
// Set loading state
self.loading = true;
self.error = None;
cx.notify();
// Get weak reference for async task
let entity = cx.entity().downgrade();
cx.spawn(async move |cx| {
// Simulate async operation
tokio::time::sleep(Duration::from_secs(2)).await;
let result = fetch_data().await;
// Update entity with result
let _ = entity.update(cx, |state, cx| {
state.loading = false;
match result {
Ok(data) => state.data = Some(data),
Err(e) => state.error = Some(e.to_string()),
}
cx.notify();
});
}).detach();
}
}
async fn fetch_data() -> Result<String, anyhow::Error> {
// Actual fetch implementation
Ok("Fetched data".to_string())
}
```
### Background Task Coordination
Using background tasks with entity updates.
```rust
struct ImageProcessor {
images: Vec<ProcessedImage>,
processing: bool,
}
struct ProcessedImage {
path: PathBuf,
thumbnail: Option<Vec<u8>>,
}
impl ImageProcessor {
fn process_images(&mut self, paths: Vec<PathBuf>, cx: &mut Context<Self>) {
self.processing = true;
cx.notify();
let entity = cx.entity().downgrade();
cx.background_spawn({
let paths = paths.clone();
async move {
let mut processed = Vec::new();
for path in paths {
// Process image on background thread
let thumbnail = generate_thumbnail(&path).await;
processed.push((path, thumbnail));
}
// Send results back to foreground
processed
}
})
.then(cx.spawn(move |processed, cx| {
// Update entity on foreground thread
let _ = entity.update(cx, |state, cx| {
for (path, thumbnail) in processed {
state.images.push(ProcessedImage {
path,
thumbnail: Some(thumbnail),
});
}
state.processing = false;
cx.notify();
});
}))
.detach();
}
}
```
## Common Patterns
### 1. Stateful Components
Use entities for components that maintain internal state.
```rust
struct StatefulComponent {
value: i32,
history: Vec<i32>,
}
impl StatefulComponent {
fn update_value(&mut self, new_value: i32, cx: &mut Context<Self>) {
self.history.push(self.value);
self.value = new_value;
cx.notify();
}
fn undo(&mut self, cx: &mut Context<Self>) {
if let Some(prev_value) = self.history.pop() {
self.value = prev_value;
cx.notify();
}
}
}
```
### 2. Shared State
Share state between multiple components using entities.
```rust
struct SharedState {
theme: Theme,
user: Option<User>,
}
struct ComponentA {
shared: Entity<SharedState>,
}
struct ComponentB {
shared: Entity<SharedState>,
}
// Both components can read/update the same shared state
impl ComponentA {
fn update_theme(&mut self, theme: Theme, cx: &mut Context<Self>) {
self.shared.update(cx, |state, cx| {
state.theme = theme;
cx.notify();
});
}
}
```
### 3. Event Coordination
Use entities to coordinate events between components.
```rust
struct EventCoordinator {
listeners: Vec<WeakEntity<dyn EventListener>>,
}
trait EventListener {
fn on_event(&mut self, event: &AppEvent, cx: &mut App);
}
impl EventCoordinator {
fn emit_event(&mut self, event: AppEvent, cx: &mut Context<Self>) {
// Notify all listeners
self.listeners.retain(|weak_listener| {
weak_listener.update(cx, |listener, cx| {
listener.on_event(&event, cx);
}).is_ok()
});
cx.notify();
}
}
```
### 4. Async State Management
Manage state that changes based on async operations.
```rust
struct AsyncState<T> {
state: AsyncValue<T>,
}
enum AsyncValue<T> {
Loading,
Loaded(T),
Error(String),
}
impl<T> AsyncState<T> {
fn is_loading(&self) -> bool {
matches!(self.state, AsyncValue::Loading)
}
fn value(&self) -> Option<&T> {
match &self.state {
AsyncValue::Loaded(v) => Some(v),
_ => None,
}
}
}
```
### 5. Parent-Child Relationships
Manage hierarchical relationships with weak references.
```rust
struct Parent {
children: Vec<Entity<Child>>,
}
struct Child {
parent: WeakEntity<Parent>,
data: String,
}
impl Child {
fn notify_parent_of_change(&mut self, cx: &mut Context<Self>) {
if let Ok(_) = self.parent.update(cx, |parent, cx| {
// Parent can react to child change
cx.notify();
}) {
// Successfully notified
}
}
}
```
### 6. Observer Pattern
React to entity state changes using observers.
```rust
struct Observable {
value: i32,
}
struct Observer {
observed: Entity<Observable>,
}
impl Observer {
fn new(observed: Entity<Observable>, cx: &mut App) -> Entity<Self> {
cx.new(|cx| {
// Observe the entity
cx.observe(&observed, |this, observed_entity, cx| {
// React to changes
let value = observed_entity.read(cx).value;
println!("Value changed to: {}", value);
}).detach();
Self { observed }
})
}
}
```
### 7. Event Subscription
Handle events emitted by other entities.
```rust
#[derive(Clone)]
enum DataEvent {
Updated,
Deleted,
}
struct DataSource {
data: Vec<String>,
}
impl DataSource {
fn update_data(&mut self, cx: &mut Context<Self>) {
// Update data
cx.emit(DataEvent::Updated);
cx.notify();
}
}
struct DataConsumer {
source: Entity<DataSource>,
}
impl DataConsumer {
fn new(source: Entity<DataSource>, cx: &mut App) -> Entity<Self> {
cx.new(|cx| {
// Subscribe to events
cx.subscribe(&source, |this, source, event: &DataEvent, cx| {
match event {
DataEvent::Updated => {
// Handle update
cx.notify();
}
DataEvent::Deleted => {
// Handle deletion
}
}
}).detach();
Self { source }
})
}
}
```
### 8. Resource Management
Manage external resources with proper cleanup.
```rust
struct FileHandle {
path: PathBuf,
file: Option<File>,
}
impl FileHandle {
fn open(&mut self, cx: &mut Context<Self>) -> Result<()> {
self.file = Some(File::open(&self.path)?);
cx.notify();
Ok(())
}
fn close(&mut self, cx: &mut Context<Self>) {
self.file = None;
cx.notify();
}
}
impl Drop for FileHandle {
fn drop(&mut self) {
// Cleanup when entity is dropped
if let Some(file) = self.file.take() {
drop(file);
}
}
}
```
## Pattern Selection Guide
| Need | Pattern | Complexity |
|------|---------|------------|
| Component with internal state | Stateful Components | Low |
| State shared by multiple components | Shared State | Low |
| Coordinate events between components | Event Coordination | Medium |
| Handle async data fetching | Async State Management | Medium |
| Parent-child component hierarchy | Parent-Child Relationships | Medium |
| React to state changes | Observer Pattern | Medium |
| Handle custom events | Event Subscription | Medium-High |
| Manage external resources | Resource Management | High |
Choose the simplest pattern that meets your requirements. Combine patterns as needed for complex scenarios.

View File

@@ -0,0 +1,176 @@
---
name: gpui-event
description: Event handling and subscriptions in GPUI. Use when implementing events, observers, or event-driven patterns. Supports custom events, entity observations, and event subscriptions for coordinating between components.
---
## Overview
GPUI provides event system for component coordination:
**Event Mechanisms:**
- **Custom Events**: Define and emit type-safe events
- **Observations**: React to entity state changes
- **Subscriptions**: Listen to events from other entities
- **Global Events**: App-wide event handling
## Quick Start
### Define and Emit Events
```rust
#[derive(Clone)]
enum MyEvent {
DataUpdated(String),
ActionTriggered,
}
impl MyComponent {
fn update_data(&mut self, data: String, cx: &mut Context<Self>) {
self.data = data.clone();
// Emit event
cx.emit(MyEvent::DataUpdated(data));
cx.notify();
}
}
```
### Subscribe to Events
```rust
impl Listener {
fn new(source: Entity<MyComponent>, cx: &mut App) -> Entity<Self> {
cx.new(|cx| {
// Subscribe to events
cx.subscribe(&source, |this, emitter, event: &MyEvent, cx| {
match event {
MyEvent::DataUpdated(data) => {
this.handle_update(data.clone(), cx);
}
MyEvent::ActionTriggered => {
this.handle_action(cx);
}
}
}).detach();
Self { source }
})
}
}
```
### Observe Entity Changes
```rust
impl Observer {
fn new(target: Entity<Target>, cx: &mut App) -> Entity<Self> {
cx.new(|cx| {
// Observe entity for any changes
cx.observe(&target, |this, observed, cx| {
// Called when observed.update() calls cx.notify()
println!("Target changed");
cx.notify();
}).detach();
Self { target }
})
}
}
```
## Common Patterns
### 1. Parent-Child Communication
```rust
// Parent emits events
impl Parent {
fn notify_children(&mut self, cx: &mut Context<Self>) {
cx.emit(ParentEvent::Updated);
cx.notify();
}
}
// Children subscribe
impl Child {
fn new(parent: Entity<Parent>, cx: &mut App) -> Entity<Self> {
cx.new(|cx| {
cx.subscribe(&parent, |this, parent, event, cx| {
this.handle_parent_event(event, cx);
}).detach();
Self { parent }
})
}
}
```
### 2. Global Event Broadcasting
```rust
struct EventBus {
listeners: Vec<WeakEntity<dyn Listener>>,
}
impl EventBus {
fn broadcast(&mut self, event: GlobalEvent, cx: &mut Context<Self>) {
self.listeners.retain(|weak| {
weak.update(cx, |listener, cx| {
listener.on_event(&event, cx);
}).is_ok()
});
}
}
```
### 3. Observer Pattern
```rust
cx.observe(&entity, |this, observed, cx| {
// React to any state change
let state = observed.read(cx);
this.sync_with_state(state, cx);
}).detach();
```
## Best Practices
### ✅ Detach Subscriptions
```rust
// ✅ Detach to keep alive
cx.subscribe(&entity, |this, source, event, cx| {
// Handle event
}).detach();
```
### ✅ Clean Event Types
```rust
#[derive(Clone)]
enum AppEvent {
DataChanged { id: usize, value: String },
ActionPerformed(ActionType),
Error(String),
}
```
### ❌ Avoid Event Loops
```rust
// ❌ Don't create mutual subscriptions
entity1.subscribe(entity2) emits event
entity2.subscribe(entity1) emits event infinite loop!
```
## Reference Documentation
- **API Reference**: See [api-reference.md](references/api-reference.md)
- Event definition, emission, subscriptions
- Observations, global events
- Subscription lifecycle
- **Patterns**: See [patterns.md](references/patterns.md)
- Event-driven architectures
- Communication patterns
- Best practices and pitfalls

View File

@@ -0,0 +1,232 @@
---
name: gpui-focus-handle
description: Focus management and keyboard navigation in GPUI. Use when handling focus, focus handles, or keyboard navigation. Enables keyboard-driven interfaces with proper focus tracking and navigation between focusable elements.
---
## Overview
GPUI's focus system enables keyboard navigation and focus management.
**Key Concepts:**
- **FocusHandle**: Reference to focusable element
- **Focus tracking**: Current focused element
- **Keyboard navigation**: Tab/Shift-Tab between elements
- **Focus events**: on_focus, on_blur
## Quick Start
### Creating Focus Handles
```rust
struct FocusableComponent {
focus_handle: FocusHandle,
}
impl FocusableComponent {
fn new(cx: &mut Context<Self>) -> Self {
Self {
focus_handle: cx.focus_handle(),
}
}
}
```
### Making Elements Focusable
```rust
impl Render for FocusableComponent {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::on_enter))
.child("Focusable content")
}
fn on_enter(&mut self, _: &Enter, cx: &mut Context<Self>) {
// Handle Enter key when focused
cx.notify();
}
}
```
### Focus Management
```rust
impl MyComponent {
fn focus(&mut self, cx: &mut Context<Self>) {
self.focus_handle.focus(cx);
}
fn is_focused(&self, cx: &App) -> bool {
self.focus_handle.is_focused(cx)
}
fn blur(&mut self, cx: &mut Context<Self>) {
cx.blur();
}
}
```
## Focus Events
### Handling Focus Changes
```rust
impl Render for MyInput {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_focused = self.focus_handle.is_focused(cx);
div()
.track_focus(&self.focus_handle)
.on_focus(cx.listener(|this, _event, cx| {
this.on_focus(cx);
}))
.on_blur(cx.listener(|this, _event, cx| {
this.on_blur(cx);
}))
.when(is_focused, |el| {
el.bg(cx.theme().focused_background)
})
.child(self.render_content())
}
}
impl MyInput {
fn on_focus(&mut self, cx: &mut Context<Self>) {
// Handle focus gained
cx.notify();
}
fn on_blur(&mut self, cx: &mut Context<Self>) {
// Handle focus lost
cx.notify();
}
}
```
## Keyboard Navigation
### Tab Order
Elements with `track_focus()` automatically participate in Tab navigation.
```rust
div()
.child(
input1.track_focus(&focus1) // Tab order: 1
)
.child(
input2.track_focus(&focus2) // Tab order: 2
)
.child(
input3.track_focus(&focus3) // Tab order: 3
)
```
### Focus Within Containers
```rust
impl Container {
fn focus_first(&mut self, cx: &mut Context<Self>) {
if let Some(first) = self.children.first() {
first.update(cx, |child, cx| {
child.focus_handle.focus(cx);
});
}
}
fn focus_next(&mut self, cx: &mut Context<Self>) {
// Custom focus navigation logic
}
}
```
## Common Patterns
### 1. Auto-focus on Mount
```rust
impl MyDialog {
fn new(cx: &mut Context<Self>) -> Self {
let focus_handle = cx.focus_handle();
// Focus when created
focus_handle.focus(cx);
Self { focus_handle }
}
}
```
### 2. Focus Trap (Modal)
```rust
impl Modal {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.track_focus(&self.focus_handle)
.on_key_down(cx.listener(|this, event: &KeyDownEvent, cx| {
if event.key == Key::Tab {
// Keep focus within modal
this.focus_next_in_modal(cx);
cx.stop_propagation();
}
}))
.child(self.render_content())
}
}
```
### 3. Conditional Focus
```rust
impl Searchable {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.track_focus(&self.focus_handle)
.when(self.search_active, |el| {
el.on_mount(cx.listener(|this, _, cx| {
this.focus_handle.focus(cx);
}))
})
.child(self.search_input())
}
}
```
## Best Practices
### ✅ Track Focus on Interactive Elements
```rust
// ✅ Good: Track focus for keyboard interaction
input()
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::on_enter))
```
### ✅ Provide Visual Focus Indicators
```rust
let is_focused = self.focus_handle.is_focused(cx);
div()
.when(is_focused, |el| {
el.border_color(cx.theme().focused_border)
})
```
### ❌ Don't: Forget to Track Focus
```rust
// ❌ Bad: No track_focus, keyboard navigation won't work
div()
.on_action(cx.listener(Self::on_enter))
```
## Reference Documentation
- **API Reference**: See [api-reference.md](references/api-reference.md)
- FocusHandle API, focus management
- Events, keyboard navigation
- Best practices

View File

@@ -0,0 +1,204 @@
---
name: gpui-global
description: Global state management in GPUI. Use when implementing global state, app-wide configuration, or shared resources.
---
## Overview
Global state in GPUI provides app-wide shared data accessible from any context.
**Key Trait**: `Global` - Implement on types to make them globally accessible
## Quick Start
### Define Global State
```rust
use gpui::Global;
#[derive(Clone)]
struct AppSettings {
theme: Theme,
language: String,
}
impl Global for AppSettings {}
```
### Set and Access Globals
```rust
fn main() {
let app = Application::new();
app.run(|cx: &mut App| {
// Set global
cx.set_global(AppSettings {
theme: Theme::Dark,
language: "en".to_string(),
});
// Access global (read-only)
let settings = cx.global::<AppSettings>();
println!("Theme: {:?}", settings.theme);
});
}
```
### Update Globals
```rust
impl MyComponent {
fn change_theme(&mut self, new_theme: Theme, cx: &mut Context<Self>) {
cx.update_global::<AppSettings, _>(|settings, cx| {
settings.theme = new_theme;
// Global updates don't trigger automatic notifications
// Manually notify components that care
});
cx.notify(); // Re-render this component
}
}
```
## Common Use Cases
### 1. App Configuration
```rust
#[derive(Clone)]
struct AppConfig {
api_endpoint: String,
max_retries: u32,
timeout: Duration,
}
impl Global for AppConfig {}
// Set once at startup
cx.set_global(AppConfig {
api_endpoint: "https://api.example.com".to_string(),
max_retries: 3,
timeout: Duration::from_secs(30),
});
// Access anywhere
let config = cx.global::<AppConfig>();
```
### 2. Feature Flags
```rust
#[derive(Clone)]
struct FeatureFlags {
enable_beta_features: bool,
enable_analytics: bool,
}
impl Global for FeatureFlags {}
impl MyComponent {
fn render_beta_feature(&self, cx: &App) -> Option<impl IntoElement> {
let flags = cx.global::<FeatureFlags>();
if flags.enable_beta_features {
Some(div().child("Beta feature"))
} else {
None
}
}
}
```
### 3. Shared Services
```rust
#[derive(Clone)]
struct ServiceRegistry {
http_client: Arc<HttpClient>,
logger: Arc<Logger>,
}
impl Global for ServiceRegistry {}
impl MyComponent {
fn fetch_data(&mut self, cx: &mut Context<Self>) {
let registry = cx.global::<ServiceRegistry>();
let client = registry.http_client.clone();
cx.spawn(async move |cx| {
let data = client.get("api/data").await?;
// Process data...
Ok::<_, anyhow::Error>(())
}).detach();
}
}
```
## Best Practices
### ✅ Use Arc for Shared Resources
```rust
#[derive(Clone)]
struct GlobalState {
database: Arc<Database>, // Cheap to clone
cache: Arc<RwLock<Cache>>,
}
impl Global for GlobalState {}
```
### ✅ Immutable by Default
Globals are read-only by default. Use interior mutability when needed:
```rust
#[derive(Clone)]
struct Counter {
count: Arc<AtomicUsize>,
}
impl Global for Counter {}
impl Counter {
fn increment(&self) {
self.count.fetch_add(1, Ordering::SeqCst);
}
fn get(&self) -> usize {
self.count.load(Ordering::SeqCst)
}
}
```
### ❌ Don't: Overuse Globals
```rust
// ❌ Bad: Too many globals
cx.set_global(UserState { ... });
cx.set_global(CartState { ... });
cx.set_global(CheckoutState { ... });
// ✅ Good: Use entities for component state
let user_entity = cx.new(|_| UserState { ... });
```
## When to Use
**Use Globals for:**
- App-wide configuration
- Feature flags
- Shared services (HTTP client, logger)
- Read-only reference data
**Use Entities for:**
- Component-specific state
- State that changes frequently
- State that needs notifications
## Reference Documentation
- **API Reference**: See [api-reference.md](references/api-reference.md)
- Global trait, set_global, update_global
- Interior mutability patterns
- Best practices and anti-patterns

View File

@@ -0,0 +1,177 @@
---
name: gpui-layout-and-style
description: Layout and styling in GPUI. Use when styling components, layout systems, or CSS-like properties.
---
## Overview
GPUI provides CSS-like styling with Rust type safety.
**Key Concepts:**
- Flexbox layout system
- Styled trait for chaining styles
- Size units: `px()`, `rems()`, `relative()`
- Colors, borders, shadows
## Quick Start
### Basic Styling
```rust
use gpui::*;
div()
.w(px(200.))
.h(px(100.))
.bg(rgb(0x2196F3))
.text_color(rgb(0xFFFFFF))
.rounded(px(8.))
.p(px(16.))
.child("Styled content")
```
### Flexbox Layout
```rust
div()
.flex()
.flex_row() // or flex_col() for column
.gap(px(8.))
.items_center()
.justify_between()
.children([
div().child("Item 1"),
div().child("Item 2"),
div().child("Item 3"),
])
```
### Size Units
```rust
div()
.w(px(200.)) // Pixels
.h(rems(10.)) // Relative to font size
.w(relative(0.5)) // 50% of parent
.min_w(px(100.))
.max_w(px(400.))
```
## Common Patterns
### Centered Content
```rust
div()
.flex()
.items_center()
.justify_center()
.size_full()
.child("Centered")
```
### Card Layout
```rust
div()
.w(px(300.))
.bg(cx.theme().surface)
.rounded(px(8.))
.shadow_md()
.p(px(16.))
.gap(px(12.))
.flex()
.flex_col()
.child(heading())
.child(content())
```
### Responsive Spacing
```rust
div()
.p(px(16.)) // Padding all sides
.px(px(20.)) // Padding horizontal
.py(px(12.)) // Padding vertical
.pt(px(8.)) // Padding top
.gap(px(8.)) // Gap between children
```
## Styling Methods
### Dimensions
```rust
.w(px(200.)) // Width
.h(px(100.)) // Height
.size(px(200.)) // Width and height
.min_w(px(100.)) // Min width
.max_w(px(400.)) // Max width
```
### Colors
```rust
.bg(rgb(0x2196F3)) // Background
.text_color(rgb(0xFFFFFF)) // Text color
.border_color(rgb(0x000000)) // Border color
```
### Borders
```rust
.border(px(1.)) // Border width
.rounded(px(8.)) // Border radius
.rounded_t(px(8.)) // Top corners
.border_color(rgb(0x000000))
```
### Spacing
```rust
.p(px(16.)) // Padding
.m(px(8.)) // Margin
.gap(px(8.)) // Gap between flex children
```
### Flexbox
```rust
.flex() // Enable flexbox
.flex_row() // Row direction
.flex_col() // Column direction
.items_center() // Align items center
.justify_between() // Space between items
.flex_grow() // Grow to fill space
```
## Theme Integration
```rust
div()
.bg(cx.theme().surface)
.text_color(cx.theme().foreground)
.border_color(cx.theme().border)
.when(is_hovered, |el| {
el.bg(cx.theme().hover)
})
```
## Conditional Styling
```rust
div()
.when(is_active, |el| {
el.bg(cx.theme().primary)
})
.when_some(optional_color, |el, color| {
el.bg(color)
})
```
## Reference Documentation
- **Complete Guide**: See [reference.md](references/reference.md)
- All styling methods
- Layout strategies
- Theming, responsive design

View File

@@ -0,0 +1,94 @@
---
name: gpui-test
description: Writing tests for GPUI applications. Use when testing components, async operations, or UI behavior.
---
## Overview
GPUI provides a comprehensive testing framework that allows you to test UI components, async operations, and distributed systems. Tests run on a single-threaded executor that provides deterministic execution and the ability to test complex async scenarios. GPUI tests use the `#[gpui::test]` attribute and work with `TestAppContext` for basic testing and `VisualTestContext` for window-dependent tests.
### Rules
- If test does not require windows or rendering, we can avoid use `[gpui::test]` and `TestAppContext`, just write simple rust test.
## Core Testing Infrastructure
### Test Attributes
#### Basic Test
```rust
#[gpui::test]
fn my_test(cx: &mut TestAppContext) {
// Test implementation
}
```
#### Async Test
```rust
#[gpui::test]
async fn my_async_test(cx: &mut TestAppContext) {
// Async test implementation
}
```
#### Property Test with Iterations
```rust
#[gpui::test(iterations = 10)]
fn my_property_test(cx: &mut TestAppContext, mut rng: StdRng) {
// Property testing with random data
}
```
### Test Contexts
#### TestAppContext
`TestAppContext` provides access to GPUI's core functionality without windows:
```rust
#[gpui::test]
fn test_entity_operations(cx: &mut TestAppContext) {
// Create entities
let entity = cx.new(|cx| MyComponent::new(cx));
// Update entities
entity.update(cx, |component, cx| {
component.value = 42;
cx.notify();
});
// Read entities
let value = entity.read_with(cx, |component, _| component.value);
assert_eq!(value, 42);
}
```
#### VisualTestContext
`VisualTestContext` extends `TestAppContext` with window support:
```rust
#[gpui::test]
fn test_with_window(cx: &mut TestAppContext) {
// Create window with component
let window = cx.update(|cx| {
cx.open_window(Default::default(), |_, cx| {
cx.new(|cx| MyComponent::new(cx))
}).unwrap()
});
// Convert to visual context
let mut cx = VisualTestContext::from_window(window.into(), cx);
// Access window and component
let component = window.root(&mut cx).unwrap();
}
```
## Additional Resources
- For detailed testing patterns and examples, see [reference.md](reference.md)
- For best practices and running tests, see [examples.md](examples.md)

View File

@@ -0,0 +1,172 @@
## Testing Best Practices
### Test Organization
Group related tests in modules:
```rust
#[cfg(test)]
mod tests {
use super::*;
mod entity_tests {
use super::*;
#[gpui::test]
fn test_creation() { /* ... */ }
#[gpui::test]
fn test_updates() { /* ... */ }
}
mod async_tests {
use super::*;
#[gpui::test]
async fn test_async_ops() { /* ... */ }
}
mod distributed_tests {
use super::*;
#[gpui::test]
fn test_multi_app() { /* ... */ }
}
}
```
### Setup and Teardown
Use helper functions for common setup:
```rust
fn create_test_counter(cx: &mut TestAppContext) -> Entity<Counter> {
cx.new(|cx| Counter::new(cx))
}
#[gpui::test]
fn test_counter_operations(cx: &mut TestAppContext) {
let counter = create_test_counter(cx);
// Test operations
}
```
### Assertions
Use descriptive assertions:
```rust
#[gpui::test]
fn test_counter_bounds(cx: &mut TestAppContext) {
let counter = create_test_counter(cx);
// Test upper bound
for _ in 0..100 {
counter.update(cx, |counter, cx| {
counter.increment(cx);
});
}
let count = counter.read_with(cx, |counter, _| counter.count);
assert!(count <= 100, "Counter should not exceed maximum");
// Test lower bound
for _ in 0..200 {
counter.update(cx, |counter, cx| {
counter.decrement(cx);
});
}
let count = counter.read_with(cx, |counter, _| counter.count);
assert!(count >= 0, "Counter should not go below minimum");
}
```
### Performance Testing
Test performance characteristics:
```rust
#[gpui::test]
fn test_operation_performance(cx: &mut TestAppContext) {
let component = cx.new(|cx| MyComponent::new(cx));
let start = std::time::Instant::now();
// Perform many operations
for i in 0..1000 {
component.update(cx, |comp, cx| {
comp.perform_operation(i, cx);
});
}
let elapsed = start.elapsed();
assert!(elapsed < Duration::from_millis(100), "Operations should complete quickly");
}
```
## Running Tests
### Basic Test Execution
```bash
# Run all tests
cargo test
# Run specific test
cargo test test_counter_operations
# Run tests in a specific module
cargo test entity_tests::
# Run with output
cargo test -- --nocapture
```
### Test Configuration
Enable test-support feature for GPUI tests:
```toml
[features]
test-support = ["gpui/test-support"]
```
```bash
cargo test --features test-support
```
### Advanced Test Execution
```bash
# Run tests with iterations for property testing
cargo test -- --test-threads=1
# Run tests matching a pattern
cargo test test_async
# Run tests with backtrace on failure
RUST_BACKTRACE=1 cargo test
```
### CI/CD Integration
For continuous integration:
```yaml
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable
- name: Run tests
run: cargo test --features test-support
```
GPUI's testing framework provides deterministic, fast, and comprehensive testing capabilities that mirror real application behavior while providing the control needed for thorough testing of complex UI and async scenarios.

View File

@@ -0,0 +1,350 @@
## Testing Patterns
### Basic Entity Testing
Test entity creation, updates, and reads:
```rust
#[gpui::test]
fn test_counter_entity(cx: &mut TestAppContext) {
let counter = cx.new(|cx| Counter::new(cx));
// Test initial state
let initial_count = counter.read_with(cx, |counter, _| counter.count);
assert_eq!(initial_count, 0);
// Test updates
counter.update(cx, |counter, cx| {
counter.count = 42;
cx.notify();
});
let updated_count = counter.read_with(cx, |counter, _| counter.count);
assert_eq!(updated_count, 42);
}
```
### Event Testing
Test event emission and handling:
```rust
#[derive(Clone)]
struct ValueChanged {
new_value: i32,
}
impl EventEmitter<ValueChanged> for MyComponent {}
#[gpui::test]
fn test_event_emission(cx: &mut TestAppContext) {
let component = cx.new(|cx| {
let mut comp = MyComponent::default();
// Subscribe to self
cx.subscribe_self(|this, event: &ValueChanged, cx| {
this.received_value = event.new_value;
cx.notify();
});
comp
});
// Emit event
component.update(cx, |_, cx| {
cx.emit(ValueChanged { new_value: 123 });
});
// Verify event was handled
let received = component.read_with(cx, |comp, _| comp.received_value);
assert_eq!(received, 123);
}
```
### Action Testing
Test action dispatching and handling:
```rust
actions!(my_app, [Increment, Decrement]);
#[gpui::test]
fn test_action_dispatch(cx: &mut TestAppContext) {
let window = cx.update(|cx| {
cx.open_window(Default::default(), |_, cx| {
cx.new(|cx| MyComponent::new(cx))
}).unwrap()
});
let mut cx = VisualTestContext::from_window(window.into(), cx);
let counter = window.root(&mut cx).unwrap();
// Dispatch action via focus handle
let focus_handle = counter.read_with(&cx, |counter, _| counter.focus_handle.clone());
cx.update(|window, cx| {
focus_handle.dispatch_action(&Increment, window, cx);
});
let count = counter.read_with(&cx, |counter, _| counter.count);
assert_eq!(count, 1);
}
```
### Async Testing
Test async operations and background tasks:
```rust
impl MyComponent {
fn load_data(&self, cx: &mut Context<Self>) -> Task<i32> {
cx.spawn(async move |this, cx| {
// Simulate async work
this.update(cx, |comp, _| comp.loading = true).await;
// Return result
42
})
}
fn background_update(&self, cx: &mut Context<Self>) {
cx.spawn(async move |this, cx| {
// Background work
this.update(cx, |comp, _| {
comp.value += 10;
}).await;
}).detach();
}
}
#[gpui::test]
async fn test_async_operations(cx: &mut TestAppContext) {
let component = cx.new(|cx| MyComponent::new(cx));
// Test awaited task
let result = component.update(cx, |comp, cx| comp.load_data(cx)).await;
assert_eq!(result, 42);
// Test detached task
component.update(cx, |comp, cx| comp.background_update(cx));
// Detached tasks don't run until you yield
let value_before = component.read_with(cx, |comp, _| comp.value);
assert_eq!(value_before, 0);
// Run pending tasks
cx.run_until_parked();
let value_after = component.read_with(cx, |comp, _| comp.value);
assert_eq!(value_after, 10);
}
```
### Timer Testing
Test timer-based operations:
```rust
impl MyComponent {
fn delayed_action(&self, cx: &mut Context<Self>) {
cx.spawn(async move |this, cx| {
cx.background_executor()
.timer(Duration::from_millis(100))
.await;
this.update(cx, |comp, cx| {
comp.action_performed = true;
cx.notify();
}).await;
}).detach();
}
}
#[gpui::test]
async fn test_timers(cx: &mut TestAppContext) {
let component = cx.new(|cx| MyComponent::new(cx));
component.update(cx, |comp, cx| comp.delayed_action(cx));
// Action shouldn't have completed yet
let performed = component.read_with(cx, |comp, _| comp.action_performed);
assert!(!performed);
// Run until parked (timers complete)
cx.run_until_parked();
let performed = component.read_with(cx, |comp, _| comp.action_performed);
assert!(performed);
}
```
### External I/O Testing
For tests involving external systems, use `allow_parking()`:
```rust
#[gpui::test]
async fn test_external_io(cx: &mut TestAppContext) {
// Allow parking for external I/O
cx.executor().allow_parking();
// Simulate external operation
let (tx, rx) = futures::channel::oneshot::channel();
std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(10));
tx.send(42).ok();
});
let result = rx.await.unwrap();
assert_eq!(result, 42);
}
```
## Property Testing
Use random data to test edge cases:
```rust
#[gpui::test(iterations = 10)]
fn test_counter_random_operations(cx: &mut TestAppContext, mut rng: StdRng) {
let counter = cx.new(|cx| Counter::new(cx));
let mut expected = 0i32;
for _ in 0..100 {
let delta = rng.random_range(-10..=10);
expected += delta;
counter.update(cx, |counter, cx| {
counter.count += delta;
cx.notify();
});
}
let actual = counter.read_with(cx, |counter, _| counter.count);
assert_eq!(actual, expected);
}
```
## Distributed Systems Testing
Test multiple app contexts communicating:
```rust
#[derive(Clone)]
struct NetworkMessage {
from: String,
to: String,
data: i32,
}
#[gpui::test]
fn test_distributed_apps(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
// Create components in different app contexts
let comp_a = cx_a.new(|_| MyComponent::new("A".to_string()));
let comp_b = cx_b.new(|_| MyComponent::new("B".to_string()));
// Simulate message passing
comp_a.update(cx_a, |comp, cx| {
comp.send_message("B", 42, cx);
});
// Run async operations
cx_a.run_until_parked();
// Verify message received in other context
comp_b.update(cx_b, |comp, _| {
comp.receive_messages();
});
let messages = comp_b.read_with(cx_b, |comp, _| comp.messages.clone());
assert_eq!(messages.len(), 1);
assert_eq!(messages[0].data, 42);
}
```
### Interleaving Testing
Test concurrent operations with random execution order:
```rust
#[gpui::test(iterations = 10)]
fn test_concurrent_operations(
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
mut rng: StdRng,
) {
let comp_a = cx_a.new(|_| MyComponent::new());
let comp_b = cx_b.new(|_| MyComponent::new());
// Perform random operations across contexts
for i in 0..20 {
if rng.random_bool(0.5) {
comp_a.update(cx_a, |comp, cx| {
comp.perform_operation(i, cx);
});
} else {
comp_b.update(cx_b, |comp, cx| {
comp.perform_operation(i, cx);
});
}
}
// Run all pending operations
cx_a.run_until_parked();
// Verify final state
let state_a = comp_a.read_with(cx_a, |comp, _| comp.state);
let state_b = comp_b.read_with(cx_b, |comp, _| comp.state);
// Assert invariants hold despite execution order
assert!(state_a.is_consistent());
assert!(state_b.is_consistent());
}
```
## Mocking and Isolation
### Network Mocking
Create mock networks for testing distributed features:
```rust
struct MockNetwork {
messages: Arc<Mutex<Vec<NetworkMessage>>>,
}
impl MockNetwork {
fn new() -> Self {
Self {
messages: Arc::new(Mutex::new(Vec::new())),
}
}
fn send(&self, message: NetworkMessage) {
self.messages.lock().unwrap().push(message);
}
fn receive_all(&self) -> Vec<NetworkMessage> {
self.messages.lock().unwrap().drain(..).collect()
}
}
#[gpui::test]
fn test_networked_components(cx: &mut TestAppContext) {
let network = Arc::new(MockNetwork::new());
let sender = cx.new(|_| MessageSender::new(network.clone()));
let receiver = cx.new(|_| MessageReceiver::new(network));
// Send message
sender.update(cx, |sender, _| {
sender.send("Hello");
});
// Receive message
receiver.update(cx, |receiver, _| {
receiver.receive_all();
});
let received = receiver.read_with(cx, |receiver, _| receiver.messages.clone());
assert_eq!(received, vec!["Hello"]);
}
```