Skip to content

Conversation

@moe-moe-pupil
Copy link
Contributor

@moe-moe-pupil moe-moe-pupil commented Nov 15, 2025

Objective

Solution

  • Describe the solution used to achieve the objective above.
    • use the hit from hover_map rather than the hit in state.dragging_over

Testing

  • Did you test these changes? If so, how?
    • yes, tested this simple demo and examples/picking/debug_picking.rs and ui_drag_and_drop.rs on my mac.
  • Are there any parts that need more testing?
    • no, i think it's enough.
  • How can other people (reviewers) test your changes? Is there anything specific they need to know?
    • i pasted the code for testing at the bottom
  • If relevant, what platforms did you test these changes on, and are there any important ones you can't test?
    • mac, but it doesnt matter

Showcase

514759590-6bbf6631-89d6-4211-9078-d09b13fd3980.mp4
fix_DragDrop.mp4
Click to view showcase
use bevy::{
    prelude::*,
    window::WindowMode,
};

#[derive(Component)]
struct Area;

#[derive(Component)]
struct ExampleButton;

#[derive(Component)]
struct GhostElement;

#[derive(Component)]
struct Element;


const AREA_SIZE: f32 = 500.0;
const BUTTON_SIZE: i32 = 50;
const ELEMENT_SIZE: f32 = 25.0;

#[bevy_main]
pub fn main() -> AppExit {
    App::new()
        .add_plugins((
            DefaultPlugins
                .set(WindowPlugin {
                    primary_window: Some(Window {
                        mode: WindowMode::BorderlessFullscreen(MonitorSelection::Primary),
                        ..default()
                    }),
                    ..default()
                }),
            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: percent(100),
            height: percent(100),
            align_items: AlignItems::Center,
            justify_content: JustifyContent::Start,
            ..default()
        },
        Pickable::IGNORE,
    ))
        .with_children(|parent| {
            parent.spawn((
                ExampleButton,
                Button,
                Node {
                    width: px(BUTTON_SIZE),
                    height: px(BUTTON_SIZE),
                    margin: UiRect::all(px(10)),
                    border_radius: BorderRadius::MAX,
                    ..default()
                },
                BackgroundColor(Color::srgb(1.0, 0.0, 0.0)),
            ))
                .observe(|mut event: On<Pointer<DragStart>>, mut button_color: Single<&mut BackgroundColor, With<ExampleButton>>| {
                    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<ExampleButton>>| {
                    button_color.0 = Color::srgb(1.0, 0.0, 0.0);
                    event.propagate(false);
                });
        });


    commands.spawn((
        Area,
        Mesh2d(meshes.add(Rectangle::new(AREA_SIZE, AREA_SIZE))),
        MeshMaterial2d(materials.add(Color::srgb(0.1, 0.4, 0.1))),
        Transform::IDENTITY,
    ))
        .observe(on_enter)
        .observe(on_over)
        .observe(on_drop)
        .observe(on_leave);
}


fn on_enter(
    mut event: On<Pointer<DragEnter>>,
    button: Single<Entity, With<ExampleButton>>,
    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((
            GhostElement,
            Mesh2d(meshes.add(Circle::new(ELEMENT_SIZE))),
            MeshMaterial2d(materials.add(Color::srgba(1.0, 1.0, 0.6, 0.5))),
            Transform::from_translation(position),
            Pickable::IGNORE,
        ));
        event.propagate(false);
    }
}


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

 fn on_drop(
    mut event: On<Pointer<DragDrop>>,
    button: Single<Entity, With<ExampleButton>>,
    mut commands: Commands,
    ghost_element: Single<Entity, With<GhostElement>>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    if event.dropped == *button {
        println!("{:?}", event);
        commands.entity(*ghost_element).despawn();
        let Some(position) = event.hit.position else { return; };
        commands.spawn((
            Element,
            Mesh2d(meshes.add(Circle::new(ELEMENT_SIZE))),
            MeshMaterial2d(materials.add(Color::srgb(1.0, 1.0, 0.6))),
            Transform::from_translation(position),
            Pickable::IGNORE,
        ));
        event.propagate(false);
    }
}

fn on_leave(
    mut event: On<Pointer<DragLeave>>,
    button: Single<Entity, With<ExampleButton>>,
    mut commands: Commands,
    ghost_element: Single<Entity, With<GhostElement>>,
) {
    if event.dragged == *button {
        commands.entity(*ghost_element).despawn();
        event.propagate(false);
    }
}

previously, it returns the hit of DragEnter event
@github-actions
Copy link
Contributor

Welcome, new contributor!

Please make sure you've read our contributing guide and we look forward to reviewing your pull request shortly ✨

@alice-i-cecile alice-i-cecile added this to the 0.17.3 milestone Nov 16, 2025
@alice-i-cecile alice-i-cecile added C-Bug An unexpected or incorrect behavior D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Needs-Review Needs reviewer attention (from anyone!) to move forward A-Picking Pointing at and selecting objects of all sorts labels Nov 16, 2025
@alice-i-cecile
Copy link
Member

@MarkGordeychuk can I get your review here please? I'd love a quick test of your reproduction code.

@mockersf mockersf modified the milestones: 0.17.3, 0.17.4 Nov 17, 2025
Copy link
Contributor

@ickshonpe ickshonpe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works correctly, looks good to merge.

Maybe there's an underlying problem with how the hitdata is updated? But that could be looked at in a follow up.

for (drag_target, drag) in state.dragging.drain() {
// Emit DragDrop
for (dragged_over, hit) in state.dragging_over.iter() {
for (dragged_over, _) in state.dragging_over.iter() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the underlying problem further down than this, shouldn't the HitData in the dragging_over map be updated every time the pointer moves?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, i checked where we handle the PointerAction::Move, and update the hit in state.dragging_over there

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, im new to rust, i also noticed this but bc im afraid of calling .clone() many times, i didnt do it 😂

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep most of the time it's not worth worrying about cloning for simple types like Hitdata where it won't cause any heap allocations. Here in picking, the real work has already been done during hit detection. We should only be dispatching a tiny number of picking events per frame, so any difference isn't going to be measurable.

@ickshonpe
Copy link
Contributor

ickshonpe commented Nov 18, 2025

Also might be useful to add the bug demo here to the picking examples.

@moe-moe-pupil
Copy link
Contributor Author

Also might be useful to add the bug demo here to the picking examples.

agree, there are not that many examples in /picking, glad to add one, but @MarkGordeychuk made the test demo, i can add one later and add him as co-author for that commit

.flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.to_owned())))
.filter(|(hovered_entity, _)| *hovered_entity != *drag_target)
{
*state.dragging_over.get_mut(&hovered_entity).unwrap() = hit.clone();
Copy link
Contributor

@ickshonpe ickshonpe Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense now, this definitely seems like a more complete solution.

It's mostly just cosmetic, but you should be able to remove the second clone below on line 776:

                                DragOver {
                                    button,
                                    dragged: *drag_target,
                                    hit,
                                },

@ickshonpe
Copy link
Contributor

ickshonpe commented Nov 20, 2025

Also might be useful to add the bug demo here to the picking examples.

agree, there are not that many examples in /picking, glad to add one, but @MarkGordeychuk made the test demo, i can add one later and add him as co-author for that commit

Sounds good, I've added an issue about the example: #21894 .

@alice-i-cecile alice-i-cecile added this pull request to the merge queue Nov 25, 2025
Merged via the queue into bevyengine:main with commit c7f607a Nov 25, 2025
36 checks passed
@baozaolaoba-top baozaolaoba-top mentioned this pull request Nov 26, 2025
beicause pushed a commit to beicause/bevy that referenced this pull request Nov 26, 2025
# Objective

- previously, the hit field in DragDrop event is the clone of the
DragEnter one.
- Fixes bevyengine#21849.

## Solution

- Describe the solution used to achieve the objective above.
- use the hit from hover_map rather than the hit in state.dragging_over

## Testing

- Did you test these changes? If so, how?
- yes, tested this simple demo and examples/picking/debug_picking.rs and
ui_drag_and_drop.rs on my mac.
- Are there any parts that need more testing?
  - no, i think it's enough.
- How can other people (reviewers) test your changes? Is there anything
specific they need to know?
  - i pasted the code for testing at the bottom 
- If relevant, what platforms did you test these changes on, and are
there any important ones you can't test?
  - mac, but it doesnt matter
---

## Showcase



https://github.com/user-attachments/assets/ff35a03d-00a0-41b8-952f-3f2e77530d5a


https://github.com/user-attachments/assets/a7aa5172-f89c-4010-a153-b90171621204

<details>
  <summary>Click to view showcase</summary>

```rust
use bevy::{
    prelude::*,
    window::WindowMode,
};

#[derive(Component)]
struct Area;

#[derive(Component)]
struct ExampleButton;

#[derive(Component)]
struct GhostElement;

#[derive(Component)]
struct Element;


const AREA_SIZE: f32 = 500.0;
const BUTTON_SIZE: i32 = 50;
const ELEMENT_SIZE: f32 = 25.0;

#[bevy_main]
pub fn main() -> AppExit {
    App::new()
        .add_plugins((
            DefaultPlugins
                .set(WindowPlugin {
                    primary_window: Some(Window {
                        mode: WindowMode::BorderlessFullscreen(MonitorSelection::Primary),
                        ..default()
                    }),
                    ..default()
                }),
            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: percent(100),
            height: percent(100),
            align_items: AlignItems::Center,
            justify_content: JustifyContent::Start,
            ..default()
        },
        Pickable::IGNORE,
    ))
        .with_children(|parent| {
            parent.spawn((
                ExampleButton,
                Button,
                Node {
                    width: px(BUTTON_SIZE),
                    height: px(BUTTON_SIZE),
                    margin: UiRect::all(px(10)),
                    border_radius: BorderRadius::MAX,
                    ..default()
                },
                BackgroundColor(Color::srgb(1.0, 0.0, 0.0)),
            ))
                .observe(|mut event: On<Pointer<DragStart>>, mut button_color: Single<&mut BackgroundColor, With<ExampleButton>>| {
                    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<ExampleButton>>| {
                    button_color.0 = Color::srgb(1.0, 0.0, 0.0);
                    event.propagate(false);
                });
        });


    commands.spawn((
        Area,
        Mesh2d(meshes.add(Rectangle::new(AREA_SIZE, AREA_SIZE))),
        MeshMaterial2d(materials.add(Color::srgb(0.1, 0.4, 0.1))),
        Transform::IDENTITY,
    ))
        .observe(on_enter)
        .observe(on_over)
        .observe(on_drop)
        .observe(on_leave);
}


fn on_enter(
    mut event: On<Pointer<DragEnter>>,
    button: Single<Entity, With<ExampleButton>>,
    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((
            GhostElement,
            Mesh2d(meshes.add(Circle::new(ELEMENT_SIZE))),
            MeshMaterial2d(materials.add(Color::srgba(1.0, 1.0, 0.6, 0.5))),
            Transform::from_translation(position),
            Pickable::IGNORE,
        ));
        event.propagate(false);
    }
}


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

 fn on_drop(
    mut event: On<Pointer<DragDrop>>,
    button: Single<Entity, With<ExampleButton>>,
    mut commands: Commands,
    ghost_element: Single<Entity, With<GhostElement>>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    if event.dropped == *button {
        println!("{:?}", event);
        commands.entity(*ghost_element).despawn();
        let Some(position) = event.hit.position else { return; };
        commands.spawn((
            Element,
            Mesh2d(meshes.add(Circle::new(ELEMENT_SIZE))),
            MeshMaterial2d(materials.add(Color::srgb(1.0, 1.0, 0.6))),
            Transform::from_translation(position),
            Pickable::IGNORE,
        ));
        event.propagate(false);
    }
}

fn on_leave(
    mut event: On<Pointer<DragLeave>>,
    button: Single<Entity, With<ExampleButton>>,
    mut commands: Commands,
    ghost_element: Single<Entity, With<GhostElement>>,
) {
    if event.dragged == *button {
        commands.entity(*ghost_element).despawn();
        event.propagate(false);
    }
}
```

</details>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Picking Pointing at and selecting objects of all sorts C-Bug An unexpected or incorrect behavior D-Straightforward Simple bug fixes and API improvements, docs, test and examples S-Needs-Review Needs reviewer attention (from anyone!) to move forward

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Odd bevy_picking's DragDrop event hit position

5 participants