Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Drag & Drop Example

Live Demo

Source Code

#![cfg_attr(target_family = "wasm", no_main)]

use gpui::{hsla, px, size, App, Bounds, Hsla, WindowBounds, WindowOptions};
use vgui::for_each;
use vgui::prelude::*;

#[cfg(not(target_family = "wasm"))]
use gpui_platform::application;

#[cfg(target_family = "wasm")]
use gpui_platform::single_threaded_web;

// ── Drag value types ─────────────────────────────────────────────────

/// A draggable color card. `Clone + Render` lets the default drag
/// preview constructor work without an explicit `drag:preview={...}`.
#[derive(Clone, Copy, PartialEq)]
struct DragItem {
    id: usize,
    color: Hsla,
}

impl Render for DragItem {
    fn render(&mut self, _window: &mut gpui::Window, _cx: &mut gpui::Context<Self>) -> impl gpui::IntoElement {
        let color = self.color;
        let id = self.id;
        view! {
            <div class="w-20 h-20 rounded-lg border-2 flex items-center justify-center text-white text-sm"
                 style={vgui::Css::new(move |s| {
                    s.background = Some(color.into());
                    s.border_color = Some(hsla(0.0, 0.0, 1.0, 0.5).into());
                })}>
                {format!("#{}", id)}
            </div>
        }
    }
}

/// Drag value for tab reordering. Implements `Render` so the default
/// drag preview works.
#[derive(Clone, Copy)]
struct TabDrag {
    ix: usize,
}

impl Render for TabDrag {
    fn render(&mut self, _window: &mut gpui::Window, _cx: &mut gpui::Context<Self>) -> impl gpui::IntoElement {
        view! {
            <div class="px-3 py-1 rounded bg-[#45475a] text-white text-xs">
                {"move tab"}
            </div>
        }
    }
}

// ── App ──────────────────────────────────────────────────────────────

fn app() -> impl gpui::IntoElement {
    // Last dropped color card.
    let (dropped, set_dropped) = create_signal::<Option<DragItem>>(None);

    // Dropped file paths (OS file drag — native only).
    let (files, set_files) = create_signal::<Vec<String>>(Vec::new());

    // Tab reordering.
    let tabs = vec!["Inbox", "Drafts", "Sent", "Archive", "Spam"];
    let (tab_order, set_tab_order) = create_signal::<Vec<usize>>((0..tabs.len()).collect());

    let colors = [
        hsla(0.0, 0.7, 0.5, 1.0),       // red
        hsla(0.33, 0.7, 0.5, 1.0),      // green
        hsla(0.58, 0.7, 0.5, 1.0),      // blue
    ];

    let drop_border = dropped.get().map(|d| d.color).unwrap_or(hsla(0.0, 0.0, 0.5, 1.0));
    let drop_bg = dropped.get().map(|d| hsla(d.color.h, d.color.s, d.color.l, 0.15)).unwrap_or(hsla(0.0, 0.0, 0.0, 0.0));

    view! {
        <div class="flex flex-col gap-4 p-4 bg-[#1e1e2e] w-full h-full text-white overflow-y-auto">
            // ── Draggable color cards ───────────────────────────────
            <div class="flex gap-3">
                {for_each(colors, move |color, ix| {
                    let id = ix + 1;
                    view! {
                        <div drag={DragItem { id, color }}
                             class="w-20 h-20 rounded-lg border-2 flex items-center justify-center text-white text-sm cursor-grab"
                             style={vgui::Css::new(move |s| {
                                s.background = Some(color.into());
                                s.border_color = Some(hsla(0.0, 0.0, 1.0, 0.5).into());
                            })}>
                            {format!("#{}", id)}
                        </div>
                    }
                })}
            </div>

            // ── Drop target ─────────────────────────────────────────
            <div
                class="h-32 rounded-lg border-2 border-dashed flex items-center justify-center text-lg"
                style={vgui::Css::new(move |s| {
                    s.border_color = Some(drop_border.into());
                    s.background = Some(drop_bg.into());
                })}
                on:drop={move |item: &DragItem, _window, cx| {
                    set_dropped.update(cx, |_| Some(*item));
                }}
            >
                {if let Some(d) = dropped.get() {
                    format!("Dropped #{}", d.id)
                } else {
                    "Drop a card here".to_string()
                }}
            </div>

            // ── File drop zone (OS file drag — native only) ─────────
            <div
                class="h-24 rounded-lg border-2 border-dashed border-[#666] flex flex-col items-center justify-center text-sm text-[#aaa] p-2"
                on:drop={move |paths: &ExternalPaths, _window, cx| {
                    set_files.update(cx, |_| paths.paths().iter().map(|p| p.display().to_string()).collect::<Vec<_>>());
                }}
            >
                {if files.get().is_empty() {
                    "Drop files from your file manager here".to_string()
                } else {
                    files.get().join("\n")
                }}
            </div>

            // ── Tab bar with drag reordering ────────────────────────
            <div class="flex gap-1 bg-[#181825] rounded-lg p-1">
                {for_each(tab_order.get(), move |ix, _| {
                    let label = tabs[ix].to_string();
                    let set_tab_order = set_tab_order.clone();
                    view! {
                        <div drag={TabDrag { ix }}
                             on:drop={move |d: &TabDrag, _w, cx| {
                                 set_tab_order.update(cx, |order| {
                                     let from = d.ix;
                                     let to = ix;
                                     if from != to {
                                         let item = order.remove(from);
                                         order.insert(to, item);
                                     }
                                 });
                             }}
                             class="px-4 py-2 rounded bg-[#313244] text-sm cursor-grab hover:bg-[#45475a]">
                            {label}
                        </div>
                    }
                })}
            </div>

            // ── Drag status indicator ───────────────────────────────
            <div class="text-sm text-[#888]">
                {format!("Drag active: {}", has_active_drag())}
            </div>
        </div>
    }
}

// ── Entry points ─────────────────────────────────────────────────────

fn run() {
    #[cfg(not(target_family = "wasm"))]
    let gpui_app = application();

    #[cfg(target_family = "wasm")]
    let gpui_app = single_threaded_web();

    let launch = |cx: &mut App| {
        let bounds = Bounds::centered(None, size(px(520.), px(560.0)), cx);
        cx.open_window(
            WindowOptions {
                window_bounds: Some(WindowBounds::Windowed(bounds)),
                ..Default::default()
            },
            |window, cx| vgui::mount(window, cx, app),
        )
        .unwrap();
    };

    #[cfg(not(target_family = "wasm"))]
    gpui_app.run(launch);

    #[cfg(target_family = "wasm")]
    std::mem::forget(gpui_app.run_embedded(launch));
}

#[cfg(not(target_family = "wasm"))]
fn main() {
    run();
}

#[cfg(target_family = "wasm")]
#[wasm_bindgen::prelude::wasm_bindgen(start)]
pub fn start() {
    gpui_platform::web_init();
    vgui::intercept_keyboard_events();
    run();
}

Key Concepts

drag={value} — starting a drag

Attach drag={value} to any element to make it draggable. When the user clicks and moves more than a few pixels, gpui starts a drag operation carrying the typed value. The element automatically gets an .id() so on_drag can register on the StatefulInteractiveElement.

#![allow(unused)]
fn main() {
<div drag={DragItem { id, color }} class="...">
    {format!("#{}", id)}
</div>
}

Default drag preview

When drag:preview is omitted, vgui generates a default preview constructor that clones the drag value and renders it. This works when the drag value type implements Clone + Render:

#![allow(unused)]
fn main() {
#[derive(Clone, Copy)]
struct DragItem { id: usize, color: Hsla }

impl Render for DragItem { ... }
}

For types that don’t impl Clone + Render, supply an explicit preview constructor:

#![allow(unused)]
fn main() {
<div drag={my_value} drag:preview={move |item, pos, window, cx| {
    cx.new(|_| MyPreview { value: item.clone() })
}} class="...">
}

on:drop — receiving drops

on:drop registers a drop listener. Rust infers the drag value type T from the closure’s first parameter type annotation. The listener fires only when the dropped value’s TypeId matches T.

#![allow(unused)]
fn main() {
<div on:drop={move |item: &DragItem, _window, cx| {
    set_dropped.update(cx, |_| Some(*item));
}} class="...">
}

OS file drops with ExternalPaths

For files dragged from the OS file manager, the drop handler receives &ExternalPaths. This works on native platforms; on WASM the handler compiles but never fires.

#![allow(unused)]
fn main() {
<div on:drop={move |paths: &ExternalPaths, _window, cx| {
    set_files.update(cx, |_| paths.paths().iter().map(|p| p.display().to_string()).collect());
}} class="...">
}

on:drag_move — tracking drag movement

on:drag_move fires while a drag of the matching type is moving over the element. The handler receives &DragMoveEvent<T>, which provides the mouse event, the element bounds, and access to the dragged item.

#![allow(unused)]
fn main() {
<div on:drag_move={move |e: &DragMoveEvent<TabDrag>, _w, _cx| {
    // e.bounds — element bounds
    // e.drag(_cx) — the dragged TabDrag value
}} class="...">
}

can_drop — drop validation

can_drop={predicate} controls whether a drop is allowed. The predicate receives &dyn Any, the window, and the app context, and returns bool.

#![allow(unused)]
fn main() {
<div can_drop={move |value, _window, _cx| {
    value.is::<DragItem>()
}} class="...">
}

has_active_drag() — imperative query

Check whether a drag operation is in progress from any reactive scope:

#![allow(unused)]
fn main() {
{format!("Drag active: {}", has_active_drag())}
}

For stop_active_drag and set_active_drag_cursor_style, call them directly on the App context from within event handlers where &mut Window is available:

#![allow(unused)]
fn main() {
on:click={move |_e, window, cx| {
    cx.stop_active_drag(window);
    cx.set_active_drag_cursor_style(gpui::CursorStyle::Default, window);
}}
}

Tab reordering

The tab bar demonstrates reordering via drag. Each tab carries a TabDrag { ix } value and accepts drops of the same type. On drop, the source index is removed and inserted at the target index:

#![allow(unused)]
fn main() {
<div drag={TabDrag { ix }}
     on:drop={move |d: &TabDrag, _w, cx| {
         set_tab_order.update(cx, |order| {
             let from = d.ix;
             let to = ix;
             if from != to {
                 let item = order.remove(from);
                 order.insert(to, item);
             }
         });
     }}
     class="...">
    {label}
</div>
}

Running

Native

cargo run -p vgui-drag-drop

Web (WASM)

cargo build --target wasm32-unknown-unknown -p vgui-drag-drop --release
wasm-bindgen --target web --out-dir examples/drag-drop/dist \
    --no-typescript target/wasm32-unknown-unknown/release/drag-drop.wasm
python3 scripts/serve_plain.py 8080 examples/drag-drop