Add drag-and-drop APIs with payloads storage (#3887)
* Closes https://github.com/emilk/egui/issues/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 <abeyeler@ab-ware.com>
This commit is contained in:
parent
40ed503ebd
commit
abd028bad3
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -417,6 +418,7 @@ pub use {
|
|||
},
|
||||
Key,
|
||||
},
|
||||
drag_and_drop::DragAndDrop,
|
||||
grid::Grid,
|
||||
id::{Id, IdMap},
|
||||
input_state::{InputState, MultiTouchInfo, PointerState},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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,
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
@ -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`].
|
||||
///
|
||||
|
|
@ -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.
|
||||
|
|
@ -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))
|
||||
|
|
@ -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.
|
||||
|
|
@ -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| {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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`].
|
||||
|
|
|
|||
|
|
@ -1,78 +1,5 @@
|
|||
use egui::*;
|
||||
|
||||
pub fn drag_source(ui: &mut Ui, id: Id, body: impl FnOnce(&mut Ui)) {
|
||||
let is_being_dragged = ui.memory(|mem| mem.is_being_dragged(id));
|
||||
|
||||
if !is_being_dragged {
|
||||
let response = ui.scope(body).response;
|
||||
|
||||
// Check for drags:
|
||||
let response = ui.interact(response.rect, id, Sense::drag());
|
||||
if response.hovered() {
|
||||
ui.ctx().set_cursor_icon(CursorIcon::Grab);
|
||||
}
|
||||
} else {
|
||||
ui.ctx().set_cursor_icon(CursorIcon::Grabbing);
|
||||
|
||||
// Paint the body to a new layer:
|
||||
let layer_id = LayerId::new(Order::Tooltip, id);
|
||||
let response = ui.with_layer_id(layer_id, body).response;
|
||||
|
||||
// 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) = ui.ctx().pointer_interact_pos() {
|
||||
let delta = pointer_pos - response.rect.center();
|
||||
ui.ctx().translate_layer(layer_id, delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drop_target<R>(
|
||||
ui: &mut Ui,
|
||||
can_accept_what_is_being_dragged: bool,
|
||||
body: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
let is_being_dragged = ui.memory(|mem| mem.is_anything_being_dragged());
|
||||
|
||||
let margin = Vec2::splat(4.0);
|
||||
|
||||
let outer_rect_bounds = ui.available_rect_before_wrap();
|
||||
let inner_rect = outer_rect_bounds.shrink2(margin);
|
||||
let where_to_put_background = ui.painter().add(Shape::Noop);
|
||||
let mut content_ui = ui.child_ui(inner_rect, *ui.layout());
|
||||
let ret = body(&mut content_ui);
|
||||
let outer_rect = Rect::from_min_max(outer_rect_bounds.min, content_ui.min_rect().max + margin);
|
||||
let (rect, response) = ui.allocate_at_least(outer_rect.size(), Sense::hover());
|
||||
|
||||
// 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_being_dragged && can_accept_what_is_being_dragged && response.contains_pointer() {
|
||||
ui.visuals().widgets.active
|
||||
} else {
|
||||
ui.visuals().widgets.inactive
|
||||
};
|
||||
|
||||
let mut fill = style.bg_fill;
|
||||
let mut stroke = style.bg_stroke;
|
||||
if is_being_dragged && !can_accept_what_is_being_dragged {
|
||||
fill = ui.visuals().gray_out(fill);
|
||||
stroke.color = ui.visuals().gray_out(stroke.color);
|
||||
}
|
||||
|
||||
ui.painter().set(
|
||||
where_to_put_background,
|
||||
epaint::RectShape::new(rect, style.rounding, fill, stroke),
|
||||
);
|
||||
|
||||
InnerResponse::new(ret, response)
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct DragAndDropDemo {
|
||||
|
|
@ -84,9 +11,9 @@ impl Default for DragAndDropDemo {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
columns: vec![
|
||||
vec!["Item A", "Item B", "Item C"],
|
||||
vec!["Item D", "Item E"],
|
||||
vec!["Item F", "Item G", "Item H"],
|
||||
vec!["Item A", "Item B", "Item C", "Item D"],
|
||||
vec!["Item E", "Item F", "Item G"],
|
||||
vec!["Item H", "Item I", "Item J", "Item K"],
|
||||
]
|
||||
.into_iter()
|
||||
.map(|v| v.into_iter().map(ToString::to_string).collect())
|
||||
|
|
@ -111,66 +38,100 @@ impl super::Demo for DragAndDropDemo {
|
|||
}
|
||||
}
|
||||
|
||||
/// What is being dragged.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct Location {
|
||||
col: usize,
|
||||
row: usize,
|
||||
}
|
||||
|
||||
impl super::View for DragAndDropDemo {
|
||||
fn ui(&mut self, ui: &mut Ui) {
|
||||
ui.label("This is a proof-of-concept of drag-and-drop in egui.");
|
||||
ui.label("This is a simple example of drag-and-drop in egui.");
|
||||
ui.label("Drag items between columns.");
|
||||
|
||||
let id_source = "my_drag_and_drop_demo";
|
||||
let mut source_col_row = None;
|
||||
let mut drop_col = None;
|
||||
// If there is a drop, store the location of the item being dragged, and the destination for the drop.
|
||||
let mut from = None;
|
||||
let mut to = None;
|
||||
|
||||
ui.columns(self.columns.len(), |uis| {
|
||||
for (col_idx, column) in self.columns.clone().into_iter().enumerate() {
|
||||
let ui = &mut uis[col_idx];
|
||||
let can_accept_what_is_being_dragged = true; // We accept anything being dragged (for now) ¯\_(ツ)_/¯
|
||||
let response = drop_target(ui, can_accept_what_is_being_dragged, |ui| {
|
||||
|
||||
let frame = Frame::default().inner_margin(4.0);
|
||||
|
||||
let (_, dropped_payload) = ui.dnd_drop_zone::<Location>(frame, |ui| {
|
||||
ui.set_min_size(vec2(64.0, 100.0));
|
||||
for (row_idx, item) in column.iter().enumerate() {
|
||||
let item_id = Id::new(id_source).with(col_idx).with(row_idx);
|
||||
drag_source(ui, item_id, |ui| {
|
||||
let response = ui.add(Label::new(item).sense(Sense::click()));
|
||||
response.context_menu(|ui| {
|
||||
if ui.button("Remove").clicked() {
|
||||
self.columns[col_idx].remove(row_idx);
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
});
|
||||
let item_id = Id::new(("my_drag_and_drop_demo", col_idx, row_idx));
|
||||
let item_location = Location {
|
||||
col: col_idx,
|
||||
row: row_idx,
|
||||
};
|
||||
let response = ui
|
||||
.dnd_drag_source(item_id, item_location, |ui| {
|
||||
ui.label(item);
|
||||
})
|
||||
.response;
|
||||
|
||||
if ui.memory(|mem| mem.is_being_dragged(item_id)) {
|
||||
source_col_row = Some((col_idx, row_idx));
|
||||
// Detect drops onto this item:
|
||||
if let (Some(pointer), Some(hovered_payload)) = (
|
||||
ui.input(|i| i.pointer.interact_pos()),
|
||||
response.dnd_hover_payload::<Location>(),
|
||||
) {
|
||||
let rect = response.rect;
|
||||
|
||||
// Preview insertion:
|
||||
let stroke = egui::Stroke::new(1.0, Color32::WHITE);
|
||||
let insert_row_idx = if *hovered_payload == item_location {
|
||||
// We are dragged onto ourselves
|
||||
ui.painter().hline(rect.x_range(), rect.center().y, stroke);
|
||||
row_idx
|
||||
} else if pointer.y < rect.center().y {
|
||||
// Above us
|
||||
ui.painter().hline(rect.x_range(), rect.top(), stroke);
|
||||
row_idx
|
||||
} else {
|
||||
// Below us
|
||||
ui.painter().hline(rect.x_range(), rect.bottom(), stroke);
|
||||
row_idx + 1
|
||||
};
|
||||
|
||||
if let Some(dragged_payload) = response.dnd_release_payload() {
|
||||
// The user dropped onto this item.
|
||||
from = Some(dragged_payload);
|
||||
to = Some(Location {
|
||||
col: col_idx,
|
||||
row: insert_row_idx,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.response;
|
||||
|
||||
let response = response.context_menu(|ui| {
|
||||
if ui.button("New Item").clicked() {
|
||||
self.columns[col_idx].push("New Item".to_owned());
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
|
||||
let is_being_dragged = ui.memory(|mem| mem.is_anything_being_dragged());
|
||||
// NOTE: we use `response.contains_pointer` here instead of `hovered`, because
|
||||
// `hovered` is always false when another widget is being dragged.
|
||||
if is_being_dragged
|
||||
&& can_accept_what_is_being_dragged
|
||||
&& response.contains_pointer()
|
||||
{
|
||||
drop_col = Some(col_idx);
|
||||
if let Some(dragged_payload) = dropped_payload {
|
||||
// The user dropped onto the column, but not on any one item.
|
||||
from = Some(dragged_payload);
|
||||
to = Some(Location {
|
||||
col: col_idx,
|
||||
row: usize::MAX, // Inset last
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if let Some((source_col, source_row)) = source_col_row {
|
||||
if let Some(drop_col) = drop_col {
|
||||
if ui.input(|i| i.pointer.any_released()) {
|
||||
// do the drop:
|
||||
let item = self.columns[source_col].remove(source_row);
|
||||
self.columns[drop_col].push(item);
|
||||
}
|
||||
if let (Some(from), Some(mut to)) = (from, to) {
|
||||
if from.col == to.col {
|
||||
// Dragging within the same column.
|
||||
// Adjust row index if we are re-ordering:
|
||||
to.row -= (from.row < to.row) as usize;
|
||||
}
|
||||
|
||||
let item = self.columns[from.col].remove(from.row);
|
||||
|
||||
let column = &mut self.columns[to.col];
|
||||
to.row = to.row.min(column.len());
|
||||
column.insert(to.row, item);
|
||||
}
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
|
|
|
|||
Loading…
Reference in New Issue