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