Initial commit
This commit is contained in:
603
skills/gpui-patterns/SKILL.md
Normal file
603
skills/gpui-patterns/SKILL.md
Normal 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
|
||||
603
skills/gpui-performance/SKILL.md
Normal file
603
skills/gpui-performance/SKILL.md
Normal 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
|
||||
617
skills/gpui-styling/SKILL.md
Normal file
617
skills/gpui-styling/SKILL.md
Normal 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
|
||||
606
skills/rust-ui-architecture/SKILL.md
Normal file
606
skills/rust-ui-architecture/SKILL.md
Normal 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
|
||||
Reference in New Issue
Block a user