Files
coop/.agents/skills/gpui-element/references/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

14 KiB

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

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

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

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

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

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.