Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:28:12 +08:00
commit 1ab103caa3
16 changed files with 6782 additions and 0 deletions

View File

@@ -0,0 +1,603 @@
---
name: gpui-patterns
description: Common GPUI patterns including component composition, state management strategies, event handling, and action dispatching. Use when user needs guidance on GPUI patterns, component design, or state management approaches.
---
# GPUI Patterns
## Metadata
This skill provides comprehensive guidance on common GPUI patterns and best practices for building maintainable, performant applications.
## Instructions
### Component Composition Patterns
#### Basic Component Structure
```rust
use gpui::*;
// View component with state
struct MyView {
state: Model<MyState>,
_subscription: Subscription,
}
impl MyView {
fn new(state: Model<MyState>, cx: &mut ViewContext<Self>) -> Self {
let _subscription = cx.observe(&state, |_, _, cx| cx.notify());
Self { state, _subscription }
}
}
impl Render for MyView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let state = self.state.read(cx);
div()
.flex()
.flex_col()
.child(format!("Value: {}", state.value))
}
}
```
#### Container/Presenter Pattern
**Container** (manages state and logic):
```rust
struct Container {
model: Model<AppState>,
_subscription: Subscription,
}
impl Container {
fn new(model: Model<AppState>, cx: &mut ViewContext<Self>) -> Self {
let _subscription = cx.observe(&model, |_, _, cx| cx.notify());
Self { model, _subscription }
}
}
impl Render for Container {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let state = self.model.read(cx);
// Pass data to presenter
Presenter::new(state.data.clone())
}
}
```
**Presenter** (pure rendering):
```rust
struct Presenter {
data: String,
}
impl Presenter {
fn new(data: String) -> Self {
Self { data }
}
}
impl Render for Presenter {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div().child(self.data.as_str())
}
}
```
#### Compound Components
```rust
// Parent component with shared context
pub struct Tabs {
items: Vec<TabItem>,
active_index: usize,
}
pub struct TabItem {
label: String,
content: Box<dyn Fn() -> AnyElement>,
}
impl Tabs {
pub fn new() -> Self {
Self {
items: Vec::new(),
active_index: 0,
}
}
pub fn add_tab(
mut self,
label: impl Into<String>,
content: impl Fn() -> AnyElement + 'static,
) -> Self {
self.items.push(TabItem {
label: label.into(),
content: Box::new(content),
});
self
}
fn set_active(&mut self, index: usize, cx: &mut ViewContext<Self>) {
self.active_index = index;
cx.notify();
}
}
impl Render for Tabs {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.child(
// Tab headers
div()
.flex()
.children(
self.items.iter().enumerate().map(|(i, item)| {
tab_header(&item.label, i == self.active_index, || {
self.set_active(i, cx)
})
})
)
)
.child(
// Active tab content
(self.items[self.active_index].content)()
)
}
}
```
### State Management Strategies
#### Model-View Pattern
```rust
// Model: Application state
#[derive(Clone)]
struct AppState {
count: usize,
items: Vec<String>,
}
// View: Observes and renders state
struct AppView {
state: Model<AppState>,
_subscription: Subscription,
}
impl AppView {
fn new(state: Model<AppState>, cx: &mut ViewContext<Self>) -> Self {
let _subscription = cx.observe(&state, |_, _, cx| cx.notify());
Self { state, _subscription }
}
fn increment(&mut self, cx: &mut ViewContext<Self>) {
self.state.update(cx, |state, cx| {
state.count += 1;
cx.notify();
});
}
}
```
#### Context-Based State
```rust
// Global state via context
#[derive(Clone)]
struct GlobalSettings {
theme: Theme,
language: String,
}
impl Global for GlobalSettings {}
// Initialize in app
fn init_app(cx: &mut AppContext) {
cx.set_global(GlobalSettings {
theme: Theme::Light,
language: "en".to_string(),
});
}
// Access in components
impl Render for MyView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let settings = cx.global::<GlobalSettings>();
div()
.child(format!("Language: {}", settings.language))
}
}
```
#### Subscription Patterns
**Basic Subscription**:
```rust
struct Observer {
model: Model<Data>,
_subscription: Subscription,
}
impl Observer {
fn new(model: Model<Data>, cx: &mut ViewContext<Self>) -> Self {
let _subscription = cx.observe(&model, |_, _, cx| {
cx.notify(); // Rerender on change
});
Self { model, _subscription }
}
}
```
**Selective Updates**:
```rust
impl Observer {
fn new(model: Model<Data>, cx: &mut ViewContext<Self>) -> Self {
let _subscription = cx.observe(&model, |this, model, cx| {
let data = model.read(cx);
// Only rerender if specific field changed
if data.important_field != this.cached_field {
this.cached_field = data.important_field.clone();
cx.notify();
}
});
Self {
model,
cached_field: String::new(),
_subscription,
}
}
}
```
**Multiple Subscriptions**:
```rust
struct MultiObserver {
model_a: Model<DataA>,
model_b: Model<DataB>,
_subscriptions: Vec<Subscription>,
}
impl MultiObserver {
fn new(
model_a: Model<DataA>,
model_b: Model<DataB>,
cx: &mut ViewContext<Self>,
) -> Self {
let mut subscriptions = Vec::new();
subscriptions.push(cx.observe(&model_a, |_, _, cx| cx.notify()));
subscriptions.push(cx.observe(&model_b, |_, _, cx| cx.notify()));
Self {
model_a,
model_b,
_subscriptions: subscriptions,
}
}
}
```
### Event Handling Patterns
#### Click Events
```rust
div()
.on_click(cx.listener(|this, event: &ClickEvent, cx| {
// Handle click
this.handle_click(cx);
}))
.child("Click me")
```
#### Keyboard Events
```rust
div()
.on_key_down(cx.listener(|this, event: &KeyDownEvent, cx| {
match event.key.as_str() {
"Enter" => this.submit(cx),
"Escape" => this.cancel(cx),
_ => {}
}
}))
```
#### Event Propagation
```rust
// Stop propagation
div()
.on_click(|event, cx| {
event.stop_propagation();
// Handle click
})
// Prevent default
div()
.on_key_down(|event, cx| {
if event.key == "Tab" {
event.prevent_default();
// Custom tab handling
}
})
```
#### Mouse Events
```rust
div()
.on_mouse_down(cx.listener(|this, event, cx| {
this.mouse_down_position = Some(event.position);
}))
.on_mouse_move(cx.listener(|this, event, cx| {
if let Some(start) = this.mouse_down_position {
let delta = event.position - start;
this.handle_drag(delta, cx);
}
}))
.on_mouse_up(cx.listener(|this, event, cx| {
this.mouse_down_position = None;
}))
```
### Action System
#### Define Actions
```rust
use gpui::*;
actions!(app, [
Increment,
Decrement,
Reset,
SetValue
]);
// Action with data
#[derive(Clone, PartialEq)]
pub struct SetValue {
pub value: i32,
}
impl_actions!(app, [SetValue]);
```
#### Register Action Handlers
```rust
impl Counter {
fn register_actions(&mut self, cx: &mut ViewContext<Self>) {
cx.on_action(cx.listener(|this, _: &Increment, cx| {
this.model.update(cx, |state, cx| {
state.count += 1;
cx.notify();
});
}));
cx.on_action(cx.listener(|this, _: &Decrement, cx| {
this.model.update(cx, |state, cx| {
state.count = state.count.saturating_sub(1);
cx.notify();
});
}));
cx.on_action(cx.listener(|this, action: &SetValue, cx| {
this.model.update(cx, |state, cx| {
state.count = action.value;
cx.notify();
});
}));
}
}
```
#### Dispatch Actions
```rust
// From within component
fn handle_button_click(&mut self, cx: &mut ViewContext<Self>) {
cx.dispatch_action(Increment);
}
// With data
fn set_specific_value(&mut self, value: i32, cx: &mut ViewContext<Self>) {
cx.dispatch_action(SetValue { value });
}
// Global action dispatch
cx.dispatch_action_on_window(Reset, window_id);
```
#### Keybindings
```rust
// Register global keybindings
fn register_keybindings(cx: &mut AppContext) {
cx.bind_keys([
KeyBinding::new("cmd-+", Increment, None),
KeyBinding::new("cmd--", Decrement, None),
KeyBinding::new("cmd-0", Reset, None),
]);
}
```
### Element Composition
#### Builder Pattern
```rust
fn card(title: &str, content: impl IntoElement) -> impl IntoElement {
div()
.flex()
.flex_col()
.bg(white())
.border_1()
.rounded_lg()
.shadow_sm()
.p_6()
.child(
div()
.text_lg()
.font_semibold()
.mb_4()
.child(title)
)
.child(content)
}
```
#### Conditional Rendering
```rust
div()
.when(condition, |this| {
this.bg(blue_500())
})
.when_some(optional_value, |this, value| {
this.child(format!("Value: {}", value))
})
.map(|this| {
if complex_condition {
this.border_1()
} else {
this.border_2()
}
})
```
#### Dynamic Children
```rust
div()
.children(
items.iter().map(|item| {
div().child(item.name.as_str())
})
)
```
### View Lifecycle
#### Initialization
```rust
impl MyView {
fn new(cx: &mut ViewContext<Self>) -> Self {
// Initialize state
let model = cx.new_model(|_| MyState::default());
// Set up subscriptions
let subscription = cx.observe(&model, |_, _, cx| cx.notify());
// Spawn async tasks
cx.spawn(|this, mut cx| async move {
// Async initialization
}).detach();
Self {
model,
_subscription: subscription,
}
}
}
```
#### Update Notifications
```rust
impl MyView {
fn update_state(&mut self, new_data: Data, cx: &mut ViewContext<Self>) {
self.model.update(cx, |state, cx| {
state.data = new_data;
cx.notify(); // Trigger rerender
});
}
}
```
#### Cleanup
```rust
impl Drop for MyView {
fn drop(&mut self) {
// Manual cleanup if needed
// Subscriptions are automatically dropped
}
}
```
### Reactive Patterns
#### Derived State
```rust
impl Render for MyView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let state = self.model.read(cx);
// Compute derived values
let total = state.items.iter().map(|i| i.value).sum::<i32>();
let average = total / state.items.len() as i32;
div()
.child(format!("Total: {}", total))
.child(format!("Average: {}", average))
}
}
```
#### Async Updates
```rust
impl MyView {
fn load_data(&mut self, cx: &mut ViewContext<Self>) {
let model = self.model.clone();
cx.spawn(|_, mut cx| async move {
let data = fetch_data().await?;
cx.update_model(&model, |state, cx| {
state.data = data;
cx.notify();
})?;
Ok::<_, anyhow::Error>(())
}).detach();
}
}
```
## Resources
### Official Documentation
- GPUI GitHub: https://github.com/zed-industries/zed/tree/main/crates/gpui
- Zed Editor Source: Real-world GPUI examples
### Common Patterns Reference
- Model-View: State management pattern
- Container-Presenter: Separation of concerns
- Compound Components: Related components working together
- Action System: Command pattern for user interactions
- Subscriptions: Observer pattern for reactive updates
### Best Practices
- Store subscriptions to prevent cleanup
- Use `cx.notify()` sparingly
- Prefer composition over inheritance
- Keep render methods pure
- Handle errors gracefully
- Document component APIs
- Test component behavior

View File

@@ -0,0 +1,603 @@
---
name: gpui-performance
description: Performance optimization techniques for GPUI including rendering optimization, layout performance, memory management, and profiling strategies. Use when user needs to optimize GPUI application performance or debug performance issues.
---
# GPUI Performance Optimization
## Metadata
This skill provides comprehensive guidance on optimizing GPUI applications for rendering performance, memory efficiency, and overall runtime speed.
## Instructions
### Rendering Optimization
#### Understanding the Render Cycle
```
State Change → cx.notify() → Render → Layout → Paint → Display
```
**Key Points**:
- Only call `cx.notify()` when state actually changes
- Minimize work in `render()` method
- Cache expensive computations
- Reduce element count and nesting
#### Avoiding Unnecessary Renders
```rust
// BAD: Renders on every frame
impl MyComponent {
fn start_animation(&mut self, cx: &mut ViewContext<Self>) {
cx.spawn(|this, mut cx| async move {
loop {
cx.update(|_, cx| cx.notify()).ok(); // Forces rerender!
Timer::after(Duration::from_millis(16)).await;
}
}).detach();
}
}
// GOOD: Only render when state changes
impl MyComponent {
fn update_value(&mut self, new_value: i32, cx: &mut ViewContext<Self>) {
if self.value != new_value {
self.value = new_value;
cx.notify(); // Only notify on actual change
}
}
}
```
#### Optimize Subscription Updates
```rust
// BAD: Always rerenders on model change
let _subscription = cx.observe(&model, |_, _, cx| {
cx.notify(); // Rerenders even if nothing relevant changed
});
// GOOD: Selective updates
let _subscription = cx.observe(&model, |this, model, cx| {
let data = model.read(cx);
// Only rerender if relevant field changed
if data.relevant_field != this.cached_field {
this.cached_field = data.relevant_field.clone();
cx.notify();
}
});
```
#### Memoization Pattern
```rust
use std::cell::RefCell;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
struct MemoizedComponent {
model: Model<Data>,
cached_result: RefCell<Option<(u64, String)>>, // (hash, result)
}
impl MemoizedComponent {
fn expensive_computation(&self, cx: &ViewContext<Self>) -> String {
let data = self.model.read(cx);
// Calculate hash of input
let mut hasher = DefaultHasher::new();
data.relevant_fields.hash(&mut hasher);
let hash = hasher.finish();
// Return cached if unchanged
if let Some((cached_hash, cached_result)) = &*self.cached_result.borrow() {
if *cached_hash == hash {
return cached_result.clone();
}
}
// Compute and cache
let result = perform_expensive_computation(&data);
*self.cached_result.borrow_mut() = Some((hash, result.clone()));
result
}
}
```
### Layout Performance
#### Minimize Layout Complexity
```rust
// BAD: Deep nesting
div()
.flex()
.child(
div()
.flex()
.child(
div()
.flex()
.child(
div().child("Content")
)
)
)
// GOOD: Flat structure
div()
.flex()
.flex_col()
.gap_4()
.child("Header")
.child("Content")
.child("Footer")
```
#### Use Fixed Sizing When Possible
```rust
// BETTER: Fixed sizes (no layout calculation)
div()
.w(px(200.))
.h(px(100.))
.child("Fixed size")
// SLOWER: Dynamic sizing (requires layout calculation)
div()
.w_full()
.h_full()
.child("Dynamic size")
```
#### Avoid Layout Thrashing
```rust
// BAD: Reading layout during render
impl Render for BadComponent {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let width = cx.window_bounds().get_bounds().size.width;
// Using width immediately causes layout thrashing
div().w(width)
}
}
// GOOD: Cache layout-dependent values
struct GoodComponent {
cached_width: Pixels,
}
impl GoodComponent {
fn on_window_resize(&mut self, cx: &mut ViewContext<Self>) {
let width = cx.window_bounds().get_bounds().size.width;
if self.cached_width != width {
self.cached_width = width;
cx.notify();
}
}
}
```
#### Virtual Scrolling for Long Lists
```rust
struct VirtualList {
items: Vec<String>,
scroll_offset: f32,
viewport_height: f32,
item_height: f32,
}
impl Render for VirtualList {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
// Calculate visible range
let start_index = (self.scroll_offset / self.item_height).floor() as usize;
let visible_count = (self.viewport_height / self.item_height).ceil() as usize;
let end_index = (start_index + visible_count).min(self.items.len());
// Only render visible items
div()
.h(px(self.viewport_height))
.overflow_y_scroll()
.on_scroll(cx.listener(|this, event, cx| {
this.scroll_offset = event.scroll_offset.y;
cx.notify();
}))
.child(
div()
.h(px(self.items.len() as f32 * self.item_height))
.child(
div()
.absolute()
.top(px(start_index as f32 * self.item_height))
.children(
self.items[start_index..end_index]
.iter()
.map(|item| {
div()
.h(px(self.item_height))
.child(item.as_str())
})
)
)
)
}
}
```
### Memory Management
#### Preventing Memory Leaks
```rust
// LEAK: Subscription not stored
impl BadView {
fn new(model: Model<Data>, cx: &mut ViewContext<Self>) -> Self {
cx.observe(&model, |_, _, cx| cx.notify()); // Leak!
Self { model }
}
}
// CORRECT: Store subscription
struct GoodView {
model: Model<Data>,
_subscription: Subscription, // Cleaned up on Drop
}
impl GoodView {
fn new(model: Model<Data>, cx: &mut ViewContext<Self>) -> Self {
let _subscription = cx.observe(&model, |_, _, cx| cx.notify());
Self { model, _subscription }
}
}
```
#### Avoid Circular References
```rust
// BAD: Circular reference
struct CircularRef {
self_view: Option<View<Self>>, // Circular!
}
// GOOD: Use weak references or redesign
struct NoCycle {
other_view: View<OtherView>, // No cycle
}
```
#### Bounded Collections
```rust
use std::collections::VecDeque;
const MAX_HISTORY: usize = 100;
struct BoundedHistory {
items: VecDeque<Item>,
}
impl BoundedHistory {
fn add_item(&mut self, item: Item) {
self.items.push_back(item);
// Maintain size limit
while self.items.len() > MAX_HISTORY {
self.items.pop_front();
}
}
}
```
#### Reuse Allocations
```rust
struct BufferedComponent {
buffer: String, // Reused across operations
}
impl BufferedComponent {
fn format_data(&mut self, data: &[Item]) -> &str {
self.buffer.clear(); // Reuse allocation
for item in data {
use std::fmt::Write;
write!(&mut self.buffer, "{}\n", item.name).ok();
}
&self.buffer
}
}
```
### Profiling Strategies
#### CPU Profiling with cargo-flamegraph
```bash
# Install
cargo install flamegraph
# Profile application
cargo flamegraph --bin your-app
# With specific features
cargo flamegraph --bin your-app --features profiling
# Opens flamegraph.svg showing CPU time distribution
```
#### Memory Profiling
```bash
# valgrind (Linux)
valgrind --tool=massif --massif-out-file=massif.out ./target/release/your-app
ms_print massif.out
# heaptrack (Linux)
heaptrack ./target/release/your-app
heaptrack_gui heaptrack.your-app.*.gz
# Instruments (macOS)
instruments -t "Allocations" ./target/release/your-app
```
#### Custom Performance Monitoring
```rust
use std::time::Instant;
struct PerformanceMonitor {
frame_times: VecDeque<Duration>,
max_samples: usize,
}
impl PerformanceMonitor {
fn new() -> Self {
Self {
frame_times: VecDeque::with_capacity(100),
max_samples: 100,
}
}
fn record_frame(&mut self, duration: Duration) {
self.frame_times.push_back(duration);
if self.frame_times.len() > self.max_samples {
self.frame_times.pop_front();
}
// Warn if frame is slow (> 16ms for 60fps)
if duration.as_millis() > 16 {
eprintln!("⚠️ Slow frame: {}ms", duration.as_millis());
}
}
fn average_fps(&self) -> f64 {
if self.frame_times.is_empty() {
return 0.0;
}
let total: Duration = self.frame_times.iter().sum();
let avg = total / self.frame_times.len() as u32;
1000.0 / avg.as_millis() as f64
}
fn percentile(&self, p: f64) -> Duration {
let mut sorted: Vec<_> = self.frame_times.iter().copied().collect();
sorted.sort();
let index = (sorted.len() as f64 * p) as usize;
sorted[index.min(sorted.len() - 1)]
}
}
// Usage in component
impl MyView {
fn measure_render<F>(&mut self, f: F, cx: &mut ViewContext<Self>)
where
F: FnOnce(&mut Self, &mut ViewContext<Self>)
{
let start = Instant::now();
f(self, cx);
let elapsed = start.elapsed();
self.perf_monitor.record_frame(elapsed);
// Log stats periodically
if self.frame_count % 60 == 0 {
println!(
"Avg FPS: {:.1}, p95: {}ms, p99: {}ms",
self.perf_monitor.average_fps(),
self.perf_monitor.percentile(0.95).as_millis(),
self.perf_monitor.percentile(0.99).as_millis(),
);
}
}
}
```
#### Benchmark with Criterion
```rust
// benches/component_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId};
fn render_benchmark(c: &mut Criterion) {
let mut group = c.benchmark_group("rendering");
for size in [10, 100, 1000].iter() {
group.bench_with_input(
BenchmarkId::from_parameter(size),
size,
|b, &size| {
b.iter(|| {
App::test(|cx| {
let items = vec![Item::default(); size];
let view = cx.new_view(|cx| {
ListView::new(items, cx)
});
view.update(cx, |view, cx| {
black_box(view.render(cx));
});
});
});
}
);
}
group.finish();
}
criterion_group!(benches, render_benchmark);
criterion_main!(benches);
```
### Batching Updates
```rust
// BAD: Multiple individual updates
for item in items {
self.model.update(cx, |model, cx| {
model.add_item(item); // Triggers rerender each time!
cx.notify();
});
}
// GOOD: Batch into single update
self.model.update(cx, |model, cx| {
for item in items {
model.add_item(item);
}
cx.notify(); // Single rerender
});
```
### Async Rendering Optimization
```rust
struct AsyncView {
loading_state: Model<LoadingState>,
}
impl AsyncView {
fn load_data(&mut self, cx: &mut ViewContext<Self>) {
let loading_state = self.loading_state.clone();
// Show loading immediately
self.loading_state.update(cx, |state, cx| {
*state = LoadingState::Loading;
cx.notify();
});
// Load asynchronously
cx.spawn(|_, mut cx| async move {
// Fetch data
let data = fetch_data().await?;
// Update state once
cx.update_model(&loading_state, |state, cx| {
*state = LoadingState::Loaded(data);
cx.notify();
})?;
Ok::<_, anyhow::Error>(())
}).detach();
}
}
```
### Caching Strategies
#### Result Caching
```rust
use std::collections::HashMap;
struct CachedRenderer {
cache: RefCell<HashMap<String, CachedElement>>,
}
impl CachedRenderer {
fn render_cached(
&self,
key: String,
render_fn: impl FnOnce() -> AnyElement,
) -> AnyElement {
let mut cache = self.cache.borrow_mut();
cache.entry(key)
.or_insert_with(|| CachedElement::new(render_fn()))
.element
.clone()
}
fn invalidate(&self, key: &str) {
self.cache.borrow_mut().remove(key);
}
}
```
## Resources
### Performance Targets
**Rendering**:
- Target: 60 FPS (16.67ms per frame)
- Render + Layout: ~10ms
- Paint: ~6ms
- Warning: Any frame > 16ms
**Memory**:
- Monitor heap growth
- Warning: Steady increase (leak)
- Target: Stable after initialization
**Startup**:
- Window display: < 100ms
- Fully interactive: < 500ms
### Profiling Tools
**CPU Profiling**:
- cargo-flamegraph: Visualize CPU time
- perf (Linux): System-level profiling
- Instruments (macOS): Apple's profiler
**Memory Profiling**:
- valgrind/massif: Memory usage tracking
- heaptrack: Heap allocation tracking
- Instruments: Memory allocations
**Benchmarking**:
- criterion: Statistical benchmarking
- cargo bench: Built-in benchmarks
- hyperfine: Command-line tool benchmarking
### Best Practices
1. **Measure First**: Profile before optimizing
2. **Minimize Renders**: Only `cx.notify()` when necessary
3. **Cache Results**: Memoize expensive computations
4. **Batch Updates**: Group state changes
5. **Virtual Scrolling**: For long lists
6. **Flat Layouts**: Avoid deep nesting
7. **Fixed Sizing**: When possible
8. **Monitor Memory**: Watch for leaks
9. **Async Loading**: Don't block UI
10. **Test Performance**: Include benchmarks
### Common Bottlenecks
- Subscription in render (memory leak)
- Expensive computation in render
- Deep component nesting
- Unnecessary rerenders
- Layout thrashing
- Large lists without virtualization
- Memory leaks from circular refs
- Unbounded collections

View File

@@ -0,0 +1,617 @@
---
name: gpui-styling
description: GPUI styling system including theme design, responsive layouts, visual design patterns, and style composition. Use when user needs help with styling, theming, or visual design in GPUI.
---
# GPUI Styling
## Metadata
This skill provides comprehensive guidance on GPUI's styling system, theme management, and visual design patterns for creating beautiful, consistent user interfaces.
## Instructions
### Styling API Fundamentals
#### Basic Styling
```rust
use gpui::*;
div()
// Colors
.bg(rgb(0x2563eb)) // Background
.text_color(white()) // Text color
.border_color(rgb(0xe5e7eb)) // Border color
// Spacing
.p_4() // Padding: 1rem
.px_6() // Padding horizontal
.py_2() // Padding vertical
.m_4() // Margin
.gap_3() // Gap between children
// Sizing
.w_64() // Width: 16rem
.h_32() // Height: 8rem
.w_full() // Width: 100%
.h_full() // Height: 100%
// Borders
.border_1() // Border: 1px
.rounded_lg() // Border radius: large
```
#### Color Types
```rust
// RGB from hex
let blue = rgb(0x2563eb);
// RGBA with alpha
let transparent_blue = rgba(0x2563eb, 0.5);
// HSLA (hue, saturation, lightness, alpha)
let hsla_color = hsla(0.6, 0.8, 0.5, 1.0);
// Named colors
let white = white();
let black = black();
```
#### Layout with Flexbox
```rust
div()
.flex() // Enable flexbox
.flex_row() // Horizontal layout
.flex_col() // Vertical layout
.items_center() // Align items center
.justify_between() // Space between
.gap_4() // Gap between items
.child(/* ... */)
.child(/* ... */)
```
### Theme System
#### Basic Theme Structure
```rust
use gpui::*;
#[derive(Clone)]
pub struct AppTheme {
pub colors: ThemeColors,
pub typography: Typography,
pub spacing: Spacing,
pub shadows: Shadows,
}
#[derive(Clone)]
pub struct ThemeColors {
// Base colors
pub background: Hsla,
pub foreground: Hsla,
// UI colors
pub primary: Hsla,
pub primary_foreground: Hsla,
pub primary_hover: Hsla,
pub secondary: Hsla,
pub secondary_foreground: Hsla,
pub secondary_hover: Hsla,
pub accent: Hsla,
pub accent_foreground: Hsla,
pub destructive: Hsla,
pub destructive_foreground: Hsla,
// Neutral colors
pub muted: Hsla,
pub muted_foreground: Hsla,
pub border: Hsla,
pub input: Hsla,
pub ring: Hsla,
}
#[derive(Clone)]
pub struct Typography {
pub font_sans: Vec<String>,
pub font_mono: Vec<String>,
pub text_xs: Pixels,
pub text_sm: Pixels,
pub text_base: Pixels,
pub text_lg: Pixels,
pub text_xl: Pixels,
pub text_2xl: Pixels,
}
#[derive(Clone)]
pub struct Spacing {
pub xs: Pixels,
pub sm: Pixels,
pub md: Pixels,
pub lg: Pixels,
pub xl: Pixels,
}
```
#### Light Theme Implementation
```rust
impl AppTheme {
pub fn light() -> Self {
Self {
colors: ThemeColors {
background: rgb(0xffffff),
foreground: rgb(0x0a0a0a),
primary: rgb(0x2563eb),
primary_foreground: rgb(0xffffff),
primary_hover: rgb(0x1d4ed8),
secondary: rgb(0xf1f5f9),
secondary_foreground: rgb(0x0f172a),
secondary_hover: rgb(0xe2e8f0),
accent: rgb(0xf1f5f9),
accent_foreground: rgb(0x0f172a),
destructive: rgb(0xef4444),
destructive_foreground: rgb(0xffffff),
muted: rgb(0xf1f5f9),
muted_foreground: rgb(0x64748b),
border: rgb(0xe2e8f0),
input: rgb(0xe2e8f0),
ring: rgb(0x2563eb),
},
typography: Typography {
font_sans: vec![
"Inter".to_string(),
"system-ui".to_string(),
"sans-serif".to_string(),
],
font_mono: vec![
"JetBrains Mono".to_string(),
"monospace".to_string(),
],
text_xs: px(12.0),
text_sm: px(14.0),
text_base: px(16.0),
text_lg: px(18.0),
text_xl: px(20.0),
text_2xl: px(24.0),
},
spacing: Spacing {
xs: px(4.0),
sm: px(8.0),
md: px(16.0),
lg: px(24.0),
xl: px(32.0),
},
shadows: Shadows {
sm: Shadow::new(px(1.0), rgba(0x000000, 0.05)),
md: Shadow::new(px(4.0), rgba(0x000000, 0.1)),
lg: Shadow::new(px(8.0), rgba(0x000000, 0.15)),
},
}
}
}
```
#### Dark Theme Implementation
```rust
impl AppTheme {
pub fn dark() -> Self {
Self {
colors: ThemeColors {
background: rgb(0x0a0a0a),
foreground: rgb(0xfafafa),
primary: rgb(0x3b82f6),
primary_foreground: rgb(0xffffff),
primary_hover: rgb(0x2563eb),
secondary: rgb(0x1e293b),
secondary_foreground: rgb(0xf1f5f9),
secondary_hover: rgb(0x334155),
accent: rgb(0x1e293b),
accent_foreground: rgb(0xf1f5f9),
destructive: rgb(0xef4444),
destructive_foreground: rgb(0xffffff),
muted: rgb(0x1e293b),
muted_foreground: rgb(0x94a3b8),
border: rgb(0x334155),
input: rgb(0x334155),
ring: rgb(0x3b82f6),
},
typography: Typography {
font_sans: vec![
"Inter".to_string(),
"system-ui".to_string(),
"sans-serif".to_string(),
],
font_mono: vec![
"JetBrains Mono".to_string(),
"monospace".to_string(),
],
text_xs: px(12.0),
text_sm: px(14.0),
text_base: px(16.0),
text_lg: px(18.0),
text_xl: px(20.0),
text_2xl: px(24.0),
},
spacing: Spacing {
xs: px(4.0),
sm: px(8.0),
md: px(16.0),
lg: px(24.0),
xl: px(32.0),
},
shadows: Shadows {
sm: Shadow::new(px(1.0), rgba(0x000000, 0.2)),
md: Shadow::new(px(4.0), rgba(0x000000, 0.3)),
lg: Shadow::new(px(8.0), rgba(0x000000, 0.4)),
},
}
}
}
```
#### Using Themes in Components
```rust
impl Render for ThemedComponent {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let theme = cx.global::<AppTheme>();
div()
.bg(theme.colors.background)
.text_color(theme.colors.foreground)
.p(theme.spacing.md)
.border_1()
.border_color(theme.colors.border)
.child("Themed content")
}
}
```
#### Theme Switching
```rust
pub fn toggle_theme(cx: &mut AppContext) {
let current = cx.global::<AppTheme>().clone();
let new_theme = match current.mode {
ThemeMode::Light => AppTheme::dark(),
ThemeMode::Dark => AppTheme::light(),
};
cx.set_global(new_theme);
cx.refresh();
}
```
### Responsive Design
#### Window Size Detection
```rust
impl Render for ResponsiveView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let window_size = cx.window_bounds().get_bounds().size;
let is_mobile = window_size.width < px(768.0);
let is_tablet = window_size.width >= px(768.0) && window_size.width < px(1024.0);
let is_desktop = window_size.width >= px(1024.0);
div()
.flex()
.when(is_mobile, |this| {
this.flex_col().gap_2()
})
.when(is_desktop, |this| {
this.flex_row().gap_6()
})
.child(sidebar())
.child(main_content())
}
}
```
#### Breakpoint-Based Styling
```rust
pub struct Breakpoints;
impl Breakpoints {
pub const SM: f32 = 640.0;
pub const MD: f32 = 768.0;
pub const LG: f32 = 1024.0;
pub const XL: f32 = 1280.0;
pub const XXL: f32 = 1536.0;
}
fn responsive_grid(width: Pixels) -> impl IntoElement {
div()
.grid()
.when(width.0 < Breakpoints::SM, |this| this.grid_cols_1())
.when(width.0 >= Breakpoints::SM && width.0 < Breakpoints::LG, |this| {
this.grid_cols_2()
})
.when(width.0 >= Breakpoints::LG, |this| this.grid_cols_3())
.gap_4()
}
```
### Visual Design Patterns
#### Card Component
```rust
pub fn card(
title: impl Into<String>,
description: impl Into<String>,
content: impl IntoElement,
) -> impl IntoElement {
let theme = cx.global::<AppTheme>();
div()
.bg(theme.colors.background)
.border_1()
.border_color(theme.colors.border)
.rounded_lg()
.shadow_sm()
.overflow_hidden()
.child(
div()
.p_6()
.border_b_1()
.border_color(theme.colors.border)
.child(
div()
.text_lg()
.font_semibold()
.child(title.into())
)
.child(
div()
.text_sm()
.text_color(theme.colors.muted_foreground)
.child(description.into())
)
)
.child(
div()
.p_6()
.child(content)
)
}
```
#### Button Variants
```rust
pub enum ButtonVariant {
Primary,
Secondary,
Outline,
Ghost,
Destructive,
}
pub fn button(
label: &str,
variant: ButtonVariant,
) -> impl IntoElement {
let theme = cx.global::<AppTheme>();
let (bg, fg, hover_bg) = match variant {
ButtonVariant::Primary => (
theme.colors.primary,
theme.colors.primary_foreground,
theme.colors.primary_hover,
),
ButtonVariant::Secondary => (
theme.colors.secondary,
theme.colors.secondary_foreground,
theme.colors.secondary_hover,
),
ButtonVariant::Outline => (
hsla(0.0, 0.0, 0.0, 0.0),
theme.colors.foreground,
theme.colors.accent,
),
ButtonVariant::Ghost => (
hsla(0.0, 0.0, 0.0, 0.0),
theme.colors.foreground,
theme.colors.accent,
),
ButtonVariant::Destructive => (
theme.colors.destructive,
theme.colors.destructive_foreground,
theme.colors.destructive,
),
};
div()
.px_4()
.py_2()
.bg(bg)
.text_color(fg)
.rounded_md()
.font_medium()
.cursor_pointer()
.when(matches!(variant, ButtonVariant::Outline), |this| {
this.border_1().border_color(theme.colors.border)
})
.hover(|this| this.bg(hover_bg))
.transition_colors()
.duration_150()
.child(label)
}
```
#### Input Fields
```rust
pub fn text_input(
value: &str,
placeholder: &str,
) -> impl IntoElement {
let theme = cx.global::<AppTheme>();
div()
.flex()
.items_center()
.w_full()
.px_3()
.py_2()
.bg(theme.colors.background)
.border_1()
.border_color(theme.colors.input)
.rounded_md()
.text_color(theme.colors.foreground)
.focus(|this| {
this.border_color(theme.colors.ring)
.ring_2()
.ring_color(rgba(theme.colors.ring, 0.2))
})
.child(
input()
.w_full()
.bg(hsla(0.0, 0.0, 0.0, 0.0))
.placeholder(placeholder)
.value(value)
)
}
```
### Style Composition
#### Reusable Style Functions
```rust
pub fn focus_ring(theme: &AppTheme) -> StyleRefinement {
StyleRefinement::default()
.ring_2()
.ring_color(rgba(theme.colors.ring, 0.2))
.border_color(theme.colors.ring)
}
pub fn shadow_sm(theme: &AppTheme) -> StyleRefinement {
StyleRefinement::default()
.shadow(theme.shadows.sm)
}
// Usage
div()
.apply(focus_ring(&theme))
.apply(shadow_sm(&theme))
.child("Styled element")
```
#### Conditional Styles
```rust
fn dynamic_button(
label: &str,
is_loading: bool,
is_disabled: bool,
) -> impl IntoElement {
let theme = cx.global::<AppTheme>();
div()
.px_4()
.py_2()
.bg(theme.colors.primary)
.text_color(theme.colors.primary_foreground)
.rounded_md()
.when(is_disabled || is_loading, |this| {
this.opacity(0.5).cursor_not_allowed()
})
.when(!is_disabled && !is_loading, |this| {
this.cursor_pointer()
.hover(|this| this.bg(theme.colors.primary_hover))
})
.child(
if is_loading {
"Loading..."
} else {
label
}
)
}
```
### Animation and Transitions
#### Hover Transitions
```rust
div()
.transition_all() // Transition all properties
.duration_200() // 200ms duration
.bg(blue_500())
.hover(|this| {
this.bg(blue_600())
.scale_105() // Scale to 105%
})
.child("Hover me")
```
#### Transform Animations
```rust
div()
.transition_transform()
.duration_300()
.ease_in_out()
.hover(|this| {
this.rotate(5.0) // Rotate 5 degrees
.translate_y(px(-2.0)) // Move up 2px
})
.child("Animated element")
```
## Resources
### Color Systems
- Use HSL for color manipulation
- Maintain consistent color contrast ratios
- Define semantic color names (primary, secondary, etc.)
- Support both light and dark themes
### Typography Scale
- Base: 16px (1rem)
- Scale: 1.125 (Major Second) or 1.2 (Minor Third)
- Sizes: xs, sm, base, lg, xl, 2xl, etc.
- Weights: normal, medium, semibold, bold
### Spacing Scale
- Base unit: 4px or 8px
- Multipliers: 0.5, 1, 2, 3, 4, 6, 8, 12, 16, 24, 32
- Consistent throughout application
- Used for padding, margin, gap
### Best Practices
- Define theme at app level
- Use semantic color names
- Implement both light and dark themes
- Support responsive design
- Maintain consistent spacing
- Use transitions for smooth interactions
- Ensure accessibility (contrast, focus indicators)
- Document theme structure

View File

@@ -0,0 +1,606 @@
---
name: rust-ui-architecture
description: Architecture patterns for Rust UI applications including GPUI-specific patterns, code organization, modularity, and scalability. Use when user needs guidance on application architecture, code organization, or scaling UI applications.
---
# Rust UI Architecture
## Metadata
This skill provides comprehensive guidance on architecting scalable, maintainable Rust UI applications using GPUI, covering project structure, design patterns, and best practices.
## Instructions
### Application Structure
#### Recommended Project Layout
```
my-gpui-app/
├── Cargo.toml
├── src/
│ ├── main.rs # Application entry point
│ ├── app.rs # Main application struct
│ ├── ui/ # UI layer
│ │ ├── mod.rs
│ │ ├── views/ # High-level views
│ │ │ ├── mod.rs
│ │ │ ├── main_view.rs
│ │ │ ├── sidebar.rs
│ │ │ └── editor.rs
│ │ ├── components/ # Reusable components
│ │ │ ├── mod.rs
│ │ │ ├── button.rs
│ │ │ ├── input.rs
│ │ │ └── modal.rs
│ │ └── theme.rs # Theme definitions
│ ├── models/ # Application state
│ │ ├── mod.rs
│ │ ├── document.rs
│ │ ├── project.rs
│ │ └── settings.rs
│ ├── services/ # External integrations
│ │ ├── mod.rs
│ │ ├── file_service.rs
│ │ └── api_client.rs
│ ├── domain/ # Core business logic
│ │ ├── mod.rs
│ │ └── operations.rs
│ └── utils/ # Utilities
│ ├── mod.rs
│ └── helpers.rs
├── examples/ # Example applications
│ └── basic.rs
└── tests/ # Integration tests
├── integration/
└── ui/
```
### Layer Separation
#### Four-Layer Architecture
```
┌─────────────────────────────────┐
│ UI Layer (Views) │ - GPUI views and components
│ │ - User interactions
│ │ - Render logic
├─────────────────────────────────┤
│ Application Layer (Models) │ - Application state (Model<T>)
│ │ - State coordination
│ │ - Business logic orchestration
├─────────────────────────────────┤
│ Service Layer (Services) │ - File I/O
│ │ - Network requests
│ │ - External APIs
├─────────────────────────────────┤
│ Domain Layer (Core) │ - Pure business logic
│ │ - Domain types
│ │ - No dependencies on UI/GPUI
└─────────────────────────────────┘
```
#### Example Implementation
```rust
// Domain Layer (pure logic)
pub mod domain {
#[derive(Clone, Debug)]
pub struct Document {
pub id: DocumentId,
pub content: String,
pub language: Language,
}
impl Document {
pub fn word_count(&self) -> usize {
self.content.split_whitespace().count()
}
pub fn is_empty(&self) -> bool {
self.content.trim().is_empty()
}
}
}
// Service Layer (external integration)
pub mod services {
use super::domain::*;
pub trait FileService: Send + Sync {
fn read(&self, path: &Path) -> Result<String>;
fn write(&self, path: &Path, content: &str) -> Result<()>;
}
pub struct RealFileService;
impl FileService for RealFileService {
fn read(&self, path: &Path) -> Result<String> {
std::fs::read_to_string(path)
.map_err(|e| anyhow::anyhow!("Failed to read: {}", e))
}
fn write(&self, path: &Path, content: &str) -> Result<()> {
std::fs::write(path, content)
.map_err(|e| anyhow::anyhow!("Failed to write: {}", e))
}
}
}
// Application Layer (state management)
pub mod models {
use super::domain::*;
use super::services::*;
pub struct DocumentModel {
document: Document,
file_service: Arc<dyn FileService>,
is_modified: bool,
}
impl DocumentModel {
pub fn new(document: Document, file_service: Arc<dyn FileService>) -> Self {
Self {
document,
file_service,
is_modified: false,
}
}
pub fn update_content(&mut self, content: String) {
self.document.content = content;
self.is_modified = true;
}
pub async fn save(&mut self) -> Result<()> {
self.file_service.write(&self.document.path, &self.document.content)?;
self.is_modified = false;
Ok(())
}
}
}
// UI Layer (views)
pub mod ui {
use gpui::*;
use super::models::*;
pub struct DocumentView {
model: Model<DocumentModel>,
_subscription: Subscription,
}
impl DocumentView {
pub fn new(model: Model<DocumentModel>, cx: &mut ViewContext<Self>) -> Self {
let _subscription = cx.observe(&model, |_, _, cx| cx.notify());
Self { model, _subscription }
}
}
impl Render for DocumentView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let model = self.model.read(cx);
div()
.child(format!("Words: {}", model.document.word_count()))
.when(model.is_modified, |this| {
this.child("(modified)")
})
}
}
}
```
### Component Hierarchies
#### Container-Presenter Pattern
```rust
// Container: Manages state and logic
pub struct EditorContainer {
document: Model<DocumentModel>,
_subscription: Subscription,
}
impl EditorContainer {
pub fn new(document: Model<DocumentModel>, cx: &mut ViewContext<Self>) -> Self {
let _subscription = cx.observe(&document, |_, _, cx| cx.notify());
Self { document, _subscription }
}
fn handle_save(&mut self, cx: &mut ViewContext<Self>) {
let document = self.document.clone();
cx.spawn(|_, mut cx| async move {
cx.update_model(&document, |doc, _| {
doc.save().await
}).await?;
Ok::<_, anyhow::Error>(())
}).detach();
}
}
impl Render for EditorContainer {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let doc = self.document.read(cx);
EditorPresenter::new(
doc.document.content.clone(),
doc.is_modified,
cx.listener(|this, content, cx| {
this.document.update(cx, |doc, _| {
doc.update_content(content);
});
}),
)
}
}
// Presenter: Pure rendering
pub struct EditorPresenter {
content: String,
is_modified: bool,
on_change: Box<dyn Fn(String, &mut WindowContext)>,
}
impl EditorPresenter {
pub fn new(
content: String,
is_modified: bool,
on_change: impl Fn(String, &mut WindowContext) + 'static,
) -> Self {
Self {
content,
is_modified,
on_change: Box::new(on_change),
}
}
}
impl Render for EditorPresenter {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.child(
textarea()
.value(&self.content)
.on_input(|value, cx| {
(self.on_change)(value, cx);
})
)
.when(self.is_modified, |this| {
this.child("Unsaved changes")
})
}
}
```
### Module Organization
#### Feature-Based Structure
```
src/
├── features/
│ ├── editor/
│ │ ├── mod.rs
│ │ ├── model.rs # EditorModel
│ │ ├── view.rs # EditorView
│ │ ├── commands.rs # Editor actions
│ │ └── components/ # Editor-specific components
│ ├── sidebar/
│ │ ├── mod.rs
│ │ ├── model.rs
│ │ ├── view.rs
│ │ └── components/
│ └── statusbar/
│ ├── mod.rs
│ ├── model.rs
│ └── view.rs
```
**Benefits**:
- Clear feature boundaries
- Easy to understand and navigate
- Scales well with team size
- Enables feature-based development
### State Management Architecture
#### Unidirectional Data Flow
```
User Action → Action Dispatch → State Update → View Rerender
↑ ↓
└──────────────── Event Handlers ─────────────┘
```
**Implementation**:
```rust
// Define actions
actions!(app, [AddTodo, ToggleTodo, DeleteTodo]);
// State model
pub struct TodoListModel {
todos: Vec<Todo>,
}
impl TodoListModel {
pub fn add_todo(&mut self, text: String) {
self.todos.push(Todo {
id: TodoId::new(),
text,
completed: false,
});
}
pub fn toggle_todo(&mut self, id: TodoId) {
if let Some(todo) = self.todos.iter_mut().find(|t| t.id == id) {
todo.completed = !todo.completed;
}
}
}
// View with action handlers
pub struct TodoListView {
model: Model<TodoListModel>,
}
impl TodoListView {
fn register_actions(&mut self, cx: &mut ViewContext<Self>) {
cx.on_action(cx.listener(|this, action: &AddTodo, cx| {
this.model.update(cx, |model, cx| {
model.add_todo(action.text.clone());
cx.notify();
});
}));
cx.on_action(cx.listener(|this, action: &ToggleTodo, cx| {
this.model.update(cx, |model, cx| {
model.toggle_todo(action.id);
cx.notify();
});
}));
}
}
```
#### State Ownership Patterns
**Single Source of Truth**:
```rust
pub struct AppModel {
// Root owns all state
documents: Vec<Model<DocumentModel>>,
settings: Model<Settings>,
ui_state: Model<UiState>,
}
```
**Hierarchical Ownership**:
```rust
pub struct WorkspaceModel {
// Workspace owns workspace-level state
panes: Vec<Model<PaneModel>>,
}
pub struct PaneModel {
// Pane owns pane-level state
tabs: Vec<Model<TabModel>>,
active_index: usize,
}
```
### Separation of Concerns
#### Clear Boundaries
```rust
// ✓ GOOD: Clear responsibilities
// Domain logic (no GPUI)
pub mod document {
pub struct Document {
content: String,
}
impl Document {
pub fn insert(&mut self, pos: usize, text: &str) {
self.content.insert_str(pos, text);
}
}
}
// Application logic (uses GPUI models)
pub mod editor_model {
use gpui::*;
use super::document::Document;
pub struct EditorModel {
document: Document,
cursor_position: usize,
}
impl EditorModel {
pub fn insert_at_cursor(&mut self, text: &str) {
self.document.insert(self.cursor_position, text);
self.cursor_position += text.len();
}
}
}
// UI logic (GPUI views)
pub mod editor_view {
use gpui::*;
use super::editor_model::EditorModel;
pub struct EditorView {
model: Model<EditorModel>,
}
impl Render for EditorView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
// Rendering logic
}
}
}
```
### Testability Patterns
#### Dependency Injection
```rust
// Define trait for external dependencies
pub trait FileService: Send + Sync {
fn read(&self, path: &Path) -> Result<String>;
fn write(&self, path: &Path, content: &str) -> Result<()>;
}
// Production implementation
pub struct RealFileService;
impl FileService for RealFileService {
// Real implementation
}
// Test implementation
#[cfg(test)]
pub struct MockFileService {
read_results: HashMap<PathBuf, Result<String>>,
written_files: RefCell<Vec<(PathBuf, String)>>,
}
#[cfg(test)]
impl FileService for MockFileService {
fn read(&self, path: &Path) -> Result<String> {
self.read_results
.get(path)
.cloned()
.unwrap_or_else(|| Err(anyhow::anyhow!("File not found")))
}
fn write(&self, path: &Path, content: &str) -> Result<()> {
self.written_files
.borrow_mut()
.push((path.to_path_buf(), content.to_string()));
Ok(())
}
}
// Model accepts any FileService
pub struct DocumentModel {
file_service: Arc<dyn FileService>,
}
// Tests use mock
#[cfg(test)]
mod tests {
#[test]
fn test_save() {
let mock_service = Arc::new(MockFileService::new());
let model = DocumentModel::new(mock_service.clone());
model.save().unwrap();
assert_eq!(mock_service.written_files.borrow().len(), 1);
}
}
```
### Plugin Architecture
#### Extension System
```rust
// Define plugin trait
pub trait EditorPlugin: Send + Sync {
fn name(&self) -> &str;
fn on_document_open(&self, doc: &Document) -> Result<()>;
fn on_document_save(&self, doc: &Document) -> Result<()>;
}
// Plugin manager
pub struct PluginManager {
plugins: Vec<Box<dyn EditorPlugin>>,
}
impl PluginManager {
pub fn register(&mut self, plugin: Box<dyn EditorPlugin>) {
self.plugins.push(plugin);
}
pub fn notify_document_open(&self, doc: &Document) -> Result<()> {
for plugin in &self.plugins {
plugin.on_document_open(doc)?;
}
Ok(())
}
}
// Example plugin
pub struct AutoSavePlugin {
interval: Duration,
}
impl EditorPlugin for AutoSavePlugin {
fn name(&self) -> &str {
"AutoSave"
}
fn on_document_open(&self, doc: &Document) -> Result<()> {
// Start auto-save timer
Ok(())
}
fn on_document_save(&self, doc: &Document) -> Result<()> {
println!("Document saved: {}", doc.path.display());
Ok(())
}
}
```
## Resources
### Design Patterns
**Architectural Patterns**:
- Model-View pattern (GPUI-specific)
- Container-Presenter (separation of concerns)
- Service-oriented (external dependencies)
- Plugin architecture (extensibility)
**Code Organization**:
- Feature-based modules
- Layer separation
- Clear boundaries
- Dependency injection
**State Management**:
- Unidirectional data flow
- Single source of truth
- Hierarchical ownership
- Reactive updates
### Best Practices
1. **Separation of Concerns**: Keep UI, logic, and data separate
2. **Dependency Injection**: Use traits for testability
3. **Feature Organization**: Group related code by feature
4. **State Ownership**: Clear ownership hierarchy
5. **Testable Design**: Design for testing from the start
6. **Documentation**: Document architecture decisions
7. **Modularity**: Small, focused modules
8. **Scalability**: Design for growth
### Common Patterns
- **Repository Pattern**: Data access abstraction
- **Command Pattern**: Action system
- **Observer Pattern**: Subscriptions
- **Factory Pattern**: Component creation
- **Strategy Pattern**: Pluggable behaviors
- **Facade Pattern**: Simplified interfaces