# 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); type PrepaintState = (Hitbox, Vec); ``` **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, } 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) { // 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, ..) { // 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, last_width: Option, // Cached result cached_layout: Option, } 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, 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, container_bounds: &Bounds) -> 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, 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) { // 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) { 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) { // 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, } ``` **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, window: &mut Window, ..) { // Bad: Creating hitbox in paint let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); } ``` **Good:** ```rust fn prepaint(&mut self, .., bounds: Bounds, 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