Skip to content

Instantly share code, notes, and snippets.

@sumeet
Created May 23, 2021 19:04
Show Gist options
  • Save sumeet/0f4e97801ed29468986694db2211f0fd to your computer and use it in GitHub Desktop.
Save sumeet/0f4e97801ed29468986694db2211f0fd to your computer and use it in GitHub Desktop.
egui dragdrop probe
use eframe::egui::*;
use std::hash::Hash;
struct DragDropContext<S: DragDropSource> {
dragged_source: Option<S>,
}
impl<S: DragDropSource> DragDropContext<S> {
fn new() -> Self {
Self {
dragged_source: None,
}
}
// should be called first, will discard source if it was never dropped on a target
pub fn init(&mut self, ui: &mut Ui) {
if !ui.memory().is_anything_being_dragged() {
self.dragged_source = None;
}
}
pub fn source(&mut self, ui: &mut Ui, source: S, body: impl FnOnce(&mut Ui)) {
let id = Id::new("dragdrop").with(&source);
let is_being_dragged = ui.memory().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.output().cursor_icon = CursorIcon::Grab;
}
return;
}
ui.output().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.input().pointer.interact_pos() {
let delta = pointer_pos - response.rect.center();
ui.ctx().translate_layer(layer_id, delta);
}
self.dragged_source = Some(source);
}
pub fn target(
&mut self,
ui: &mut Ui,
body: impl FnOnce(&mut Ui, &mut Self),
can_be_dropped_here: impl FnOnce(&S) -> bool,
on_drop: impl FnOnce(S),
) {
let is_being_dragged = ui.memory().is_anything_being_dragged();
// this is the size the virtual drop target takes up, so get a big fat thing to drop on
// this might need to be pulled out into the caller
let margin = Vec2::splat(12.0);
let outer_rect_bounds = ui.available_rect_before_wrap();
let inner_rect = outer_rect_bounds.shrink2(margin);
// not sure if we need this anymore
// let _where_to_put_background = ui.painter().add(Shape::Noop);
let mut content_ui = ui.child_ui(inner_rect, *ui.layout());
body(&mut content_ui, self);
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());
let item_is_over_us = is_being_dragged
&& response.hovered()
&& self
.dragged_source
.as_ref()
.map_or(false, can_be_dropped_here);
if !item_is_over_us {
// the * 2. seems like magic... apparently the margin above gets applied twice
ui.add_space(-margin.x * 2.);
}
if item_is_over_us && ui.input().pointer.any_released() {
on_drop(self.dragged_source.take().unwrap());
}
}
}
trait DragDropSource = Hash;
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "persistence", serde(default))] // if we add new fields, give them default values when deserializing old state
pub struct DragAndDropDemo {
guys: Vec<String>,
#[cfg_attr(feature = "persistence", serde(skip))]
drag_drop_ctx: DragDropContext<usize>,
}
impl Default for DragAndDropDemo {
fn default() -> Self {
Self {
drag_drop_ctx: DragDropContext::new(),
guys: vec![
"hello".into(),
"my".into(),
"name".into(),
"is".into(),
"smt".into(),
],
}
}
}
#[derive(Hash)]
enum DropPlace {
Start,
After(usize),
}
impl DragAndDropDemo {
pub fn draw_window(&mut self, ctx: &CtxRef) {
Window::new("drag drop demo")
.open(&mut true)
.scroll(false)
.resizable(true)
.show(ctx, |ui| self.ui(ui));
}
fn ui(&mut self, ui: &mut Ui) {
self.drag_drop_ctx.init(ui);
let mut drag_drop_occurred = None;
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing = Vec2::ZERO;
for (i, guy) in self.guys.iter().enumerate() {
if i == 0 {
self.drag_drop_ctx.target(
ui,
|ui, _| ui.add_space(0.),
|&source| source != i,
|source| drag_drop_occurred = Some((source, DropPlace::Start)),
);
}
self.drag_drop_ctx.source(ui, i, |ui| {
ui.button(guy).clicked();
});
self.drag_drop_ctx.target(
ui,
|ui, _| ui.add_space(0.),
|&source| source != i && source != i + 1,
|source| drag_drop_occurred = Some((source, DropPlace::After(i))),
);
}
});
if let Some((src, dest)) = drag_drop_occurred {
let removed = self.guys.remove(src);
match dest {
DropPlace::Start => self.guys.insert(0, removed),
DropPlace::After(mut after) => {
after += 1;
if src < after {
after -= 1;
}
self.guys.insert(after, removed);
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment