Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add drag-and-drop APIs with payloads storage #3887

Merged
merged 24 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
126 changes: 126 additions & 0 deletions crates/egui/src/drag_and_drop.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//! Support for drag-and-drop in egui.
//!
//! This module implements the low-level drag-and-drop API with support for manually saving, retrieving, and clearing
//! drag-and-drop payloads. This enables the maximum flexibility for complex drag-and-drop interactions.
//!
//! For a higher-level API, see:
//! - [`crate::Ui::dnd_drag_source`],
//! - [`crate::Ui::dnd_drop_zone`],
//! - and the drag-related functions in [`crate::Response`].
emilk marked this conversation as resolved.
Show resolved Hide resolved

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

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

/// Helpers for drag-and-drop in egui.
///
/// 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
Loading