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");
});
}