← Examples

Interactive Elements

This example demonstrates interactive patterns in GPUI: click events, hover states, mouse events, drag and drop.

Run

cargo run --example interactive_elements

Category

learn

Source

View on GitHub →

Source Code

//! Interactive Elements Example
//!
//! This example demonstrates interactive patterns in GPUI:
//!
//! 1. Click events - single click, double click, click count
//! 2. Hover states - hover styling and on_hover callbacks
//! 3. Mouse events - mouse down, mouse up, mouse move
//! 4. Drag and drop - draggable elements and drop targets

#[path = "../prelude.rs"]
mod example_prelude;

use example_prelude::init_example;
use gpui::{
    App, Application, Bounds, ClickEvent, Colors, Context, Entity, Half, Hsla, IntoElement,
    MouseButton, MouseMoveEvent, Pixels, Point, Render, Window, WindowBounds, WindowOptions, div,
    prelude::*, px, size,
};

// ============================================================================
// Click Events Demo
// ============================================================================
//
// Demonstrates different click interactions:
// - Single click
// - Double click (click_count == 2)
// - Click count tracking

struct ClickDemo {
    click_count: usize,
    last_click_type: String,
}

impl ClickDemo {
    fn new() -> Self {
        Self {
            click_count: 0,
            last_click_type: "None".to_string(),
        }
    }
}

impl Render for ClickDemo {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let colors = Colors::for_appearance(window);

        div()
            .flex()
            .flex_col()
            .gap_3()
            .p_4()
            .rounded_lg()
            .bg(colors.surface)
            .child(
                div()
                    .text_sm()
                    .font_weight(gpui::FontWeight::SEMIBOLD)
                    .text_color(colors.text)
                    .child("Click Events"),
            )
            .child(
                div()
                    .text_xs()
                    .text_color(colors.text_muted)
                    .child("Single click, double click, or triple click the button"),
            )
            .child(
                div()
                    .id("click-target")
                    .px_4()
                    .py_2()
                    .rounded_md()
                    .bg(colors.accent)
                    .text_color(colors.selected_text)
                    .text_sm()
                    .cursor_pointer()
                    .hover(|style| style.bg(colors.accent_hover))
                    .active(|style| style.bg(colors.accent_active))
                    .child("Click Me!")
                    // on_click receives a ClickEvent with click_count() method
                    .on_click(cx.listener(|this, event: &ClickEvent, _window, cx| {
                        this.click_count += 1;
                        this.last_click_type = match event.click_count() {
                            1 => "Single Click".to_string(),
                            2 => "Double Click".to_string(),
                            3 => "Triple Click".to_string(),
                            n => format!("{n}x Click"),
                        };
                        cx.notify();
                    })),
            )
            .child(
                div()
                    .flex()
                    .flex_col()
                    .gap_1()
                    .mt_2()
                    .child(
                        div()
                            .text_xs()
                            .text_color(colors.text_muted)
                            .child(format!("Total clicks: {}", self.click_count)),
                    )
                    .child(
                        div()
                            .text_xs()
                            .text_color(colors.text_muted)
                            .child(format!("Last: {}", self.last_click_type)),
                    ),
            )
    }
}

// ============================================================================
// Hover Demo
// ============================================================================
//
// Demonstrates hover interactions:
// - hover() style modifier for CSS-like hover states
// - on_hover() callback for programmatic hover detection

struct HoverDemo {
    is_hovered: bool,
    hover_count: usize,
}

impl HoverDemo {
    fn new() -> Self {
        Self {
            is_hovered: false,
            hover_count: 0,
        }
    }
}

impl Render for HoverDemo {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let colors = Colors::for_appearance(window);
        let is_hovered = self.is_hovered;

        div()
            .flex()
            .flex_col()
            .gap_3()
            .p_4()
            .rounded_lg()
            .bg(colors.surface)
            .child(
                div()
                    .text_sm()
                    .font_weight(gpui::FontWeight::SEMIBOLD)
                    .text_color(colors.text)
                    .child("Hover Events"),
            )
            .child(
                div()
                    .text_xs()
                    .text_color(colors.text_muted)
                    .child("Move your mouse in and out of the target"),
            )
            .child(
                div()
                    .id("hover-target")
                    .px_4()
                    .py_3()
                    .rounded_md()
                    .border_2()
                    .border_color(if is_hovered {
                        colors.accent
                    } else {
                        colors.border
                    })
                    .bg(if is_hovered {
                        colors.accent_hover
                    } else {
                        colors.surface_hover
                    })
                    .text_color(if is_hovered {
                        colors.selected_text
                    } else {
                        colors.text
                    })
                    .text_sm()
                    .cursor_pointer()
                    .child(if is_hovered {
                        "Mouse Inside!"
                    } else {
                        "Hover Over Me"
                    })
                    // on_hover callback receives a bool: true when mouse enters, false when it leaves
                    .on_hover(cx.listener(|this, &hovered, _window, cx| {
                        this.is_hovered = hovered;
                        if hovered {
                            this.hover_count += 1;
                        }
                        cx.notify();
                    })),
            )
            .child(
                div()
                    .text_xs()
                    .text_color(colors.text_muted)
                    .mt_2()
                    .child(format!("Times hovered: {}", self.hover_count)),
            )
    }
}

// ============================================================================
// Mouse Events Demo
// ============================================================================
//
// Demonstrates low-level mouse events:
// - on_mouse_down - fires when mouse button is pressed
// - on_mouse_up - fires when mouse button is released
// - on_mouse_move - fires when mouse moves over element

struct MouseEventsDemo {
    mouse_position: Option<Point<Pixels>>,
    is_pressed: bool,
    event_log: Vec<String>,
}

impl MouseEventsDemo {
    fn new() -> Self {
        Self {
            mouse_position: None,
            is_pressed: false,
            event_log: Vec::new(),
        }
    }

    fn log_event(&mut self, event: &str) {
        self.event_log.push(event.to_string());
        if self.event_log.len() > 5 {
            self.event_log.remove(0);
        }
    }
}

impl Render for MouseEventsDemo {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let colors = Colors::for_appearance(window);
        let is_pressed = self.is_pressed;
        let position_text = self
            .mouse_position
            .map(|p| format!("({:.0}, {:.0})", f32::from(p.x), f32::from(p.y)))
            .unwrap_or_else(|| "—".to_string());

        div()
            .flex()
            .flex_col()
            .gap_3()
            .p_4()
            .rounded_lg()
            .bg(colors.surface)
            .child(
                div()
                    .text_sm()
                    .font_weight(gpui::FontWeight::SEMIBOLD)
                    .text_color(colors.text)
                    .child("Mouse Events"),
            )
            .child(
                div()
                    .text_xs()
                    .text_color(colors.text_muted)
                    .child("Move and click within the target area"),
            )
            .child(
                div()
                    .id("mouse-events-target")
                    .h_20()
                    .rounded_md()
                    .border_2()
                    .border_color(if is_pressed {
                        colors.accent
                    } else {
                        colors.border
                    })
                    .bg(if is_pressed {
                        colors.accent_hover
                    } else {
                        colors.surface_hover
                    })
                    .flex()
                    .items_center()
                    .justify_center()
                    .text_sm()
                    .text_color(colors.text)
                    .child(format!("Position: {}", position_text))
                    .on_mouse_down(
                        MouseButton::Left,
                        cx.listener(|this, _event, _window, cx| {
                            this.is_pressed = true;
                            this.log_event("Mouse Down");
                            cx.notify();
                        }),
                    )
                    .on_mouse_up(
                        MouseButton::Left,
                        cx.listener(|this, _event, _window, cx| {
                            this.is_pressed = false;
                            this.log_event("Mouse Up");
                            cx.notify();
                        }),
                    )
                    .on_mouse_move(cx.listener(|this, event: &MouseMoveEvent, _window, cx| {
                        this.mouse_position = Some(event.position);
                        cx.notify();
                    })),
            )
            .child(
                div()
                    .flex()
                    .flex_col()
                    .gap_0p5()
                    .mt_2()
                    .text_xs()
                    .text_color(colors.text_muted)
                    .children(
                        self.event_log
                            .iter()
                            .map(|e| div().child(format!("• {}", e))),
                    ),
            )
    }
}

// ============================================================================
// Drag and Drop Demo
// ============================================================================
//
// Demonstrates drag and drop:
// - on_drag - makes an element draggable, provides drag data
// - on_drop - makes an element a drop target, receives drag data

#[derive(Clone, Copy)]
struct DragData {
    index: usize,
    color: Hsla,
    position: Point<Pixels>,
}

impl DragData {
    fn new(index: usize, color: Hsla) -> Self {
        Self {
            index,
            color,
            position: Point::default(),
        }
    }

    fn with_position(mut self, position: Point<Pixels>) -> Self {
        self.position = position;
        self
    }
}

// Render trait for DragData allows it to be rendered as drag feedback
impl Render for DragData {
    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
        let size = gpui::size(px(80.), px(40.));

        // Position the drag preview at the cursor
        div()
            .pl(self.position.x - size.width.half())
            .pt(self.position.y - size.height.half())
            .child(
                div()
                    .flex()
                    .justify_center()
                    .items_center()
                    .w(size.width)
                    .h(size.height)
                    .bg(self.color.opacity(0.8))
                    .text_color(gpui::white())
                    .text_xs()
                    .rounded_md()
                    .shadow_lg()
                    .child(format!("Item {}", self.index + 1)),
            )
    }
}

struct DragDropDemo {
    dropped_item: Option<DragData>,
}

impl DragDropDemo {
    fn new() -> Self {
        Self { dropped_item: None }
    }
}

impl Render for DragDropDemo {
    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let colors = Colors::for_appearance(window);
        let item_colors = [colors.error, colors.success, colors.warning];

        div()
            .flex()
            .flex_col()
            .gap_3()
            .p_4()
            .rounded_lg()
            .bg(colors.surface)
            .child(
                div()
                    .text_sm()
                    .font_weight(gpui::FontWeight::SEMIBOLD)
                    .text_color(colors.text)
                    .child("Drag and Drop"),
            )
            .child(
                div()
                    .text_xs()
                    .text_color(colors.text_muted)
                    .child("Drag items to the drop zone below"),
            )
            .child(
                div()
                    .flex()
                    .gap_2()
                    .children(item_colors.into_iter().enumerate().map(|(index, color)| {
                        let drag_data = DragData::new(index, color.into());

                        div()
                            .id(("drag-item", index))
                            .px_3()
                            .py_2()
                            .rounded_md()
                            .border_2()
                            .border_color(color)
                            .text_color(color)
                            .text_xs()
                            .cursor_grab()
                            .hover(move |style| {
                                let c: Hsla = color.into();
                                style.bg(c.opacity(0.1))
                            })
                            .child(format!("Item {}", index + 1))
                            // on_drag takes: drag data, and a closure that creates the drag preview
                            .on_drag(drag_data, |data: &DragData, position, _, cx| {
                                cx.new(|_| data.with_position(position))
                            })
                    })),
            )
            .child(
                div()
                    .id("drop-target")
                    .mt_2()
                    .h_16()
                    .rounded_md()
                    .border_2()
                    .border_dashed()
                    .border_color(
                        self.dropped_item
                            .map(|d| d.color)
                            .unwrap_or_else(|| colors.border.into()),
                    )
                    .when_some(self.dropped_item, |el, data| el.bg(data.color.opacity(0.2)))
                    .flex()
                    .items_center()
                    .justify_center()
                    .text_xs()
                    .text_color(colors.text_muted)
                    // on_drop receives the drag data when an item is dropped
                    .on_drop(cx.listener(|this, data: &DragData, _window, cx| {
                        this.dropped_item = Some(*data);
                        cx.notify();
                    }))
                    .child(
                        self.dropped_item
                            .map(|d| format!("Dropped: Item {}", d.index + 1))
                            .unwrap_or_else(|| "Drop Zone".to_string()),
                    ),
            )
    }
}

// ============================================================================
// Main Application
// ============================================================================

struct InteractiveElementsExample {
    click_demo: Entity<ClickDemo>,
    hover_demo: Entity<HoverDemo>,
    mouse_events_demo: Entity<MouseEventsDemo>,
    drag_drop_demo: Entity<DragDropDemo>,
}

impl InteractiveElementsExample {
    fn new(cx: &mut Context<Self>) -> Self {
        Self {
            click_demo: cx.new(|_| ClickDemo::new()),
            hover_demo: cx.new(|_| HoverDemo::new()),
            mouse_events_demo: cx.new(|_| MouseEventsDemo::new()),
            drag_drop_demo: cx.new(|_| DragDropDemo::new()),
        }
    }
}

impl Render for InteractiveElementsExample {
    fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
        let colors = Colors::for_appearance(window);

        div()
            .id("main")
            .size_full()
            .p_6()
            .bg(colors.background)
            .overflow_scroll()
            .child(
                div()
                    .flex()
                    .flex_col()
                    .gap_6()
                    .max_w(px(800.))
                    .child(
                        div()
                            .flex()
                            .flex_col()
                            .gap_1()
                            .child(
                                div()
                                    .text_xl()
                                    .font_weight(gpui::FontWeight::BOLD)
                                    .text_color(colors.text)
                                    .child("Interactive Elements"),
                            )
                            .child(
                                div()
                                    .text_sm()
                                    .text_color(colors.text_muted)
                                    .child("Click, hover, mouse events, and drag-and-drop in GPUI"),
                            ),
                    )
                    .child(
                        div()
                            .grid()
                            .grid_cols(2)
                            .gap_4()
                            .child(self.click_demo.clone())
                            .child(self.hover_demo.clone())
                            .child(self.mouse_events_demo.clone())
                            .child(self.drag_drop_demo.clone()),
                    ),
            )
    }
}

fn main() {
    Application::new().run(|cx: &mut App| {
        let bounds = Bounds::centered(None, size(px(700.), px(650.)), cx);
        cx.open_window(
            WindowOptions {
                window_bounds: Some(WindowBounds::Windowed(bounds)),
                ..Default::default()
            },
            |_, cx| cx.new(|cx| InteractiveElementsExample::new(cx)),
        )
        .expect("Failed to open window");

        init_example(cx, "Interactive Elements");
    });
}