1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-01 06:10:06 -04:00

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:
Emil Ernerfeldt
2024-01-29 11:14:18 +01:00
committed by GitHub
parent 40ed503ebd
commit abd028bad3
6 changed files with 364 additions and 122 deletions

View File

@@ -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| {