Files
coop/.agents/skills/gpui-element/references/advanced-patterns.md
Ren Amamiya 40d726c986 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>
2026-03-10 08:19:02 +00:00

20 KiB

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)

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

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

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

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.

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.

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.

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.