Skip to content

Commit

Permalink
Add drag-and-drop APIs with payloads storage (#3887)
Browse files Browse the repository at this point in the history
* Closes #3882

This adds several methods to make drag-and-drop more ergonomic in egui.

In particular, egui can now keep track of _what_ is being dragged for
you (the _payload_).

Low-level:
* `egui::DragAndDrop` hold the payload during a drag

Mid-level:
* `Response::dnd_set_drag_payload` sets it for drag-sources
* `Response::dnd_hover_payload` and `Response::dnd_release_payload`
reads it for drop-targets

High-level:
* `ui.dnd_drag_source`: make a widget draggable
* `ui.dnd_drop_zone`: a container where things can be dropped

The drag-and-drop demo is now a lot simpler:


https://github.com/emilk/egui/blob/emilk/drag-and-drop/crates/egui_demo_lib/src/demo/drag_and_drop.rs

---------

Co-authored-by: Antoine Beyeler <[email protected]>
  • Loading branch information
emilk and abey79 authored Jan 29, 2024
1 parent 40ed503 commit abd028b
Show file tree
Hide file tree
Showing 6 changed files with 365 additions and 123 deletions.
1 change: 1 addition & 0 deletions crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,7 @@ impl Default for Context {
// Register built-in plugins:
crate::debug_text::register(&ctx);
crate::text_selection::LabelSelectionState::register(&ctx);
crate::DragAndDrop::register(&ctx);

ctx
}
Expand Down
125 changes: 125 additions & 0 deletions crates/egui/src/drag_and_drop.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
use std::{any::Any, sync::Arc};

use crate::{Context, CursorIcon, Id};

/// Tracking of drag-and-drop payload.
///
/// This is a low-level API.
///
/// For a higher-level API, see:
/// - [`crate::Ui::dnd_drag_source`]
/// - [`crate::Ui::dnd_drop_zone`]
/// - [`crate::Response::dnd_set_drag_payload`]
/// - [`crate::Response::dnd_hover_payload`]
/// - [`crate::Response::dnd_release_payload`]
///
/// See [this example](https://github.com/emilk/egui/blob/master/crates/egui_demo_lib/src/demo/drag_and_drop.rs).
#[doc(alias = "drag and drop")]
#[derive(Clone, Default)]
pub struct DragAndDrop {
/// If set, something is currently being dragged
payload: Option<Arc<dyn Any + Send + Sync>>,
}

impl DragAndDrop {
pub(crate) fn register(ctx: &Context) {
ctx.on_end_frame("debug_text", std::sync::Arc::new(Self::end_frame));
}

fn end_frame(ctx: &Context) {
let pointer_released = ctx.input(|i| i.pointer.any_released());

let mut is_dragging = false;

ctx.data_mut(|data| {
let state = data.get_temp_mut_or_default::<Self>(Id::NULL);

if pointer_released {
state.payload = None;
}

is_dragging = state.payload.is_some();
});

if is_dragging {
ctx.set_cursor_icon(CursorIcon::Grabbing);
}
}

/// Set a drag-and-drop payload.
///
/// This can be read by [`Self::payload`] until the pointer is released.
pub fn set_payload<Payload>(ctx: &Context, payload: Payload)
where
Payload: Any + Send + Sync,
{
ctx.data_mut(|data| {
let state = data.get_temp_mut_or_default::<Self>(Id::NULL);
state.payload = Some(Arc::new(payload));
});
}

/// Clears the payload, setting it to `None`.
pub fn clear_payload(ctx: &Context) {
ctx.data_mut(|data| {
let state = data.get_temp_mut_or_default::<Self>(Id::NULL);
state.payload = None;
});
}

/// Retrieve the payload, if any.
///
/// Returns `None` if there is no payload, or if it is not of the requested type.
///
/// Returns `Some` both during a drag and on the frame the pointer is released
/// (if there is a payload).
pub fn payload<Payload>(ctx: &Context) -> Option<Arc<Payload>>
where
Payload: Any + Send + Sync,
{
ctx.data(|data| {
let state = data.get_temp::<Self>(Id::NULL)?;
let payload = state.payload?;
payload.downcast().ok()
})
}

/// Retrieve and clear the payload, if any.
///
/// Returns `None` if there is no payload, or if it is not of the requested type.
///
/// Returns `Some` both during a drag and on the frame the pointer is released
/// (if there is a payload).
pub fn take_payload<Payload>(ctx: &Context) -> Option<Arc<Payload>>
where
Payload: Any + Send + Sync,
{
ctx.data_mut(|data| {
let state = data.get_temp_mut_or_default::<Self>(Id::NULL);
let payload = state.payload.take()?;
payload.downcast().ok()
})
}

/// Are we carrying a payload of the given type?
///
/// Returns `true` both during a drag and on the frame the pointer is released
/// (if there is a payload).
pub fn has_payload_of_type<Payload>(ctx: &Context) -> bool
where
Payload: Any + Send + Sync,
{
Self::payload::<Payload>(ctx).is_some()
}

/// Are we carrying a payload?
///
/// Returns `true` both during a drag and on the frame the pointer is released
/// (if there is a payload).
pub fn has_any_payload(ctx: &Context) -> bool {
ctx.data(|data| {
let state = data.get_temp::<Self>(Id::NULL);
state.map_or(false, |state| state.payload.is_some())
})
}
}
2 changes: 2 additions & 0 deletions crates/egui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ pub mod containers;
mod context;
mod data;
pub mod debug_text;
mod drag_and_drop;
mod frame_state;
pub(crate) mod grid;
pub mod gui_zoom;
Expand Down Expand Up @@ -417,6 +418,7 @@ pub use {
},
Key,
},
drag_and_drop::DragAndDrop,
grid::Grid,
id::{Id, IdMap},
input_state::{InputState, MultiTouchInfo, PointerState},
Expand Down
58 changes: 55 additions & 3 deletions crates/egui/src/response.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::{any::Any, sync::Arc};

use crate::{
emath::{Align, Pos2, Rect, Vec2},
menu, Context, CursorIcon, Id, LayerId, PointerButton, Sense, Ui, WidgetText,
Expand Down Expand Up @@ -68,7 +70,7 @@ pub struct Response {
#[doc(hidden)]
pub drag_started: bool,

/// The widgets is being dragged.
/// The widget is being dragged.
#[doc(hidden)]
pub dragged: bool,

Expand Down Expand Up @@ -164,7 +166,7 @@ impl Response {
// self.rect. See Context::interact.
// This means we can be hovered and clicked even though `!self.rect.contains(pos)` is true,
// hence the extra complexity here.
if self.hovered() {
if self.contains_pointer() {
false
} else if let Some(pos) = pointer.interact_pos() {
!self.rect.contains(pos)
Expand Down Expand Up @@ -279,7 +281,7 @@ impl Response {
self.drag_started() && self.ctx.input(|i| i.pointer.button_down(button))
}

/// The widgets is being dragged.
/// The widget is being dragged.
///
/// To find out which button(s), use [`Self::dragged_by`].
///
Expand All @@ -288,6 +290,8 @@ impl Response {
/// or the user has pressed down for long enough.
/// See [`crate::input_state::PointerState::is_decidedly_dragging`] for details.
///
/// If you want to avoid the delay, use [`Self::is_pointer_button_down_on`] instead.
///
/// If the widget is NOT sensitive to drags, this will always be `false`.
/// [`crate::DragValue`] senses drags; [`crate::Label`] does not (unless you call [`crate::Label::sense`]).
/// You can use [`Self::interact`] to sense more things *after* adding a widget.
Expand All @@ -296,6 +300,7 @@ impl Response {
self.dragged
}

/// See [`Self::dragged`].
#[inline]
pub fn dragged_by(&self, button: PointerButton) -> bool {
self.dragged() && self.ctx.input(|i| i.pointer.button_down(button))
Expand All @@ -322,6 +327,51 @@ impl Response {
}
}

/// If the user started dragging this widget this frame, store the payload for drag-and-drop.
#[doc(alias = "drag and drop")]
pub fn dnd_set_drag_payload<Payload: Any + Send + Sync>(&self, payload: Payload) {
if self.drag_started() {
crate::DragAndDrop::set_payload(&self.ctx, payload);
}

if self.hovered() && !self.sense.click {
// Things that can be drag-dropped should use the Grab cursor icon,
// but if the thing is _also_ clickable, that can be annoying.
self.ctx.set_cursor_icon(CursorIcon::Grab);
}
}

/// Drag-and-Drop: Return what is being held over this widget, if any.
///
/// Only returns something if [`Self::contains_pointer`] is true,
/// and the user is drag-dropping something of this type.
#[doc(alias = "drag and drop")]
pub fn dnd_hover_payload<Payload: Any + Send + Sync>(&self) -> Option<Arc<Payload>> {
// NOTE: we use `response.contains_pointer` here instead of `hovered`, because
// `hovered` is always false when another widget is being dragged.
if self.contains_pointer() {
crate::DragAndDrop::payload::<Payload>(&self.ctx)
} else {
None
}
}

/// Drag-and-Drop: Return what is being dropped onto this widget, if any.
///
/// Only returns something if [`Self::contains_pointer`] is true,
/// the user is drag-dropping something of this type,
/// and they released it this frame
#[doc(alias = "drag and drop")]
pub fn dnd_release_payload<Payload: Any + Send + Sync>(&self) -> Option<Arc<Payload>> {
// NOTE: we use `response.contains_pointer` here instead of `hovered`, because
// `hovered` is always false when another widget is being dragged.
if self.contains_pointer() && self.ctx.input(|i| i.pointer.any_released()) {
crate::DragAndDrop::take_payload::<Payload>(&self.ctx)
} else {
None
}
}

/// Where the pointer (mouse/touch) were when when this widget was clicked or dragged.
///
/// `None` if the widget is not being interacted with.
Expand Down Expand Up @@ -705,6 +755,8 @@ impl Response {

/// Response to secondary clicks (right-clicks) by showing the given menu.
///
/// Make sure the widget senses clicks (e.g. [`crate::Button`] does, [`crate::Label`] does not).
///
/// ```
/// # use egui::{Label, Sense};
/// # egui::__run_test_ui(|ui| {
Expand Down
105 changes: 103 additions & 2 deletions crates/egui/src/ui.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
#![warn(missing_docs)] // Let's keep `Ui` well-documented.
#![allow(clippy::use_self)]

use std::hash::Hash;
use std::sync::Arc;
use std::{any::Any, hash::Hash, sync::Arc};

use epaint::mutex::RwLock;

Expand Down Expand Up @@ -2121,6 +2120,108 @@ impl Ui {
result
}

/// Create something that can be drag-and-dropped.
///
/// The `id` needs to be globally unique.
/// The payload is what will be dropped if the user starts dragging.
///
/// In contrast to [`Response::dnd_set_drag_payload`],
/// this function will paint the widget at the mouse cursor while the user is dragging.
#[doc(alias = "drag and drop")]
pub fn dnd_drag_source<Payload, R>(
&mut self,
id: Id,
payload: Payload,
add_contents: impl FnOnce(&mut Self) -> R,
) -> InnerResponse<R>
where
Payload: Any + Send + Sync,
{
let is_being_dragged = self.memory(|mem| mem.is_being_dragged(id));

if is_being_dragged {
// Paint the body to a new layer:
let layer_id = LayerId::new(Order::Tooltip, id);
let InnerResponse { inner, response } = self.with_layer_id(layer_id, add_contents);

// Now we move the visuals of the body to where the mouse is.
// Normally you need to decide a location for a widget first,
// because otherwise that widget cannot interact with the mouse.
// However, a dragged component cannot be interacted with anyway
// (anything with `Order::Tooltip` always gets an empty [`Response`])
// So this is fine!

if let Some(pointer_pos) = self.ctx().pointer_interact_pos() {
let delta = pointer_pos - response.rect.center();
self.ctx().translate_layer(layer_id, delta);
}

InnerResponse::new(inner, response)
} else {
let InnerResponse { inner, response } = self.scope(add_contents);

// Check for drags:
let dnd_response = self.interact(response.rect, id, Sense::drag());

dnd_response.dnd_set_drag_payload(payload);

InnerResponse::new(inner, dnd_response | response)
}
}

/// Surround the given ui with a frame which
/// changes colors when you can drop something onto it.
///
/// Returns the dropped item, if it was released this frame.
///
/// The given frame is used for its margins, but it color is ignored.
#[doc(alias = "drag and drop")]
pub fn dnd_drop_zone<Payload>(
&mut self,
frame: Frame,
add_contents: impl FnOnce(&mut Ui),
) -> (Response, Option<Arc<Payload>>)
where
Payload: Any + Send + Sync,
{
let is_anything_being_dragged = DragAndDrop::has_any_payload(self.ctx());
let can_accept_what_is_being_dragged =
DragAndDrop::has_payload_of_type::<Payload>(self.ctx());

let mut frame = frame.begin(self);
add_contents(&mut frame.content_ui);
let response = frame.allocate_space(self);

// NOTE: we use `response.contains_pointer` here instead of `hovered`, because
// `hovered` is always false when another widget is being dragged.
let style = if is_anything_being_dragged
&& can_accept_what_is_being_dragged
&& response.contains_pointer()
{
self.visuals().widgets.active
} else {
self.visuals().widgets.inactive
};

let mut fill = style.bg_fill;
let mut stroke = style.bg_stroke;

if is_anything_being_dragged && !can_accept_what_is_being_dragged {
// When dragging something else, show that it can't be dropped here:
fill = self.visuals().gray_out(fill);
stroke.color = self.visuals().gray_out(stroke.color);
}

frame.frame.fill = fill;
frame.frame.stroke = stroke;

frame.paint(self);

let payload = response.dnd_release_payload::<Payload>();

(response, payload)
}

/// Close the menu we are in (including submenus), if any.
///
/// See also: [`Self::menu_button`] and [`Response::context_menu`].
Expand Down
Loading

0 comments on commit abd028b

Please sign in to comment.