Created
May 23, 2021 19:04
-
-
Save sumeet/0f4e97801ed29468986694db2211f0fd to your computer and use it in GitHub Desktop.
egui dragdrop probe
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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