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

18 KiB

Element Implementation Examples

Complete examples of implementing custom elements for various scenarios.

Table of Contents

  1. Simple Text Element
  2. Interactive Element with Selection
  3. Complex Element with Child Management

Simple Text Element

A basic text element with syntax highlighting support.

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.

#[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.

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

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

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

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,
        })
}