feat: refactor to use gpui event instead of local state (#18)
Reviewed-on: #18 Co-authored-by: Ren Amamiya <reya@lume.nu> Co-committed-by: Ren Amamiya <reya@lume.nu>
This commit was merged in pull request #18.
This commit is contained in:
126
.agents/skills/gpui-element/SKILL.md
Normal file
126
.agents/skills/gpui-element/SKILL.md
Normal file
@@ -0,0 +1,126 @@
|
||||
---
|
||||
name: gpui-element
|
||||
description: Implementing custom elements using GPUI's low-level Element API (vs. high-level Render/RenderOnce APIs). Use when you need maximum control over layout, prepaint, and paint phases for complex, performance-critical custom UI components that cannot be achieved with Render/RenderOnce traits.
|
||||
---
|
||||
|
||||
## When to Use
|
||||
|
||||
Use the low-level `Element` trait when:
|
||||
- Need fine-grained control over layout calculation
|
||||
- Building complex, performance-critical components
|
||||
- Implementing custom layout algorithms (masonry, circular, etc.)
|
||||
- High-level `Render`/`RenderOnce` APIs are insufficient
|
||||
|
||||
**Prefer `Render`/`RenderOnce` for:** Simple components, standard layouts, declarative UI
|
||||
|
||||
## Quick Start
|
||||
|
||||
The `Element` trait provides direct control over three rendering phases:
|
||||
|
||||
```rust
|
||||
impl Element for MyElement {
|
||||
type RequestLayoutState = MyLayoutState; // Data passed to later phases
|
||||
type PrepaintState = MyPaintState; // Data for painting
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
// Phase 1: Calculate sizes and positions
|
||||
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
|
||||
-> (LayoutId, Self::RequestLayoutState)
|
||||
{
|
||||
let layout_id = window.request_layout(
|
||||
Style { size: size(px(200.), px(100.)), ..default() },
|
||||
vec![],
|
||||
cx
|
||||
);
|
||||
(layout_id, MyLayoutState { /* ... */ })
|
||||
}
|
||||
|
||||
// Phase 2: Create hitboxes, prepare for painting
|
||||
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, layout: &mut Self::RequestLayoutState,
|
||||
window: &mut Window, cx: &mut App) -> Self::PrepaintState
|
||||
{
|
||||
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
|
||||
MyPaintState { hitbox }
|
||||
}
|
||||
|
||||
// Phase 3: Render and handle interactions
|
||||
fn paint(&mut self, .., bounds: Bounds<Pixels>, layout: &mut Self::RequestLayoutState,
|
||||
paint_state: &mut Self::PrepaintState, window: &mut Window, cx: &mut App)
|
||||
{
|
||||
window.paint_quad(paint_quad(bounds, Corners::all(px(4.)), cx.theme().background));
|
||||
|
||||
window.on_mouse_event({
|
||||
let hitbox = paint_state.hitbox.clone();
|
||||
move |event: &MouseDownEvent, phase, window, cx| {
|
||||
if hitbox.is_hovered(window) && phase.bubble() {
|
||||
// Handle interaction
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Enable element to be used as child
|
||||
impl IntoElement for MyElement {
|
||||
type Element = Self;
|
||||
fn into_element(self) -> Self::Element { self }
|
||||
}
|
||||
```
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Three-Phase Rendering
|
||||
|
||||
1. **request_layout**: Calculate sizes and positions, return layout ID and state
|
||||
2. **prepaint**: Create hitboxes, compute final bounds, prepare for painting
|
||||
3. **paint**: Render element, set up interactions (mouse events, cursor styles)
|
||||
|
||||
### State Flow
|
||||
|
||||
```
|
||||
RequestLayoutState → PrepaintState → paint
|
||||
```
|
||||
|
||||
State flows in one direction through associated types, passed as mutable references between phases.
|
||||
|
||||
### Key Operations
|
||||
|
||||
- **Layout**: `window.request_layout(style, children, cx)` - Create layout node
|
||||
- **Hitboxes**: `window.insert_hitbox(bounds, behavior)` - Create interaction area
|
||||
- **Painting**: `window.paint_quad(...)` - Render visual content
|
||||
- **Events**: `window.on_mouse_event(handler)` - Handle user input
|
||||
|
||||
## Reference Documentation
|
||||
|
||||
### Complete API Documentation
|
||||
- **Element Trait API**: See [api-reference.md](references/api-reference.md)
|
||||
- Associated types, methods, parameters, return values
|
||||
- Hitbox system, event handling, cursor styles
|
||||
|
||||
### Implementation Guides
|
||||
- **Examples**: See [examples.md](references/examples.md)
|
||||
- Simple text element with highlighting
|
||||
- Interactive element with selection
|
||||
- Complex element with child management
|
||||
|
||||
- **Best Practices**: See [best-practices.md](references/best-practices.md)
|
||||
- State management, performance optimization
|
||||
- Interaction handling, layout strategies
|
||||
- Error handling, testing, common pitfalls
|
||||
|
||||
- **Common Patterns**: See [patterns.md](references/patterns.md)
|
||||
- Text rendering, container, interactive, composite, scrollable patterns
|
||||
- Pattern selection guide
|
||||
|
||||
- **Advanced Patterns**: See [advanced-patterns.md](references/advanced-patterns.md)
|
||||
- Custom layout algorithms (masonry, circular)
|
||||
- Element composition with traits
|
||||
- Async updates, memoization, virtual lists
|
||||
705
.agents/skills/gpui-element/references/advanced-patterns.md
Normal file
705
.agents/skills/gpui-element/references/advanced-patterns.md
Normal file
@@ -0,0 +1,705 @@
|
||||
# Advanced Element Patterns
|
||||
|
||||
Advanced techniques and patterns for implementing sophisticated GPUI elements.
|
||||
|
||||
## Custom Layout Algorithms
|
||||
|
||||
Implementing custom layout algorithms not supported by GPUI's built-in layouts.
|
||||
|
||||
### Masonry Layout (Pinterest-Style)
|
||||
|
||||
```rust
|
||||
pub struct MasonryLayout {
|
||||
id: ElementId,
|
||||
columns: usize,
|
||||
gap: Pixels,
|
||||
children: Vec<AnyElement>,
|
||||
}
|
||||
|
||||
struct MasonryLayoutState {
|
||||
column_layouts: Vec<Vec<LayoutId>>,
|
||||
column_heights: Vec<Pixels>,
|
||||
}
|
||||
|
||||
struct MasonryPaintState {
|
||||
child_bounds: Vec<Bounds<Pixels>>,
|
||||
}
|
||||
|
||||
impl Element for MasonryLayout {
|
||||
type RequestLayoutState = MasonryLayoutState;
|
||||
type PrepaintState = MasonryPaintState;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) -> (LayoutId, MasonryLayoutState) {
|
||||
// Initialize columns
|
||||
let mut columns: Vec<Vec<LayoutId>> = vec![Vec::new(); self.columns];
|
||||
let mut column_heights = vec![px(0.); self.columns];
|
||||
|
||||
// Distribute children across columns
|
||||
for child in &mut self.children {
|
||||
let (child_layout_id, _) = child.request_layout(
|
||||
global_id,
|
||||
inspector_id,
|
||||
window,
|
||||
cx
|
||||
);
|
||||
|
||||
let child_size = window.layout_bounds(child_layout_id).size;
|
||||
|
||||
// Find shortest column
|
||||
let min_column_idx = column_heights
|
||||
.iter()
|
||||
.enumerate()
|
||||
.min_by(|a, b| a.1.partial_cmp(b.1).unwrap())
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
// Add child to shortest column
|
||||
columns[min_column_idx].push(child_layout_id);
|
||||
column_heights[min_column_idx] += child_size.height + self.gap;
|
||||
}
|
||||
|
||||
// Calculate total layout size
|
||||
let column_width = px(200.); // Fixed column width
|
||||
let total_width = column_width * self.columns as f32
|
||||
+ self.gap * (self.columns - 1) as f32;
|
||||
let total_height = column_heights.iter()
|
||||
.max_by(|a, b| a.partial_cmp(b).unwrap())
|
||||
.copied()
|
||||
.unwrap_or(px(0.));
|
||||
|
||||
let layout_id = window.request_layout(
|
||||
Style {
|
||||
size: size(total_width, total_height),
|
||||
..default()
|
||||
},
|
||||
columns.iter().flatten().copied().collect(),
|
||||
cx
|
||||
);
|
||||
|
||||
(layout_id, MasonryLayoutState {
|
||||
column_layouts: columns,
|
||||
column_heights,
|
||||
})
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
layout_state: &mut MasonryLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) -> MasonryPaintState {
|
||||
let column_width = px(200.);
|
||||
let mut child_bounds = Vec::new();
|
||||
|
||||
// Position children in columns
|
||||
for (col_idx, column) in layout_state.column_layouts.iter().enumerate() {
|
||||
let x_offset = bounds.left()
|
||||
+ (column_width + self.gap) * col_idx as f32;
|
||||
let mut y_offset = bounds.top();
|
||||
|
||||
for (child_idx, layout_id) in column.iter().enumerate() {
|
||||
let child_size = window.layout_bounds(*layout_id).size;
|
||||
let child_bound = Bounds::new(
|
||||
point(x_offset, y_offset),
|
||||
size(column_width, child_size.height)
|
||||
);
|
||||
|
||||
self.children[child_idx].prepaint(
|
||||
global_id,
|
||||
inspector_id,
|
||||
child_bound,
|
||||
window,
|
||||
cx
|
||||
);
|
||||
|
||||
child_bounds.push(child_bound);
|
||||
y_offset += child_size.height + self.gap;
|
||||
}
|
||||
}
|
||||
|
||||
MasonryPaintState { child_bounds }
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_layout_state: &mut MasonryLayoutState,
|
||||
paint_state: &mut MasonryPaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) {
|
||||
for (child, bounds) in self.children.iter_mut().zip(&paint_state.child_bounds) {
|
||||
child.paint(global_id, inspector_id, *bounds, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Circular Layout
|
||||
|
||||
```rust
|
||||
pub struct CircularLayout {
|
||||
id: ElementId,
|
||||
radius: Pixels,
|
||||
children: Vec<AnyElement>,
|
||||
}
|
||||
|
||||
impl Element for CircularLayout {
|
||||
type RequestLayoutState = Vec<LayoutId>;
|
||||
type PrepaintState = Vec<Bounds<Pixels>>;
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) -> (LayoutId, Vec<LayoutId>) {
|
||||
let child_layouts: Vec<_> = self.children
|
||||
.iter_mut()
|
||||
.map(|child| child.request_layout(global_id, inspector_id, window, cx).0)
|
||||
.collect();
|
||||
|
||||
let diameter = self.radius * 2.;
|
||||
let layout_id = window.request_layout(
|
||||
Style {
|
||||
size: size(diameter, diameter),
|
||||
..default()
|
||||
},
|
||||
child_layouts.clone(),
|
||||
cx
|
||||
);
|
||||
|
||||
(layout_id, child_layouts)
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
layout_ids: &mut Vec<LayoutId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) -> Vec<Bounds<Pixels>> {
|
||||
let center = bounds.center();
|
||||
let angle_step = 2.0 * std::f32::consts::PI / self.children.len() as f32;
|
||||
|
||||
let mut child_bounds = Vec::new();
|
||||
|
||||
for (i, (child, layout_id)) in self.children.iter_mut()
|
||||
.zip(layout_ids.iter())
|
||||
.enumerate()
|
||||
{
|
||||
let angle = angle_step * i as f32;
|
||||
let child_size = window.layout_bounds(*layout_id).size;
|
||||
|
||||
// Position child on circle
|
||||
let x = center.x + self.radius * angle.cos() - child_size.width / 2.;
|
||||
let y = center.y + self.radius * angle.sin() - child_size.height / 2.;
|
||||
|
||||
let child_bound = Bounds::new(point(x, y), child_size);
|
||||
|
||||
child.prepaint(global_id, inspector_id, child_bound, window, cx);
|
||||
child_bounds.push(child_bound);
|
||||
}
|
||||
|
||||
child_bounds
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
_layout_ids: &mut Vec<LayoutId>,
|
||||
child_bounds: &mut Vec<Bounds<Pixels>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) {
|
||||
for (child, bounds) in self.children.iter_mut().zip(child_bounds) {
|
||||
child.paint(global_id, inspector_id, *bounds, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Element Composition with Traits
|
||||
|
||||
Create reusable behaviors via traits for element composition.
|
||||
|
||||
### Hoverable Trait
|
||||
|
||||
```rust
|
||||
pub trait Hoverable: Element {
|
||||
fn on_hover<F>(&mut self, f: F) -> &mut Self
|
||||
where
|
||||
F: Fn(&mut Window, &mut App) + 'static;
|
||||
|
||||
fn on_hover_end<F>(&mut self, f: F) -> &mut Self
|
||||
where
|
||||
F: Fn(&mut Window, &mut App) + 'static;
|
||||
}
|
||||
|
||||
// Implementation for custom element
|
||||
pub struct HoverableElement {
|
||||
id: ElementId,
|
||||
content: AnyElement,
|
||||
hover_handlers: Vec<Box<dyn Fn(&mut Window, &mut App)>>,
|
||||
hover_end_handlers: Vec<Box<dyn Fn(&mut Window, &mut App)>>,
|
||||
was_hovered: bool,
|
||||
}
|
||||
|
||||
impl Hoverable for HoverableElement {
|
||||
fn on_hover<F>(&mut self, f: F) -> &mut Self
|
||||
where
|
||||
F: Fn(&mut Window, &mut App) + 'static
|
||||
{
|
||||
self.hover_handlers.push(Box::new(f));
|
||||
self
|
||||
}
|
||||
|
||||
fn on_hover_end<F>(&mut self, f: F) -> &mut Self
|
||||
where
|
||||
F: Fn(&mut Window, &mut App) + 'static
|
||||
{
|
||||
self.hover_end_handlers.push(Box::new(f));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for HoverableElement {
|
||||
type RequestLayoutState = LayoutId;
|
||||
type PrepaintState = Hitbox;
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_global_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_layout: &mut LayoutId,
|
||||
hitbox: &mut Hitbox,
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) {
|
||||
let is_hovered = hitbox.is_hovered(window);
|
||||
|
||||
// Trigger hover events
|
||||
if is_hovered && !self.was_hovered {
|
||||
for handler in &self.hover_handlers {
|
||||
handler(window, cx);
|
||||
}
|
||||
} else if !is_hovered && self.was_hovered {
|
||||
for handler in &self.hover_end_handlers {
|
||||
handler(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
self.was_hovered = is_hovered;
|
||||
|
||||
// Paint content
|
||||
self.content.paint(bounds, window, cx);
|
||||
}
|
||||
|
||||
// ... other methods
|
||||
}
|
||||
```
|
||||
|
||||
### Clickable Trait
|
||||
|
||||
```rust
|
||||
pub trait Clickable: Element {
|
||||
fn on_click<F>(&mut self, f: F) -> &mut Self
|
||||
where
|
||||
F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static;
|
||||
|
||||
fn on_double_click<F>(&mut self, f: F) -> &mut Self
|
||||
where
|
||||
F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static;
|
||||
}
|
||||
|
||||
pub struct ClickableElement {
|
||||
id: ElementId,
|
||||
content: AnyElement,
|
||||
click_handlers: Vec<Box<dyn Fn(&MouseUpEvent, &mut Window, &mut App)>>,
|
||||
double_click_handlers: Vec<Box<dyn Fn(&MouseUpEvent, &mut Window, &mut App)>>,
|
||||
last_click_time: Option<Instant>,
|
||||
}
|
||||
|
||||
impl Clickable for ClickableElement {
|
||||
fn on_click<F>(&mut self, f: F) -> &mut Self
|
||||
where
|
||||
F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static
|
||||
{
|
||||
self.click_handlers.push(Box::new(f));
|
||||
self
|
||||
}
|
||||
|
||||
fn on_double_click<F>(&mut self, f: F) -> &mut Self
|
||||
where
|
||||
F: Fn(&MouseUpEvent, &mut Window, &mut App) + 'static
|
||||
{
|
||||
self.double_click_handlers.push(Box::new(f));
|
||||
self
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Async Element Updates
|
||||
|
||||
Elements that update based on async operations.
|
||||
|
||||
```rust
|
||||
pub struct AsyncElement {
|
||||
id: ElementId,
|
||||
state: Entity<AsyncState>,
|
||||
loading: bool,
|
||||
data: Option<String>,
|
||||
}
|
||||
|
||||
pub struct AsyncState {
|
||||
loading: bool,
|
||||
data: Option<String>,
|
||||
}
|
||||
|
||||
impl Element for AsyncElement {
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = Hitbox;
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_global_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_layout: &mut (),
|
||||
hitbox: &mut Hitbox,
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) {
|
||||
// Display loading or data
|
||||
if self.loading {
|
||||
// Paint loading indicator
|
||||
self.paint_loading(bounds, window, cx);
|
||||
} else if let Some(data) = &self.data {
|
||||
// Paint data
|
||||
self.paint_data(data, bounds, window, cx);
|
||||
}
|
||||
|
||||
// Trigger async update on click
|
||||
window.on_mouse_event({
|
||||
let state = self.state.clone();
|
||||
let hitbox = hitbox.clone();
|
||||
|
||||
move |event: &MouseUpEvent, phase, window, cx| {
|
||||
if hitbox.is_hovered(window) && phase.bubble() {
|
||||
// Spawn async task
|
||||
cx.spawn({
|
||||
let state = state.clone();
|
||||
async move {
|
||||
// Perform async operation
|
||||
let result = fetch_data_async().await;
|
||||
|
||||
// Update state on completion
|
||||
state.update(cx, |state, cx| {
|
||||
state.loading = false;
|
||||
state.data = Some(result);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}).detach();
|
||||
|
||||
// Set loading state immediately
|
||||
state.update(cx, |state, cx| {
|
||||
state.loading = true;
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ... other methods
|
||||
}
|
||||
|
||||
async fn fetch_data_async() -> String {
|
||||
// Simulate async operation
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
"Data loaded!".to_string()
|
||||
}
|
||||
```
|
||||
|
||||
## Element Memoization
|
||||
|
||||
Optimize performance by memoizing expensive element computations.
|
||||
|
||||
```rust
|
||||
pub struct MemoizedElement<T: PartialEq + Clone + 'static> {
|
||||
id: ElementId,
|
||||
value: T,
|
||||
render_fn: Box<dyn Fn(&T) -> AnyElement>,
|
||||
cached_element: Option<AnyElement>,
|
||||
last_value: Option<T>,
|
||||
}
|
||||
|
||||
impl<T: PartialEq + Clone + 'static> MemoizedElement<T> {
|
||||
pub fn new<F>(id: ElementId, value: T, render_fn: F) -> Self
|
||||
where
|
||||
F: Fn(&T) -> AnyElement + 'static,
|
||||
{
|
||||
Self {
|
||||
id,
|
||||
value,
|
||||
render_fn: Box::new(render_fn),
|
||||
cached_element: None,
|
||||
last_value: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq + Clone + 'static> Element for MemoizedElement<T> {
|
||||
type RequestLayoutState = LayoutId;
|
||||
type PrepaintState = ();
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) -> (LayoutId, LayoutId) {
|
||||
// Check if value changed
|
||||
if self.last_value.as_ref() != Some(&self.value) || self.cached_element.is_none() {
|
||||
// Recompute element
|
||||
self.cached_element = Some((self.render_fn)(&self.value));
|
||||
self.last_value = Some(self.value.clone());
|
||||
}
|
||||
|
||||
// Request layout for cached element
|
||||
let (layout_id, _) = self.cached_element
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.request_layout(global_id, inspector_id, window, cx);
|
||||
|
||||
(layout_id, layout_id)
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_layout_id: &mut LayoutId,
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) -> () {
|
||||
self.cached_element
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.prepaint(global_id, inspector_id, bounds, window, cx);
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_layout_id: &mut LayoutId,
|
||||
_: &mut (),
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) {
|
||||
self.cached_element
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.paint(global_id, inspector_id, bounds, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
MemoizedElement::new(
|
||||
ElementId::Name("memoized".into()),
|
||||
self.expensive_value.clone(),
|
||||
|value| {
|
||||
// Expensive rendering function only called when value changes
|
||||
div().child(format!("Computed: {}", value))
|
||||
}
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Virtual List Pattern
|
||||
|
||||
Efficiently render large lists by only rendering visible items.
|
||||
|
||||
```rust
|
||||
pub struct VirtualList {
|
||||
id: ElementId,
|
||||
item_count: usize,
|
||||
item_height: Pixels,
|
||||
viewport_height: Pixels,
|
||||
scroll_offset: Pixels,
|
||||
render_item: Box<dyn Fn(usize) -> AnyElement>,
|
||||
}
|
||||
|
||||
struct VirtualListState {
|
||||
visible_range: Range<usize>,
|
||||
visible_item_layouts: Vec<LayoutId>,
|
||||
}
|
||||
|
||||
impl Element for VirtualList {
|
||||
type RequestLayoutState = VirtualListState;
|
||||
type PrepaintState = Hitbox;
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) -> (LayoutId, VirtualListState) {
|
||||
// Calculate visible range
|
||||
let start_idx = (self.scroll_offset / self.item_height).floor() as usize;
|
||||
let end_idx = ((self.scroll_offset + self.viewport_height) / self.item_height)
|
||||
.ceil() as usize;
|
||||
let visible_range = start_idx..end_idx.min(self.item_count);
|
||||
|
||||
// Request layout only for visible items
|
||||
let visible_item_layouts: Vec<_> = visible_range.clone()
|
||||
.map(|i| {
|
||||
let mut item = (self.render_item)(i);
|
||||
item.request_layout(global_id, inspector_id, window, cx).0
|
||||
})
|
||||
.collect();
|
||||
|
||||
let total_height = self.item_height * self.item_count as f32;
|
||||
let layout_id = window.request_layout(
|
||||
Style {
|
||||
size: size(relative(1.0), self.viewport_height),
|
||||
overflow: Overflow::Hidden,
|
||||
..default()
|
||||
},
|
||||
visible_item_layouts.clone(),
|
||||
cx
|
||||
);
|
||||
|
||||
(layout_id, VirtualListState {
|
||||
visible_range,
|
||||
visible_item_layouts,
|
||||
})
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_global_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
state: &mut VirtualListState,
|
||||
window: &mut Window,
|
||||
_cx: &mut App
|
||||
) -> Hitbox {
|
||||
// Prepaint visible items at correct positions
|
||||
for (i, layout_id) in state.visible_item_layouts.iter().enumerate() {
|
||||
let item_idx = state.visible_range.start + i;
|
||||
let y = item_idx as f32 * self.item_height - self.scroll_offset;
|
||||
let item_bounds = Bounds::new(
|
||||
point(bounds.left(), bounds.top() + y),
|
||||
size(bounds.width(), self.item_height)
|
||||
);
|
||||
|
||||
// Prepaint if visible
|
||||
if item_bounds.intersects(&bounds) {
|
||||
// Prepaint item...
|
||||
}
|
||||
}
|
||||
|
||||
window.insert_hitbox(bounds, HitboxBehavior::Normal)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_global_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
state: &mut VirtualListState,
|
||||
hitbox: &mut Hitbox,
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) {
|
||||
// Paint visible items
|
||||
for (i, _layout_id) in state.visible_item_layouts.iter().enumerate() {
|
||||
let item_idx = state.visible_range.start + i;
|
||||
let y = item_idx as f32 * self.item_height - self.scroll_offset;
|
||||
let item_bounds = Bounds::new(
|
||||
point(bounds.left(), bounds.top() + y),
|
||||
size(bounds.width(), self.item_height)
|
||||
);
|
||||
|
||||
if item_bounds.intersects(&bounds) {
|
||||
let mut item = (self.render_item)(item_idx);
|
||||
item.paint(item_bounds, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle scroll
|
||||
window.on_mouse_event({
|
||||
let hitbox = hitbox.clone();
|
||||
let total_height = self.item_height * self.item_count as f32;
|
||||
|
||||
move |event: &ScrollWheelEvent, phase, window, cx| {
|
||||
if hitbox.is_hovered(window) && phase.bubble() {
|
||||
self.scroll_offset -= event.delta.y;
|
||||
self.scroll_offset = self.scroll_offset
|
||||
.max(px(0.))
|
||||
.min(total_height - self.viewport_height);
|
||||
cx.notify();
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Usage: Efficiently render 10,000 items
|
||||
let virtual_list = VirtualList {
|
||||
id: ElementId::Name("large-list".into()),
|
||||
item_count: 10_000,
|
||||
item_height: px(40.),
|
||||
viewport_height: px(400.),
|
||||
scroll_offset: px(0.),
|
||||
render_item: Box::new(|index| {
|
||||
div().child(format!("Item {}", index))
|
||||
}),
|
||||
};
|
||||
```
|
||||
|
||||
These advanced patterns enable sophisticated element implementations while maintaining performance and code quality.
|
||||
477
.agents/skills/gpui-element/references/api-reference.md
Normal file
477
.agents/skills/gpui-element/references/api-reference.md
Normal file
@@ -0,0 +1,477 @@
|
||||
# Element API Reference
|
||||
|
||||
Complete API documentation for GPUI's low-level Element trait.
|
||||
|
||||
## Element Trait Structure
|
||||
|
||||
The `Element` trait requires implementing three associated types and five methods:
|
||||
|
||||
```rust
|
||||
pub trait Element: 'static + IntoElement {
|
||||
type RequestLayoutState: 'static;
|
||||
type PrepaintState: 'static;
|
||||
|
||||
fn id(&self) -> Option<ElementId>;
|
||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>>;
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState);
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self::PrepaintState;
|
||||
fn paint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
prepaint: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Associated Types
|
||||
|
||||
### RequestLayoutState
|
||||
|
||||
Data passed from `request_layout` to `prepaint` and `paint` phases.
|
||||
|
||||
**Usage:**
|
||||
- Store layout calculations (styled text, child layout IDs)
|
||||
- Cache expensive computations
|
||||
- Pass child state between phases
|
||||
|
||||
**Examples:**
|
||||
```rust
|
||||
// Simple: no state needed
|
||||
type RequestLayoutState = ();
|
||||
|
||||
// Single value
|
||||
type RequestLayoutState = StyledText;
|
||||
|
||||
// Multiple values
|
||||
type RequestLayoutState = (StyledText, Vec<ChildLayout>);
|
||||
|
||||
// Complex struct
|
||||
pub struct MyLayoutState {
|
||||
pub styled_text: StyledText,
|
||||
pub child_layouts: Vec<(LayoutId, ChildState)>,
|
||||
pub computed_bounds: Bounds<Pixels>,
|
||||
}
|
||||
type RequestLayoutState = MyLayoutState;
|
||||
```
|
||||
|
||||
### PrepaintState
|
||||
|
||||
Data passed from `prepaint` to `paint` phase.
|
||||
|
||||
**Usage:**
|
||||
- Store hitboxes for interaction
|
||||
- Cache visual bounds
|
||||
- Store prepaint results
|
||||
|
||||
**Examples:**
|
||||
```rust
|
||||
// Simple: just a hitbox
|
||||
type PrepaintState = Hitbox;
|
||||
|
||||
// Optional hitbox
|
||||
type PrepaintState = Option<Hitbox>;
|
||||
|
||||
// Multiple values
|
||||
type PrepaintState = (Hitbox, Vec<Bounds<Pixels>>);
|
||||
|
||||
// Complex struct
|
||||
pub struct MyPaintState {
|
||||
pub hitbox: Hitbox,
|
||||
pub child_bounds: Vec<Bounds<Pixels>>,
|
||||
pub visible_range: Range<usize>,
|
||||
}
|
||||
type PrepaintState = MyPaintState;
|
||||
```
|
||||
|
||||
## Methods
|
||||
|
||||
### id()
|
||||
|
||||
Returns optional unique identifier for debugging and inspection.
|
||||
|
||||
```rust
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
// Or if no ID needed
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
```
|
||||
|
||||
### source_location()
|
||||
|
||||
Returns source location for debugging. Usually returns `None` unless debugging is needed.
|
||||
|
||||
```rust
|
||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
```
|
||||
|
||||
### request_layout()
|
||||
|
||||
Calculates sizes and positions for the element tree.
|
||||
|
||||
**Parameters:**
|
||||
- `global_id`: Global element identifier (optional)
|
||||
- `inspector_id`: Inspector element identifier (optional)
|
||||
- `window`: Mutable window reference
|
||||
- `cx`: Mutable app context
|
||||
|
||||
**Returns:**
|
||||
- `(LayoutId, Self::RequestLayoutState)`: Layout ID and state for next phases
|
||||
|
||||
**Responsibilities:**
|
||||
1. Calculate child layouts by calling `child.request_layout()`
|
||||
2. Create own layout using `window.request_layout()`
|
||||
3. Return layout ID and state to pass to next phases
|
||||
|
||||
**Example:**
|
||||
```rust
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
// 1. Calculate child layouts
|
||||
let child_layout_id = self.child.request_layout(
|
||||
global_id,
|
||||
inspector_id,
|
||||
window,
|
||||
cx
|
||||
).0;
|
||||
|
||||
// 2. Create own layout
|
||||
let layout_id = window.request_layout(
|
||||
Style {
|
||||
size: size(px(200.), px(100.)),
|
||||
..default()
|
||||
},
|
||||
vec![child_layout_id],
|
||||
cx
|
||||
);
|
||||
|
||||
// 3. Return layout ID and state
|
||||
(layout_id, MyLayoutState { child_layout_id })
|
||||
}
|
||||
```
|
||||
|
||||
### prepaint()
|
||||
|
||||
Prepares for painting by creating hitboxes and computing final bounds.
|
||||
|
||||
**Parameters:**
|
||||
- `global_id`: Global element identifier (optional)
|
||||
- `inspector_id`: Inspector element identifier (optional)
|
||||
- `bounds`: Final bounds calculated by layout engine
|
||||
- `request_layout`: Mutable reference to layout state
|
||||
- `window`: Mutable window reference
|
||||
- `cx`: Mutable app context
|
||||
|
||||
**Returns:**
|
||||
- `Self::PrepaintState`: State for paint phase
|
||||
|
||||
**Responsibilities:**
|
||||
1. Compute final child bounds based on layout bounds
|
||||
2. Call `child.prepaint()` for all children
|
||||
3. Create hitboxes using `window.insert_hitbox()`
|
||||
4. Return state for paint phase
|
||||
|
||||
**Example:**
|
||||
```rust
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
// 1. Compute child bounds
|
||||
let child_bounds = bounds; // or calculated subset
|
||||
|
||||
// 2. Prepaint children
|
||||
self.child.prepaint(
|
||||
global_id,
|
||||
inspector_id,
|
||||
child_bounds,
|
||||
&mut request_layout.child_state,
|
||||
window,
|
||||
cx
|
||||
);
|
||||
|
||||
// 3. Create hitboxes
|
||||
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
|
||||
|
||||
// 4. Return paint state
|
||||
MyPaintState { hitbox }
|
||||
}
|
||||
```
|
||||
|
||||
### paint()
|
||||
|
||||
Renders the element and handles interactions.
|
||||
|
||||
**Parameters:**
|
||||
- `global_id`: Global element identifier (optional)
|
||||
- `inspector_id`: Inspector element identifier (optional)
|
||||
- `bounds`: Final bounds for rendering
|
||||
- `request_layout`: Mutable reference to layout state
|
||||
- `prepaint`: Mutable reference to prepaint state
|
||||
- `window`: Mutable window reference
|
||||
- `cx`: Mutable app context
|
||||
|
||||
**Responsibilities:**
|
||||
1. Paint children first (bottom to top)
|
||||
2. Paint own content (backgrounds, borders, etc.)
|
||||
3. Set up interactions (mouse events, cursor styles)
|
||||
|
||||
**Example:**
|
||||
```rust
|
||||
fn paint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
prepaint: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
// 1. Paint children first
|
||||
self.child.paint(
|
||||
global_id,
|
||||
inspector_id,
|
||||
child_bounds,
|
||||
&mut request_layout.child_state,
|
||||
&mut prepaint.child_paint_state,
|
||||
window,
|
||||
cx
|
||||
);
|
||||
|
||||
// 2. Paint own content
|
||||
window.paint_quad(paint_quad(
|
||||
bounds,
|
||||
Corners::all(px(4.)),
|
||||
cx.theme().background,
|
||||
));
|
||||
|
||||
// 3. Set up interactions
|
||||
window.on_mouse_event({
|
||||
let hitbox = prepaint.hitbox.clone();
|
||||
move |event: &MouseDownEvent, phase, window, cx| {
|
||||
if hitbox.is_hovered(window) && phase.bubble() {
|
||||
// Handle click
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.set_cursor_style(CursorStyle::PointingHand, &prepaint.hitbox);
|
||||
}
|
||||
```
|
||||
|
||||
## IntoElement Integration
|
||||
|
||||
Elements must also implement `IntoElement` to be used as children:
|
||||
|
||||
```rust
|
||||
impl IntoElement for MyElement {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This allows your custom element to be used directly in the element tree:
|
||||
|
||||
```rust
|
||||
div()
|
||||
.child(MyElement::new()) // Works because of IntoElement
|
||||
```
|
||||
|
||||
## Common Parameters
|
||||
|
||||
### Global and Inspector IDs
|
||||
|
||||
Both are optional identifiers used for debugging and inspection:
|
||||
- `global_id`: Unique identifier across entire app
|
||||
- `inspector_id`: Identifier for dev tools/inspector
|
||||
|
||||
Usually passed through to children without modification.
|
||||
|
||||
### Window and Context
|
||||
|
||||
- `window: &mut Window`: Window-specific operations (painting, hitboxes, events)
|
||||
- `cx: &mut App`: App-wide operations (spawning tasks, accessing globals)
|
||||
|
||||
## Layout System Integration
|
||||
|
||||
### window.request_layout()
|
||||
|
||||
Creates a layout node with specified style and children:
|
||||
|
||||
```rust
|
||||
let layout_id = window.request_layout(
|
||||
Style {
|
||||
size: size(px(200.), px(100.)),
|
||||
flex: Flex::Column,
|
||||
gap: px(8.),
|
||||
..default()
|
||||
},
|
||||
vec![child1_layout_id, child2_layout_id],
|
||||
cx
|
||||
);
|
||||
```
|
||||
|
||||
### Bounds<Pixels>
|
||||
|
||||
Represents rectangular region:
|
||||
|
||||
```rust
|
||||
pub struct Bounds<T> {
|
||||
pub origin: Point<T>,
|
||||
pub size: Size<T>,
|
||||
}
|
||||
|
||||
// Create bounds
|
||||
let bounds = Bounds::new(
|
||||
point(px(10.), px(20.)),
|
||||
size(px(100.), px(50.))
|
||||
);
|
||||
|
||||
// Access properties
|
||||
bounds.left() // origin.x
|
||||
bounds.top() // origin.y
|
||||
bounds.right() // origin.x + size.width
|
||||
bounds.bottom() // origin.y + size.height
|
||||
bounds.center() // center point
|
||||
```
|
||||
|
||||
## Hitbox System
|
||||
|
||||
### Creating Hitboxes
|
||||
|
||||
```rust
|
||||
// Normal hitbox (blocks events)
|
||||
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
|
||||
|
||||
// Transparent hitbox (passes events through)
|
||||
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Transparent);
|
||||
```
|
||||
|
||||
### Using Hitboxes
|
||||
|
||||
```rust
|
||||
// Check if hovered
|
||||
if hitbox.is_hovered(window) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Set cursor style
|
||||
window.set_cursor_style(CursorStyle::PointingHand, &hitbox);
|
||||
|
||||
// Use in event handlers
|
||||
window.on_mouse_event(move |event, phase, window, cx| {
|
||||
if hitbox.is_hovered(window) && phase.bubble() {
|
||||
// Handle event
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Event Handling
|
||||
|
||||
### Mouse Events
|
||||
|
||||
```rust
|
||||
// Mouse down
|
||||
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
|
||||
if phase.bubble() && bounds.contains(&event.position) {
|
||||
// Handle mouse down
|
||||
cx.stop_propagation(); // Prevent bubbling
|
||||
}
|
||||
});
|
||||
|
||||
// Mouse up
|
||||
window.on_mouse_event(move |event: &MouseUpEvent, phase, window, cx| {
|
||||
// Handle mouse up
|
||||
});
|
||||
|
||||
// Mouse move
|
||||
window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| {
|
||||
// Handle mouse move
|
||||
});
|
||||
|
||||
// Scroll
|
||||
window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| {
|
||||
// Handle scroll
|
||||
});
|
||||
```
|
||||
|
||||
### Event Phase
|
||||
|
||||
Events go through two phases:
|
||||
- **Capture**: Top-down (parent → child)
|
||||
- **Bubble**: Bottom-up (child → parent)
|
||||
|
||||
```rust
|
||||
move |event, phase, window, cx| {
|
||||
if phase.capture() {
|
||||
// Handle in capture phase
|
||||
} else if phase.bubble() {
|
||||
// Handle in bubble phase
|
||||
}
|
||||
|
||||
cx.stop_propagation(); // Stop event from continuing
|
||||
}
|
||||
```
|
||||
|
||||
## Cursor Styles
|
||||
|
||||
Available cursor styles:
|
||||
|
||||
```rust
|
||||
CursorStyle::Arrow
|
||||
CursorStyle::IBeam // Text selection
|
||||
CursorStyle::PointingHand // Clickable
|
||||
CursorStyle::ResizeLeft
|
||||
CursorStyle::ResizeRight
|
||||
CursorStyle::ResizeUp
|
||||
CursorStyle::ResizeDown
|
||||
CursorStyle::ResizeLeftRight
|
||||
CursorStyle::ResizeUpDown
|
||||
CursorStyle::Crosshair
|
||||
CursorStyle::OperationNotAllowed
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```rust
|
||||
window.set_cursor_style(CursorStyle::PointingHand, &hitbox);
|
||||
```
|
||||
546
.agents/skills/gpui-element/references/best-practices.md
Normal file
546
.agents/skills/gpui-element/references/best-practices.md
Normal file
@@ -0,0 +1,546 @@
|
||||
# Element Best Practices
|
||||
|
||||
Guidelines and best practices for implementing high-quality GPUI elements.
|
||||
|
||||
## State Management
|
||||
|
||||
### Using Associated Types Effectively
|
||||
|
||||
**Good:** Use associated types to pass meaningful data between phases
|
||||
|
||||
```rust
|
||||
// Good: Structured state with type safety
|
||||
type RequestLayoutState = (StyledText, Vec<ChildLayout>);
|
||||
type PrepaintState = (Hitbox, Vec<ChildBounds>);
|
||||
```
|
||||
|
||||
**Bad:** Using empty state when you need data
|
||||
|
||||
```rust
|
||||
// Bad: No state when you need to pass data
|
||||
type RequestLayoutState = ();
|
||||
type PrepaintState = ();
|
||||
// Now you can't pass layout info to paint phase!
|
||||
```
|
||||
|
||||
### Managing Complex State
|
||||
|
||||
For elements with complex state, create dedicated structs:
|
||||
|
||||
```rust
|
||||
// Good: Dedicated struct for complex state
|
||||
pub struct TextElementState {
|
||||
pub styled_text: StyledText,
|
||||
pub text_layout: TextLayout,
|
||||
pub child_states: Vec<ChildState>,
|
||||
}
|
||||
|
||||
type RequestLayoutState = TextElementState;
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Clear documentation of state structure
|
||||
- Easy to extend
|
||||
- Type-safe access
|
||||
|
||||
### State Lifecycle
|
||||
|
||||
**Golden Rule:** State flows in one direction through the phases
|
||||
|
||||
```
|
||||
request_layout → RequestLayoutState →
|
||||
prepaint → PrepaintState →
|
||||
paint
|
||||
```
|
||||
|
||||
**Don't:**
|
||||
- Store state in the element struct that should be in associated types
|
||||
- Try to mutate element state in paint phase (use `cx.notify()` to schedule re-render)
|
||||
- Pass mutable references across phase boundaries
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Minimize Allocations in Paint Phase
|
||||
|
||||
**Critical:** Paint phase is called every frame during animations. Minimize allocations.
|
||||
|
||||
**Good:** Pre-allocate in `request_layout` or `prepaint`
|
||||
|
||||
```rust
|
||||
impl Element for MyElement {
|
||||
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
|
||||
-> (LayoutId, Vec<StyledText>)
|
||||
{
|
||||
// Allocate once during layout
|
||||
let styled_texts = self.children
|
||||
.iter()
|
||||
.map(|child| StyledText::new(child.text.clone()))
|
||||
.collect();
|
||||
|
||||
(layout_id, styled_texts)
|
||||
}
|
||||
|
||||
fn paint(&mut self, .., styled_texts: &mut Vec<StyledText>, ..) {
|
||||
// Just use pre-allocated styled_texts
|
||||
for text in styled_texts {
|
||||
text.paint(..);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Bad:** Allocate in `paint` phase
|
||||
|
||||
```rust
|
||||
fn paint(&mut self, ..) {
|
||||
// Bad: Allocation in paint phase!
|
||||
let styled_texts: Vec<_> = self.children
|
||||
.iter()
|
||||
.map(|child| StyledText::new(child.text.clone()))
|
||||
.collect();
|
||||
}
|
||||
```
|
||||
|
||||
### Cache Expensive Computations
|
||||
|
||||
Use memoization for expensive operations:
|
||||
|
||||
```rust
|
||||
pub struct CachedElement {
|
||||
// Cache key
|
||||
last_text: Option<SharedString>,
|
||||
last_width: Option<Pixels>,
|
||||
|
||||
// Cached result
|
||||
cached_layout: Option<TextLayout>,
|
||||
}
|
||||
|
||||
impl Element for CachedElement {
|
||||
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
|
||||
-> (LayoutId, TextLayout)
|
||||
{
|
||||
let current_width = window.bounds().width();
|
||||
|
||||
// Check if cache is valid
|
||||
if self.last_text.as_ref() != Some(&self.text)
|
||||
|| self.last_width != Some(current_width)
|
||||
|| self.cached_layout.is_none()
|
||||
{
|
||||
// Recompute expensive layout
|
||||
self.cached_layout = Some(self.compute_text_layout(current_width));
|
||||
self.last_text = Some(self.text.clone());
|
||||
self.last_width = Some(current_width);
|
||||
}
|
||||
|
||||
// Use cached layout
|
||||
let layout = self.cached_layout.as_ref().unwrap();
|
||||
(layout_id, layout.clone())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Lazy Child Rendering
|
||||
|
||||
Only render visible children in scrollable containers:
|
||||
|
||||
```rust
|
||||
fn paint(&mut self, .., bounds: Bounds<Pixels>, paint_state: &mut Self::PrepaintState, ..) {
|
||||
for (i, child) in self.children.iter_mut().enumerate() {
|
||||
let child_bounds = paint_state.child_bounds[i];
|
||||
|
||||
// Only paint visible children
|
||||
if self.is_visible(&child_bounds, &bounds) {
|
||||
child.paint(..);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_visible(&self, child_bounds: &Bounds<Pixels>, container_bounds: &Bounds<Pixels>) -> bool {
|
||||
child_bounds.bottom() >= container_bounds.top() &&
|
||||
child_bounds.top() <= container_bounds.bottom()
|
||||
}
|
||||
```
|
||||
|
||||
## Interaction Handling
|
||||
|
||||
### Proper Event Bubbling
|
||||
|
||||
Always check phase and bounds before handling events:
|
||||
|
||||
```rust
|
||||
fn paint(&mut self, .., window: &mut Window, cx: &mut App) {
|
||||
window.on_mouse_event({
|
||||
let hitbox = self.hitbox.clone();
|
||||
move |event: &MouseDownEvent, phase, window, cx| {
|
||||
// Check phase first
|
||||
if !phase.bubble() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if event is within bounds
|
||||
if !hitbox.is_hovered(window) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle event
|
||||
self.handle_click(event);
|
||||
|
||||
// Stop propagation if handled
|
||||
cx.stop_propagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Don't forget:**
|
||||
- Check `phase.bubble()` or `phase.capture()` as appropriate
|
||||
- Check hitbox hover state or bounds
|
||||
- Call `cx.stop_propagation()` if you handle the event
|
||||
|
||||
### Hitbox Management
|
||||
|
||||
Create hitboxes in `prepaint` phase, not `paint`:
|
||||
|
||||
**Good:**
|
||||
|
||||
```rust
|
||||
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, window: &mut Window, ..) -> Hitbox {
|
||||
// Create hitbox in prepaint
|
||||
window.insert_hitbox(bounds, HitboxBehavior::Normal)
|
||||
}
|
||||
|
||||
fn paint(&mut self, .., hitbox: &mut Hitbox, window: &mut Window, ..) {
|
||||
// Use hitbox in paint
|
||||
window.set_cursor_style(CursorStyle::PointingHand, hitbox);
|
||||
}
|
||||
```
|
||||
|
||||
**Hitbox Behaviors:**
|
||||
|
||||
```rust
|
||||
// Normal: Blocks events from passing through
|
||||
HitboxBehavior::Normal
|
||||
|
||||
// Transparent: Allows events to pass through to elements below
|
||||
HitboxBehavior::Transparent
|
||||
```
|
||||
|
||||
### Cursor Style Guidelines
|
||||
|
||||
Set appropriate cursor styles for interactivity cues:
|
||||
|
||||
```rust
|
||||
// Text selection
|
||||
window.set_cursor_style(CursorStyle::IBeam, &hitbox);
|
||||
|
||||
// Clickable elements (desktop convention: use default, not pointing hand)
|
||||
window.set_cursor_style(CursorStyle::Arrow, &hitbox);
|
||||
|
||||
// Links (web convention: use pointing hand)
|
||||
window.set_cursor_style(CursorStyle::PointingHand, &hitbox);
|
||||
|
||||
// Resizable edges
|
||||
window.set_cursor_style(CursorStyle::ResizeLeftRight, &hitbox);
|
||||
```
|
||||
|
||||
**Desktop vs Web Convention:**
|
||||
- Desktop apps: Use `Arrow` for buttons
|
||||
- Web apps: Use `PointingHand` for links only
|
||||
|
||||
## Layout Strategies
|
||||
|
||||
### Fixed Size Elements
|
||||
|
||||
For elements with known, unchanging size:
|
||||
|
||||
```rust
|
||||
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App) -> (LayoutId, ()) {
|
||||
let layout_id = window.request_layout(
|
||||
Style {
|
||||
size: size(px(200.), px(100.)),
|
||||
..default()
|
||||
},
|
||||
vec![], // No children
|
||||
cx
|
||||
);
|
||||
(layout_id, ())
|
||||
}
|
||||
```
|
||||
|
||||
### Content-Based Sizing
|
||||
|
||||
For elements sized by their content:
|
||||
|
||||
```rust
|
||||
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
|
||||
-> (LayoutId, Size<Pixels>)
|
||||
{
|
||||
// Measure content
|
||||
let text_bounds = self.measure_text(window);
|
||||
let padding = px(16.);
|
||||
|
||||
let layout_id = window.request_layout(
|
||||
Style {
|
||||
size: size(
|
||||
text_bounds.width() + padding * 2.,
|
||||
text_bounds.height() + padding * 2.,
|
||||
),
|
||||
..default()
|
||||
},
|
||||
vec![],
|
||||
cx
|
||||
);
|
||||
|
||||
(layout_id, text_bounds)
|
||||
}
|
||||
```
|
||||
|
||||
### Flexible Layouts
|
||||
|
||||
For elements that adapt to available space:
|
||||
|
||||
```rust
|
||||
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
|
||||
-> (LayoutId, Vec<LayoutId>)
|
||||
{
|
||||
let mut child_layout_ids = Vec::new();
|
||||
|
||||
for child in &mut self.children {
|
||||
let (layout_id, _) = child.request_layout(window, cx);
|
||||
child_layout_ids.push(layout_id);
|
||||
}
|
||||
|
||||
let layout_id = window.request_layout(
|
||||
Style {
|
||||
flex_direction: FlexDirection::Row,
|
||||
gap: px(8.),
|
||||
size: Size {
|
||||
width: relative(1.0), // Fill parent width
|
||||
height: auto(), // Auto height
|
||||
},
|
||||
..default()
|
||||
},
|
||||
child_layout_ids.clone(),
|
||||
cx
|
||||
);
|
||||
|
||||
(layout_id, child_layout_ids)
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Graceful Degradation
|
||||
|
||||
Handle errors gracefully, don't panic:
|
||||
|
||||
```rust
|
||||
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
|
||||
-> (LayoutId, Option<TextLayout>)
|
||||
{
|
||||
// Try to create styled text
|
||||
match StyledText::new(self.text.clone()).request_layout(None, None, window, cx) {
|
||||
Ok((layout_id, text_layout)) => {
|
||||
(layout_id, Some(text_layout))
|
||||
}
|
||||
Err(e) => {
|
||||
// Log error
|
||||
eprintln!("Failed to layout text: {}", e);
|
||||
|
||||
// Fallback to simple text
|
||||
let fallback_text = StyledText::new("(Error loading text)".into());
|
||||
let (layout_id, _) = fallback_text.request_layout(None, None, window, cx);
|
||||
(layout_id, None)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Defensive Bounds Checking
|
||||
|
||||
Always validate bounds and indices:
|
||||
|
||||
```rust
|
||||
fn paint_selection(&self, selection: &Selection, text_layout: &TextLayout, ..) {
|
||||
// Validate selection bounds
|
||||
let start = selection.start.min(self.text.len());
|
||||
let end = selection.end.min(self.text.len());
|
||||
|
||||
if start >= end {
|
||||
return; // Invalid selection
|
||||
}
|
||||
|
||||
let rects = text_layout.rects_for_range(start..end);
|
||||
// Paint selection...
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Element Implementations
|
||||
|
||||
### Layout Tests
|
||||
|
||||
Test that layout calculations are correct:
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_element_layout(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let mut window = cx.open_window(Default::default(), |_, _| ()).unwrap();
|
||||
|
||||
window.update(cx, |window, cx| {
|
||||
let mut element = MyElement::new();
|
||||
let (layout_id, layout_state) = element.request_layout(
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx
|
||||
);
|
||||
|
||||
// Assert layout properties
|
||||
let bounds = window.layout_bounds(layout_id);
|
||||
assert_eq!(bounds.size.width, px(200.));
|
||||
assert_eq!(bounds.size.height, px(100.));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Interaction Tests
|
||||
|
||||
Test that interactions work correctly:
|
||||
|
||||
```rust
|
||||
#[gpui::test]
|
||||
fn test_element_click(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let mut window = cx.open_window(Default::default(), |_, cx| {
|
||||
cx.new(|_| MyElement::new())
|
||||
}).unwrap();
|
||||
|
||||
window.update(cx, |window, cx| {
|
||||
let view = window.root_view().unwrap();
|
||||
|
||||
// Simulate click
|
||||
let position = point(px(10.), px(10.));
|
||||
window.dispatch_event(MouseDownEvent {
|
||||
position,
|
||||
button: MouseButton::Left,
|
||||
modifiers: Modifiers::default(),
|
||||
});
|
||||
|
||||
// Assert element responded
|
||||
view.read(cx).assert_clicked();
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### ❌ Storing Layout State in Element Struct
|
||||
|
||||
**Bad:**
|
||||
|
||||
```rust
|
||||
pub struct MyElement {
|
||||
id: ElementId,
|
||||
// Bad: This should be in RequestLayoutState
|
||||
cached_layout: Option<TextLayout>,
|
||||
}
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```rust
|
||||
pub struct MyElement {
|
||||
id: ElementId,
|
||||
text: SharedString,
|
||||
}
|
||||
|
||||
type RequestLayoutState = TextLayout; // Good: State in associated type
|
||||
```
|
||||
|
||||
### ❌ Mutating Element in Paint Phase
|
||||
|
||||
**Bad:**
|
||||
|
||||
```rust
|
||||
fn paint(&mut self, ..) {
|
||||
self.counter += 1; // Bad: Mutating element in paint
|
||||
}
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```rust
|
||||
fn paint(&mut self, .., window: &mut Window, cx: &mut App) {
|
||||
window.on_mouse_event(move |event, phase, window, cx| {
|
||||
if phase.bubble() {
|
||||
self.counter += 1;
|
||||
cx.notify(); // Schedule re-render
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Creating Hitboxes in Paint Phase
|
||||
|
||||
**Bad:**
|
||||
|
||||
```rust
|
||||
fn paint(&mut self, .., bounds: Bounds<Pixels>, window: &mut Window, ..) {
|
||||
// Bad: Creating hitbox in paint
|
||||
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
|
||||
}
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```rust
|
||||
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, window: &mut Window, ..) -> Hitbox {
|
||||
// Good: Creating hitbox in prepaint
|
||||
window.insert_hitbox(bounds, HitboxBehavior::Normal)
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Ignoring Event Phase
|
||||
|
||||
**Bad:**
|
||||
|
||||
```rust
|
||||
window.on_mouse_event(move |event, phase, window, cx| {
|
||||
// Bad: Not checking phase
|
||||
self.handle_click(event);
|
||||
});
|
||||
```
|
||||
|
||||
**Good:**
|
||||
|
||||
```rust
|
||||
window.on_mouse_event(move |event, phase, window, cx| {
|
||||
// Good: Checking phase
|
||||
if !phase.bubble() {
|
||||
return;
|
||||
}
|
||||
self.handle_click(event);
|
||||
});
|
||||
```
|
||||
|
||||
## Performance Checklist
|
||||
|
||||
Before shipping an element implementation, verify:
|
||||
|
||||
- [ ] No allocations in `paint` phase (except event handlers)
|
||||
- [ ] Expensive computations are cached/memoized
|
||||
- [ ] Only visible children are rendered in scrollable containers
|
||||
- [ ] Hitboxes created in `prepaint`, not `paint`
|
||||
- [ ] Event handlers check phase and bounds
|
||||
- [ ] Layout state is passed through associated types, not stored in element
|
||||
- [ ] Element implements proper error handling with fallbacks
|
||||
- [ ] Tests cover layout calculations and interactions
|
||||
632
.agents/skills/gpui-element/references/examples.md
Normal file
632
.agents/skills/gpui-element/references/examples.md
Normal file
@@ -0,0 +1,632 @@
|
||||
# Element Implementation Examples
|
||||
|
||||
Complete examples of implementing custom elements for various scenarios.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Simple Text Element](#simple-text-element)
|
||||
2. [Interactive Element with Selection](#interactive-element-with-selection)
|
||||
3. [Complex Element with Child Management](#complex-element-with-child-management)
|
||||
|
||||
## Simple Text Element
|
||||
|
||||
A basic text element with syntax highlighting support.
|
||||
|
||||
```rust
|
||||
pub struct SimpleText {
|
||||
id: ElementId,
|
||||
text: SharedString,
|
||||
highlights: Vec<(Range<usize>, HighlightStyle)>,
|
||||
}
|
||||
|
||||
impl IntoElement for SimpleText {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for SimpleText {
|
||||
type RequestLayoutState = StyledText;
|
||||
type PrepaintState = Hitbox;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
// Create styled text with highlights
|
||||
let mut runs = Vec::new();
|
||||
let mut ix = 0;
|
||||
|
||||
for (range, highlight) in &self.highlights {
|
||||
// Add unstyled text before highlight
|
||||
if ix < range.start {
|
||||
runs.push(window.text_style().to_run(range.start - ix));
|
||||
}
|
||||
|
||||
// Add highlighted text
|
||||
runs.push(
|
||||
window.text_style()
|
||||
.highlight(*highlight)
|
||||
.to_run(range.len())
|
||||
);
|
||||
ix = range.end;
|
||||
}
|
||||
|
||||
// Add remaining unstyled text
|
||||
if ix < self.text.len() {
|
||||
runs.push(window.text_style().to_run(self.text.len() - ix));
|
||||
}
|
||||
|
||||
let styled_text = StyledText::new(self.text.clone()).with_runs(runs);
|
||||
let (layout_id, _) = styled_text.request_layout(
|
||||
global_id,
|
||||
inspector_id,
|
||||
window,
|
||||
cx
|
||||
);
|
||||
|
||||
(layout_id, styled_text)
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
styled_text: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) -> Self::PrepaintState {
|
||||
// Prepaint the styled text
|
||||
styled_text.prepaint(
|
||||
global_id,
|
||||
inspector_id,
|
||||
bounds,
|
||||
&mut (),
|
||||
window,
|
||||
cx
|
||||
);
|
||||
|
||||
// Create hitbox for interaction
|
||||
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
|
||||
hitbox
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
styled_text: &mut Self::RequestLayoutState,
|
||||
hitbox: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) {
|
||||
// Paint the styled text
|
||||
styled_text.paint(
|
||||
global_id,
|
||||
inspector_id,
|
||||
bounds,
|
||||
&mut (),
|
||||
&mut (),
|
||||
window,
|
||||
cx
|
||||
);
|
||||
|
||||
// Set cursor style for text
|
||||
window.set_cursor_style(CursorStyle::IBeam, hitbox);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Interactive Element with Selection
|
||||
|
||||
A text element that supports text selection via mouse interaction.
|
||||
|
||||
```rust
|
||||
#[derive(Clone)]
|
||||
pub struct Selection {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
pub struct SelectableText {
|
||||
id: ElementId,
|
||||
text: SharedString,
|
||||
selectable: bool,
|
||||
selection: Option<Selection>,
|
||||
}
|
||||
|
||||
impl IntoElement for SelectableText {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for SelectableText {
|
||||
type RequestLayoutState = TextLayout;
|
||||
type PrepaintState = Option<Hitbox>;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let styled_text = StyledText::new(self.text.clone());
|
||||
let (layout_id, _) = styled_text.request_layout(
|
||||
global_id,
|
||||
inspector_id,
|
||||
window,
|
||||
cx
|
||||
);
|
||||
|
||||
// Extract text layout for selection painting
|
||||
let text_layout = styled_text.layout().clone();
|
||||
|
||||
(layout_id, text_layout)
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_global_id: Option<&GlobalElementId>,
|
||||
_inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_text_layout: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
_cx: &mut App
|
||||
) -> Self::PrepaintState {
|
||||
// Only create hitbox if selectable
|
||||
if self.selectable {
|
||||
Some(window.insert_hitbox(bounds, HitboxBehavior::Normal))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
text_layout: &mut Self::RequestLayoutState,
|
||||
hitbox: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) {
|
||||
// Paint text
|
||||
let styled_text = StyledText::new(self.text.clone());
|
||||
styled_text.paint(
|
||||
global_id,
|
||||
inspector_id,
|
||||
bounds,
|
||||
&mut (),
|
||||
&mut (),
|
||||
window,
|
||||
cx
|
||||
);
|
||||
|
||||
// Paint selection if any
|
||||
if let Some(selection) = &self.selection {
|
||||
Self::paint_selection(selection, text_layout, &bounds, window, cx);
|
||||
}
|
||||
|
||||
// Handle mouse events for selection
|
||||
if let Some(hitbox) = hitbox {
|
||||
window.set_cursor_style(CursorStyle::IBeam, hitbox);
|
||||
|
||||
// Mouse down to start selection
|
||||
window.on_mouse_event({
|
||||
let bounds = bounds.clone();
|
||||
move |event: &MouseDownEvent, phase, window, cx| {
|
||||
if bounds.contains(&event.position) && phase.bubble() {
|
||||
// Start selection at mouse position
|
||||
let char_index = Self::position_to_index(
|
||||
event.position,
|
||||
&bounds,
|
||||
text_layout
|
||||
);
|
||||
self.selection = Some(Selection {
|
||||
start: char_index,
|
||||
end: char_index,
|
||||
});
|
||||
cx.notify();
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Mouse drag to extend selection
|
||||
window.on_mouse_event({
|
||||
let bounds = bounds.clone();
|
||||
move |event: &MouseMoveEvent, phase, window, cx| {
|
||||
if let Some(selection) = &mut self.selection {
|
||||
if phase.bubble() {
|
||||
let char_index = Self::position_to_index(
|
||||
event.position,
|
||||
&bounds,
|
||||
text_layout
|
||||
);
|
||||
selection.end = char_index;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectableText {
|
||||
fn paint_selection(
|
||||
selection: &Selection,
|
||||
text_layout: &TextLayout,
|
||||
bounds: &Bounds<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) {
|
||||
// Calculate selection bounds from text layout
|
||||
let selection_rects = text_layout.rects_for_range(
|
||||
selection.start..selection.end
|
||||
);
|
||||
|
||||
// Paint selection background
|
||||
for rect in selection_rects {
|
||||
window.paint_quad(paint_quad(
|
||||
Bounds::new(
|
||||
point(bounds.left() + rect.origin.x, bounds.top() + rect.origin.y),
|
||||
rect.size
|
||||
),
|
||||
Corners::default(),
|
||||
cx.theme().selection_background,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn position_to_index(
|
||||
position: Point<Pixels>,
|
||||
bounds: &Bounds<Pixels>,
|
||||
text_layout: &TextLayout
|
||||
) -> usize {
|
||||
// Convert screen position to character index
|
||||
let relative_pos = point(
|
||||
position.x - bounds.left(),
|
||||
position.y - bounds.top()
|
||||
);
|
||||
text_layout.index_for_position(relative_pos)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Complex Element with Child Management
|
||||
|
||||
A container element that manages multiple children with scrolling support.
|
||||
|
||||
```rust
|
||||
pub struct ComplexElement {
|
||||
id: ElementId,
|
||||
children: Vec<Box<dyn Element<RequestLayoutState = (), PrepaintState = ()>>>,
|
||||
scrollable: bool,
|
||||
scroll_offset: Point<Pixels>,
|
||||
}
|
||||
|
||||
struct ComplexLayoutState {
|
||||
child_layouts: Vec<LayoutId>,
|
||||
total_height: Pixels,
|
||||
}
|
||||
|
||||
struct ComplexPaintState {
|
||||
child_bounds: Vec<Bounds<Pixels>>,
|
||||
hitbox: Hitbox,
|
||||
}
|
||||
|
||||
impl IntoElement for ComplexElement {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for ComplexElement {
|
||||
type RequestLayoutState = ComplexLayoutState;
|
||||
type PrepaintState = ComplexPaintState;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let mut child_layouts = Vec::new();
|
||||
let mut total_height = px(0.);
|
||||
|
||||
// Request layout for all children
|
||||
for child in &mut self.children {
|
||||
let (child_layout_id, _) = child.request_layout(
|
||||
global_id,
|
||||
inspector_id,
|
||||
window,
|
||||
cx
|
||||
);
|
||||
child_layouts.push(child_layout_id);
|
||||
|
||||
// Get child size from layout
|
||||
let child_size = window.layout_bounds(child_layout_id).size();
|
||||
total_height += child_size.height;
|
||||
}
|
||||
|
||||
// Create container layout
|
||||
let layout_id = window.request_layout(
|
||||
Style {
|
||||
flex_direction: FlexDirection::Column,
|
||||
gap: px(8.),
|
||||
size: Size {
|
||||
width: relative(1.0),
|
||||
height: if self.scrollable {
|
||||
// Fixed height for scrollable
|
||||
px(400.)
|
||||
} else {
|
||||
// Auto height for non-scrollable
|
||||
total_height
|
||||
},
|
||||
},
|
||||
..default()
|
||||
},
|
||||
child_layouts.clone(),
|
||||
cx
|
||||
);
|
||||
|
||||
(layout_id, ComplexLayoutState {
|
||||
child_layouts,
|
||||
total_height,
|
||||
})
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
layout_state: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) -> Self::PrepaintState {
|
||||
let mut child_bounds = Vec::new();
|
||||
let mut y_offset = self.scroll_offset.y;
|
||||
|
||||
// Calculate child bounds and prepaint children
|
||||
for (child, layout_id) in self.children.iter_mut()
|
||||
.zip(&layout_state.child_layouts)
|
||||
{
|
||||
let child_size = window.layout_bounds(*layout_id).size();
|
||||
let child_bound = Bounds::new(
|
||||
point(bounds.left(), bounds.top() + y_offset),
|
||||
child_size
|
||||
);
|
||||
|
||||
// Only prepaint visible children
|
||||
if self.is_visible(&child_bound, &bounds) {
|
||||
child.prepaint(
|
||||
global_id,
|
||||
inspector_id,
|
||||
child_bound,
|
||||
&mut (),
|
||||
window,
|
||||
cx
|
||||
);
|
||||
}
|
||||
|
||||
child_bounds.push(child_bound);
|
||||
y_offset += child_size.height + px(8.); // gap
|
||||
}
|
||||
|
||||
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
|
||||
|
||||
ComplexPaintState {
|
||||
child_bounds,
|
||||
hitbox,
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
global_id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
layout_state: &mut Self::RequestLayoutState,
|
||||
paint_state: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) {
|
||||
// Paint background
|
||||
window.paint_quad(paint_quad(
|
||||
bounds,
|
||||
Corners::all(px(4.)),
|
||||
cx.theme().background,
|
||||
));
|
||||
|
||||
// Paint visible children only
|
||||
for (i, child) in self.children.iter_mut().enumerate() {
|
||||
let child_bounds = paint_state.child_bounds[i];
|
||||
|
||||
if self.is_visible(&child_bounds, &bounds) {
|
||||
child.paint(
|
||||
global_id,
|
||||
inspector_id,
|
||||
child_bounds,
|
||||
&mut (),
|
||||
&mut (),
|
||||
window,
|
||||
cx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Paint scrollbar if scrollable
|
||||
if self.scrollable {
|
||||
self.paint_scrollbar(bounds, layout_state, window, cx);
|
||||
}
|
||||
|
||||
// Handle scroll events
|
||||
if self.scrollable {
|
||||
window.on_mouse_event({
|
||||
let hitbox = paint_state.hitbox.clone();
|
||||
let total_height = layout_state.total_height;
|
||||
let visible_height = bounds.size.height;
|
||||
|
||||
move |event: &ScrollWheelEvent, phase, window, cx| {
|
||||
if hitbox.is_hovered(window) && phase.bubble() {
|
||||
// Update scroll offset
|
||||
self.scroll_offset.y -= event.delta.y;
|
||||
|
||||
// Clamp scroll offset
|
||||
let max_scroll = (total_height - visible_height).max(px(0.));
|
||||
self.scroll_offset.y = self.scroll_offset.y
|
||||
.max(px(0.))
|
||||
.min(max_scroll);
|
||||
|
||||
cx.notify();
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ComplexElement {
|
||||
fn is_visible(&self, child_bounds: &Bounds<Pixels>, container_bounds: &Bounds<Pixels>) -> bool {
|
||||
// Check if child is within visible area
|
||||
child_bounds.bottom() >= container_bounds.top() &&
|
||||
child_bounds.top() <= container_bounds.bottom()
|
||||
}
|
||||
|
||||
fn paint_scrollbar(
|
||||
&self,
|
||||
bounds: Bounds<Pixels>,
|
||||
layout_state: &ComplexLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) {
|
||||
let scrollbar_width = px(8.);
|
||||
let visible_height = bounds.size.height;
|
||||
let total_height = layout_state.total_height;
|
||||
|
||||
if total_height <= visible_height {
|
||||
return; // No need for scrollbar
|
||||
}
|
||||
|
||||
// Calculate scrollbar position and size
|
||||
let scroll_ratio = self.scroll_offset.y / (total_height - visible_height);
|
||||
let thumb_height = (visible_height / total_height) * visible_height;
|
||||
let thumb_y = scroll_ratio * (visible_height - thumb_height);
|
||||
|
||||
// Paint scrollbar track
|
||||
let track_bounds = Bounds::new(
|
||||
point(bounds.right() - scrollbar_width, bounds.top()),
|
||||
size(scrollbar_width, visible_height)
|
||||
);
|
||||
window.paint_quad(paint_quad(
|
||||
track_bounds,
|
||||
Corners::default(),
|
||||
cx.theme().scrollbar_track,
|
||||
));
|
||||
|
||||
// Paint scrollbar thumb
|
||||
let thumb_bounds = Bounds::new(
|
||||
point(bounds.right() - scrollbar_width, bounds.top() + thumb_y),
|
||||
size(scrollbar_width, thumb_height)
|
||||
);
|
||||
window.paint_quad(paint_quad(
|
||||
thumb_bounds,
|
||||
Corners::all(px(4.)),
|
||||
cx.theme().scrollbar_thumb,
|
||||
));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Using SimpleText
|
||||
|
||||
```rust
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.child(SimpleText {
|
||||
id: ElementId::Name("code-text".into()),
|
||||
text: "fn main() { println!(\"Hello\"); }".into(),
|
||||
highlights: vec![
|
||||
(0..2, HighlightStyle::keyword()),
|
||||
(3..7, HighlightStyle::function()),
|
||||
],
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Using SelectableText
|
||||
|
||||
```rust
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.child(SelectableText {
|
||||
id: ElementId::Name("selectable-text".into()),
|
||||
text: "Select this text with your mouse".into(),
|
||||
selectable: true,
|
||||
selection: self.current_selection.clone(),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Using ComplexElement
|
||||
|
||||
```rust
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let children: Vec<Box<dyn Element<_, _>>> = self.items
|
||||
.iter()
|
||||
.map(|item| Box::new(div().child(item.name.clone())) as Box<_>)
|
||||
.collect();
|
||||
|
||||
div()
|
||||
.child(ComplexElement {
|
||||
id: ElementId::Name("scrollable-list".into()),
|
||||
children,
|
||||
scrollable: true,
|
||||
scroll_offset: self.scroll_offset,
|
||||
})
|
||||
}
|
||||
```
|
||||
509
.agents/skills/gpui-element/references/patterns.md
Normal file
509
.agents/skills/gpui-element/references/patterns.md
Normal file
@@ -0,0 +1,509 @@
|
||||
# Common Element Patterns
|
||||
|
||||
Reusable patterns for implementing common element types in GPUI.
|
||||
|
||||
## Text Rendering Elements
|
||||
|
||||
Elements that display and manipulate text content.
|
||||
|
||||
### Pattern Characteristics
|
||||
|
||||
- Use `StyledText` for text layout and rendering
|
||||
- Handle text selection in `paint` phase with hitbox interaction
|
||||
- Create hitboxes for text interaction in `prepaint`
|
||||
- Support text highlighting and custom styling via runs
|
||||
|
||||
### Implementation Template
|
||||
|
||||
```rust
|
||||
pub struct TextElement {
|
||||
id: ElementId,
|
||||
text: SharedString,
|
||||
style: TextStyle,
|
||||
}
|
||||
|
||||
impl Element for TextElement {
|
||||
type RequestLayoutState = StyledText;
|
||||
type PrepaintState = Hitbox;
|
||||
|
||||
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
|
||||
-> (LayoutId, StyledText)
|
||||
{
|
||||
let styled_text = StyledText::new(self.text.clone())
|
||||
.with_style(self.style);
|
||||
let (layout_id, _) = styled_text.request_layout(None, None, window, cx);
|
||||
(layout_id, styled_text)
|
||||
}
|
||||
|
||||
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, styled_text: &mut StyledText,
|
||||
window: &mut Window, cx: &mut App) -> Hitbox
|
||||
{
|
||||
styled_text.prepaint(None, None, bounds, &mut (), window, cx);
|
||||
window.insert_hitbox(bounds, HitboxBehavior::Normal)
|
||||
}
|
||||
|
||||
fn paint(&mut self, .., bounds: Bounds<Pixels>, styled_text: &mut StyledText,
|
||||
hitbox: &mut Hitbox, window: &mut Window, cx: &mut App)
|
||||
{
|
||||
styled_text.paint(None, None, bounds, &mut (), &mut (), window, cx);
|
||||
window.set_cursor_style(CursorStyle::IBeam, hitbox);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Use Cases
|
||||
|
||||
- Code editors with syntax highlighting
|
||||
- Rich text displays
|
||||
- Labels with custom formatting
|
||||
- Selectable text areas
|
||||
|
||||
## Container Elements
|
||||
|
||||
Elements that manage and layout child elements.
|
||||
|
||||
### Pattern Characteristics
|
||||
|
||||
- Manage child element layouts and positions
|
||||
- Handle scrolling and clipping when needed
|
||||
- Implement flex/grid-like layouts
|
||||
- Coordinate child interactions and event delegation
|
||||
|
||||
### Implementation Template
|
||||
|
||||
```rust
|
||||
pub struct ContainerElement {
|
||||
id: ElementId,
|
||||
children: Vec<AnyElement>,
|
||||
direction: FlexDirection,
|
||||
gap: Pixels,
|
||||
}
|
||||
|
||||
impl Element for ContainerElement {
|
||||
type RequestLayoutState = Vec<LayoutId>;
|
||||
type PrepaintState = Vec<Bounds<Pixels>>;
|
||||
|
||||
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
|
||||
-> (LayoutId, Vec<LayoutId>)
|
||||
{
|
||||
let child_layout_ids: Vec<_> = self.children
|
||||
.iter_mut()
|
||||
.map(|child| child.request_layout(window, cx).0)
|
||||
.collect();
|
||||
|
||||
let layout_id = window.request_layout(
|
||||
Style {
|
||||
flex_direction: self.direction,
|
||||
gap: self.gap,
|
||||
..default()
|
||||
},
|
||||
child_layout_ids.clone(),
|
||||
cx
|
||||
);
|
||||
|
||||
(layout_id, child_layout_ids)
|
||||
}
|
||||
|
||||
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, layout_ids: &mut Vec<LayoutId>,
|
||||
window: &mut Window, cx: &mut App) -> Vec<Bounds<Pixels>>
|
||||
{
|
||||
let mut child_bounds = Vec::new();
|
||||
|
||||
for (child, layout_id) in self.children.iter_mut().zip(layout_ids.iter()) {
|
||||
let child_bound = window.layout_bounds(*layout_id);
|
||||
child.prepaint(child_bound, window, cx);
|
||||
child_bounds.push(child_bound);
|
||||
}
|
||||
|
||||
child_bounds
|
||||
}
|
||||
|
||||
fn paint(&mut self, .., child_bounds: &mut Vec<Bounds<Pixels>>,
|
||||
window: &mut Window, cx: &mut App)
|
||||
{
|
||||
for (child, bounds) in self.children.iter_mut().zip(child_bounds.iter()) {
|
||||
child.paint(*bounds, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Use Cases
|
||||
|
||||
- Panels and split views
|
||||
- List containers
|
||||
- Grid layouts
|
||||
- Tab containers
|
||||
|
||||
## Interactive Elements
|
||||
|
||||
Elements that respond to user input (mouse, keyboard, touch).
|
||||
|
||||
### Pattern Characteristics
|
||||
|
||||
- Create appropriate hitboxes for interaction areas
|
||||
- Handle mouse/keyboard/touch events properly
|
||||
- Manage focus and cursor styles
|
||||
- Support hover, active, and disabled states
|
||||
|
||||
### Implementation Template
|
||||
|
||||
```rust
|
||||
pub struct InteractiveElement {
|
||||
id: ElementId,
|
||||
content: AnyElement,
|
||||
on_click: Option<Box<dyn Fn(&MouseUpEvent, &mut Window, &mut App)>>,
|
||||
hover_style: Option<Style>,
|
||||
}
|
||||
|
||||
impl Element for InteractiveElement {
|
||||
type RequestLayoutState = LayoutId;
|
||||
type PrepaintState = (Hitbox, bool); // hitbox and is_hovered
|
||||
|
||||
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
|
||||
-> (LayoutId, LayoutId)
|
||||
{
|
||||
let (content_layout, _) = self.content.request_layout(window, cx);
|
||||
(content_layout, content_layout)
|
||||
}
|
||||
|
||||
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, content_layout: &mut LayoutId,
|
||||
window: &mut Window, cx: &mut App) -> (Hitbox, bool)
|
||||
{
|
||||
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
|
||||
let is_hovered = hitbox.is_hovered(window);
|
||||
|
||||
self.content.prepaint(bounds, window, cx);
|
||||
|
||||
(hitbox, is_hovered)
|
||||
}
|
||||
|
||||
fn paint(&mut self, .., bounds: Bounds<Pixels>, content_layout: &mut LayoutId,
|
||||
prepaint: &mut (Hitbox, bool), window: &mut Window, cx: &mut App)
|
||||
{
|
||||
let (hitbox, is_hovered) = prepaint;
|
||||
|
||||
// Paint hover background if hovered
|
||||
if *is_hovered {
|
||||
if let Some(hover_style) = &self.hover_style {
|
||||
window.paint_quad(paint_quad(
|
||||
bounds,
|
||||
Corners::all(px(4.)),
|
||||
hover_style.background_color.unwrap_or(cx.theme().hover),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Paint content
|
||||
self.content.paint(bounds, window, cx);
|
||||
|
||||
// Handle click
|
||||
if let Some(on_click) = self.on_click.as_ref() {
|
||||
window.on_mouse_event({
|
||||
let on_click = on_click.clone();
|
||||
let hitbox = hitbox.clone();
|
||||
move |event: &MouseUpEvent, phase, window, cx| {
|
||||
if hitbox.is_hovered(window) && phase.bubble() {
|
||||
on_click(event, window, cx);
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Set cursor style
|
||||
window.set_cursor_style(CursorStyle::PointingHand, hitbox);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Use Cases
|
||||
|
||||
- Buttons
|
||||
- Links
|
||||
- Clickable cards
|
||||
- Drag handles
|
||||
- Menu items
|
||||
|
||||
## Composite Elements
|
||||
|
||||
Elements that combine multiple child elements with complex coordination.
|
||||
|
||||
### Pattern Characteristics
|
||||
|
||||
- Combine multiple child elements with different types
|
||||
- Manage complex state across children
|
||||
- Coordinate animations and transitions
|
||||
- Handle focus delegation between children
|
||||
|
||||
### Implementation Template
|
||||
|
||||
```rust
|
||||
pub struct CompositeElement {
|
||||
id: ElementId,
|
||||
header: AnyElement,
|
||||
content: AnyElement,
|
||||
footer: Option<AnyElement>,
|
||||
}
|
||||
|
||||
struct CompositeLayoutState {
|
||||
header_layout: LayoutId,
|
||||
content_layout: LayoutId,
|
||||
footer_layout: Option<LayoutId>,
|
||||
}
|
||||
|
||||
struct CompositePaintState {
|
||||
header_bounds: Bounds<Pixels>,
|
||||
content_bounds: Bounds<Pixels>,
|
||||
footer_bounds: Option<Bounds<Pixels>>,
|
||||
}
|
||||
|
||||
impl Element for CompositeElement {
|
||||
type RequestLayoutState = CompositeLayoutState;
|
||||
type PrepaintState = CompositePaintState;
|
||||
|
||||
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
|
||||
-> (LayoutId, CompositeLayoutState)
|
||||
{
|
||||
let (header_layout, _) = self.header.request_layout(window, cx);
|
||||
let (content_layout, _) = self.content.request_layout(window, cx);
|
||||
let footer_layout = self.footer.as_mut()
|
||||
.map(|f| f.request_layout(window, cx).0);
|
||||
|
||||
let mut children = vec![header_layout, content_layout];
|
||||
if let Some(footer) = footer_layout {
|
||||
children.push(footer);
|
||||
}
|
||||
|
||||
let layout_id = window.request_layout(
|
||||
Style {
|
||||
flex_direction: FlexDirection::Column,
|
||||
size: Size {
|
||||
width: relative(1.0),
|
||||
height: auto(),
|
||||
},
|
||||
..default()
|
||||
},
|
||||
children,
|
||||
cx
|
||||
);
|
||||
|
||||
(layout_id, CompositeLayoutState {
|
||||
header_layout,
|
||||
content_layout,
|
||||
footer_layout,
|
||||
})
|
||||
}
|
||||
|
||||
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, layout: &mut CompositeLayoutState,
|
||||
window: &mut Window, cx: &mut App) -> CompositePaintState
|
||||
{
|
||||
let header_bounds = window.layout_bounds(layout.header_layout);
|
||||
let content_bounds = window.layout_bounds(layout.content_layout);
|
||||
let footer_bounds = layout.footer_layout
|
||||
.map(|id| window.layout_bounds(id));
|
||||
|
||||
self.header.prepaint(header_bounds, window, cx);
|
||||
self.content.prepaint(content_bounds, window, cx);
|
||||
if let (Some(footer), Some(bounds)) = (&mut self.footer, footer_bounds) {
|
||||
footer.prepaint(bounds, window, cx);
|
||||
}
|
||||
|
||||
CompositePaintState {
|
||||
header_bounds,
|
||||
content_bounds,
|
||||
footer_bounds,
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&mut self, .., paint_state: &mut CompositePaintState,
|
||||
window: &mut Window, cx: &mut App)
|
||||
{
|
||||
self.header.paint(paint_state.header_bounds, window, cx);
|
||||
self.content.paint(paint_state.content_bounds, window, cx);
|
||||
if let (Some(footer), Some(bounds)) = (&mut self.footer, paint_state.footer_bounds) {
|
||||
footer.paint(bounds, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Use Cases
|
||||
|
||||
- Dialog boxes (header + content + footer)
|
||||
- Cards with multiple sections
|
||||
- Form layouts
|
||||
- Panels with toolbars
|
||||
|
||||
## Scrollable Elements
|
||||
|
||||
Elements with scrollable content areas.
|
||||
|
||||
### Pattern Characteristics
|
||||
|
||||
- Manage scroll state (offset, velocity)
|
||||
- Handle scroll events (wheel, drag, touch)
|
||||
- Paint scrollbars (track and thumb)
|
||||
- Clip content to visible area
|
||||
|
||||
### Implementation Template
|
||||
|
||||
```rust
|
||||
pub struct ScrollableElement {
|
||||
id: ElementId,
|
||||
content: AnyElement,
|
||||
scroll_offset: Point<Pixels>,
|
||||
content_size: Size<Pixels>,
|
||||
}
|
||||
|
||||
struct ScrollPaintState {
|
||||
hitbox: Hitbox,
|
||||
visible_bounds: Bounds<Pixels>,
|
||||
}
|
||||
|
||||
impl Element for ScrollableElement {
|
||||
type RequestLayoutState = (LayoutId, Size<Pixels>);
|
||||
type PrepaintState = ScrollPaintState;
|
||||
|
||||
fn request_layout(&mut self, .., window: &mut Window, cx: &mut App)
|
||||
-> (LayoutId, (LayoutId, Size<Pixels>))
|
||||
{
|
||||
let (content_layout, _) = self.content.request_layout(window, cx);
|
||||
let content_size = window.layout_bounds(content_layout).size;
|
||||
|
||||
let layout_id = window.request_layout(
|
||||
Style {
|
||||
size: Size {
|
||||
width: relative(1.0),
|
||||
height: px(400.), // Fixed viewport height
|
||||
},
|
||||
overflow: Overflow::Hidden,
|
||||
..default()
|
||||
},
|
||||
vec![content_layout],
|
||||
cx
|
||||
);
|
||||
|
||||
(layout_id, (content_layout, content_size))
|
||||
}
|
||||
|
||||
fn prepaint(&mut self, .., bounds: Bounds<Pixels>, layout: &mut (LayoutId, Size<Pixels>),
|
||||
window: &mut Window, cx: &mut App) -> ScrollPaintState
|
||||
{
|
||||
let (content_layout, content_size) = layout;
|
||||
|
||||
// Calculate content bounds with scroll offset
|
||||
let content_bounds = Bounds::new(
|
||||
point(bounds.left(), bounds.top() - self.scroll_offset.y),
|
||||
*content_size
|
||||
);
|
||||
|
||||
self.content.prepaint(content_bounds, window, cx);
|
||||
|
||||
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
|
||||
|
||||
ScrollPaintState {
|
||||
hitbox,
|
||||
visible_bounds: bounds,
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&mut self, .., layout: &mut (LayoutId, Size<Pixels>),
|
||||
paint_state: &mut ScrollPaintState, window: &mut Window, cx: &mut App)
|
||||
{
|
||||
let (_, content_size) = layout;
|
||||
|
||||
// Paint content
|
||||
self.content.paint(paint_state.visible_bounds, window, cx);
|
||||
|
||||
// Paint scrollbar
|
||||
self.paint_scrollbar(paint_state.visible_bounds, *content_size, window, cx);
|
||||
|
||||
// Handle scroll events
|
||||
window.on_mouse_event({
|
||||
let hitbox = paint_state.hitbox.clone();
|
||||
let content_height = content_size.height;
|
||||
let visible_height = paint_state.visible_bounds.size.height;
|
||||
|
||||
move |event: &ScrollWheelEvent, phase, window, cx| {
|
||||
if hitbox.is_hovered(window) && phase.bubble() {
|
||||
// Update scroll offset
|
||||
self.scroll_offset.y -= event.delta.y;
|
||||
|
||||
// Clamp to valid range
|
||||
let max_scroll = (content_height - visible_height).max(px(0.));
|
||||
self.scroll_offset.y = self.scroll_offset.y
|
||||
.max(px(0.))
|
||||
.min(max_scroll);
|
||||
|
||||
cx.notify();
|
||||
cx.stop_propagation();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollableElement {
|
||||
fn paint_scrollbar(
|
||||
&self,
|
||||
bounds: Bounds<Pixels>,
|
||||
content_size: Size<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut App
|
||||
) {
|
||||
let visible_height = bounds.size.height;
|
||||
let content_height = content_size.height;
|
||||
|
||||
if content_height <= visible_height {
|
||||
return; // No scrollbar needed
|
||||
}
|
||||
|
||||
let scrollbar_width = px(8.);
|
||||
|
||||
// Calculate thumb position and size
|
||||
let scroll_ratio = self.scroll_offset.y / (content_height - visible_height);
|
||||
let thumb_height = (visible_height / content_height) * visible_height;
|
||||
let thumb_y = scroll_ratio * (visible_height - thumb_height);
|
||||
|
||||
// Paint track
|
||||
window.paint_quad(paint_quad(
|
||||
Bounds::new(
|
||||
point(bounds.right() - scrollbar_width, bounds.top()),
|
||||
size(scrollbar_width, visible_height)
|
||||
),
|
||||
Corners::default(),
|
||||
cx.theme().scrollbar_track,
|
||||
));
|
||||
|
||||
// Paint thumb
|
||||
window.paint_quad(paint_quad(
|
||||
Bounds::new(
|
||||
point(bounds.right() - scrollbar_width, bounds.top() + thumb_y),
|
||||
size(scrollbar_width, thumb_height)
|
||||
),
|
||||
Corners::all(px(4.)),
|
||||
cx.theme().scrollbar_thumb,
|
||||
));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Use Cases
|
||||
|
||||
- Scrollable lists
|
||||
- Code editors with large files
|
||||
- Long-form text content
|
||||
- Image galleries
|
||||
|
||||
## Pattern Selection Guide
|
||||
|
||||
| Need | Pattern | Complexity |
|
||||
|------|---------|------------|
|
||||
| Display styled text | Text Rendering | Low |
|
||||
| Layout multiple children | Container | Low-Medium |
|
||||
| Handle clicks/hovers | Interactive | Medium |
|
||||
| Complex multi-part UI | Composite | Medium-High |
|
||||
| Large content with scrolling | Scrollable | High |
|
||||
|
||||
Choose the simplest pattern that meets your requirements, then extend as needed.
|
||||
Reference in New Issue
Block a user