Picking / Drag and Drop

Back to examples View in GitHub

Demonstrates drag and drop functionality using picking events.

use bevy::prelude::*;

#[derive(Component)]
struct DropArea;

#[derive(Component)]
struct DraggableButton;

#[derive(Component)]
struct GhostPreview;

#[derive(Component)]
struct DroppedElement;

const AREA_SIZE: f32 = 500.0;
const BUTTON_WIDTH: f32 = 150.0;
const BUTTON_HEIGHT: f32 = 50.0;
const ELEMENT_SIZE: f32 = 25.0;

fn main() {
    App::new()
        .add_plugins((DefaultPlugins, MeshPickingPlugin))
        .add_systems(Startup, setup)
        .run();
}

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    commands.spawn(Camera2d);

    commands
        .spawn((
            Node {
                width: Val::Percent(100.0),
                height: Val::Percent(100.0),
                align_items: AlignItems::Center,
                justify_content: JustifyContent::Start,
                ..default()
            },
            Pickable::IGNORE,
        ))
        .with_children(|parent| {
            parent
                .spawn((
                    DraggableButton,
                    Node {
                        width: Val::Px(BUTTON_WIDTH),
                        height: Val::Px(BUTTON_HEIGHT),
                        margin: UiRect::all(Val::Px(10.0)),
                        justify_content: JustifyContent::Center,
                        align_items: AlignItems::Center,
                        ..default()
                    },
                    BackgroundColor(Color::srgb(1.0, 0.0, 0.0)),
                ))
                .with_child((
                    Text::new("Drag from me"),
                    TextColor(Color::WHITE),
                    Pickable::IGNORE,
                ))
                .observe(
                    |mut event: On<Pointer<DragStart>>,
                     mut button_color: Single<&mut BackgroundColor, With<DraggableButton>>| {
                        button_color.0 = Color::srgb(1.0, 0.5, 0.0);
                        event.propagate(false);
                    },
                )
                .observe(
                    |mut event: On<Pointer<DragEnd>>,
                     mut button_color: Single<&mut BackgroundColor, With<DraggableButton>>| {
                        button_color.0 = Color::srgb(1.0, 0.0, 0.0);
                        event.propagate(false);
                    },
                );
        });

    commands
        .spawn((
            DropArea,
            Mesh2d(meshes.add(Rectangle::new(AREA_SIZE, AREA_SIZE))),
            MeshMaterial2d(materials.add(Color::srgb(0.1, 0.4, 0.1))),
            Transform::IDENTITY,
            children![(
                Text2d::new("Drop here"),
                TextFont::from_font_size(50.),
                TextColor(Color::BLACK),
                Pickable::IGNORE,
                Transform::from_translation(Vec3::Z),
            )],
        ))
        .observe(on_drag_enter)
        .observe(on_drag_over)
        .observe(on_drag_drop)
        .observe(on_drag_leave);
}

fn on_drag_enter(
    mut event: On<Pointer<DragEnter>>,
    button: Single<Entity, With<DraggableButton>>,
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    if event.dragged == *button {
        let Some(position) = event.hit.position else {
            return;
        };
        commands.spawn((
            GhostPreview,
            Mesh2d(meshes.add(Circle::new(ELEMENT_SIZE))),
            MeshMaterial2d(materials.add(Color::srgba(1.0, 1.0, 0.6, 0.5))),
            Transform::from_translation(position + 2. * Vec3::Z),
            Pickable::IGNORE,
        ));
        event.propagate(false);
    }
}

fn on_drag_over(
    mut event: On<Pointer<DragOver>>,
    button: Single<Entity, With<DraggableButton>>,
    mut ghost_transform: Single<&mut Transform, With<GhostPreview>>,
) {
    if event.dragged == *button {
        let Some(position) = event.hit.position else {
            return;
        };
        ghost_transform.translation = position;
        event.propagate(false);
    }
}

fn on_drag_drop(
    mut event: On<Pointer<DragDrop>>,
    button: Single<Entity, With<DraggableButton>>,
    mut commands: Commands,
    ghost: Single<Entity, With<GhostPreview>>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    if event.dropped == *button {
        commands.entity(*ghost).despawn();
        let Some(position) = event.hit.position else {
            return;
        };
        commands.spawn((
            DroppedElement,
            Mesh2d(meshes.add(Circle::new(ELEMENT_SIZE))),
            MeshMaterial2d(materials.add(Color::srgb(1.0, 1.0, 0.6))),
            Transform::from_translation(position + 2. * Vec3::Z),
            Pickable::IGNORE,
        ));
        event.propagate(false);
    }
}

fn on_drag_leave(
    mut event: On<Pointer<DragLeave>>,
    button: Single<Entity, With<DraggableButton>>,
    mut commands: Commands,
    ghost: Single<Entity, With<GhostPreview>>,
) {
    if event.dragged == *button {
        commands.entity(*ghost).despawn();
        event.propagate(false);
    }
}