mirror of
https://github.com/emilk/egui.git
synced 2026-08-31 05:40:03 -04:00
WIP
This commit is contained in:
9
Cargo.lock
generated
9
Cargo.lock
generated
@@ -2020,6 +2020,15 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
|
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hierarchical_list_drag_and_drop"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"eframe",
|
||||||
|
"env_logger",
|
||||||
|
"rand",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "home"
|
name = "home"
|
||||||
version = "0.5.5"
|
version = "0.5.5"
|
||||||
|
|||||||
22
examples/hierachical_list_drag_and_drop/Cargo.toml
Normal file
22
examples/hierachical_list_drag_and_drop/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "hierarchical_list_drag_and_drop"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.72"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
eframe = { path = "../../crates/eframe", features = [
|
||||||
|
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
|
||||||
|
] }
|
||||||
|
|
||||||
|
|
||||||
|
env_logger = { version = "0.10", default-features = false, features = [
|
||||||
|
"auto-color",
|
||||||
|
"humantime",
|
||||||
|
] }
|
||||||
|
|
||||||
|
rand = "0.8.5"
|
||||||
6
examples/hierachical_list_drag_and_drop/README.md
Normal file
6
examples/hierachical_list_drag_and_drop/README.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Example showing how to implement drag-and-drop in a hierarchical list
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run -p hierarchical_drag_and_drop
|
||||||
|
```
|
||||||
|
|
||||||
440
examples/hierachical_list_drag_and_drop/src/app.rs
Normal file
440
examples/hierachical_list_drag_and_drop/src/app.rs
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use eframe::{egui, egui::NumExt as _};
|
||||||
|
|
||||||
|
#[derive(Hash, Clone, Copy, PartialEq, Eq)]
|
||||||
|
struct ItemId(u32);
|
||||||
|
|
||||||
|
impl ItemId {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self(rand::random())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for ItemId {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "#{:04x}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ItemId> for egui::Id {
|
||||||
|
fn from(id: ItemId) -> Self {
|
||||||
|
Self::new(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Item {
|
||||||
|
Container(Vec<ItemId>),
|
||||||
|
Leaf(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum Command {
|
||||||
|
/// Set the selection to the given item.
|
||||||
|
SetSelection(ItemId),
|
||||||
|
|
||||||
|
/// Toggle the selected state of the given item.
|
||||||
|
ToggleSelected(ItemId),
|
||||||
|
|
||||||
|
/// Move the currently dragged item to the given container and position.
|
||||||
|
MoveItem {
|
||||||
|
moved_item_id: ItemId,
|
||||||
|
target_container_id: ItemId,
|
||||||
|
target_position_index: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Specify the currently identifed target container to be highlighted.
|
||||||
|
HighlightTargetContainer(ItemId),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HierarchicalDragAndDrop {
|
||||||
|
/// All items
|
||||||
|
items: HashMap<ItemId, Item>,
|
||||||
|
|
||||||
|
/// Id of the root item (not displayed in the UI)
|
||||||
|
root_id: ItemId,
|
||||||
|
|
||||||
|
/// Set of all selected items
|
||||||
|
selected_items: HashSet<ItemId>,
|
||||||
|
|
||||||
|
/// If a drag is ongoing, this is the id of the destination container (if any was identified)
|
||||||
|
///
|
||||||
|
/// This is used to highlight the target container.
|
||||||
|
target_container: Option<ItemId>,
|
||||||
|
|
||||||
|
/// Channel to receive commands from the UI
|
||||||
|
command_receiver: std::sync::mpsc::Receiver<Command>,
|
||||||
|
|
||||||
|
/// Channel to send commands from the UI
|
||||||
|
command_sender: std::sync::mpsc::Sender<Command>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for HierarchicalDragAndDrop {
|
||||||
|
fn default() -> Self {
|
||||||
|
let root_item = Item::Container(Vec::new());
|
||||||
|
let root_id = ItemId::new();
|
||||||
|
|
||||||
|
let (command_sender, command_receiver) = std::sync::mpsc::channel();
|
||||||
|
|
||||||
|
let mut res = Self {
|
||||||
|
items: std::iter::once((root_id, root_item)).collect(),
|
||||||
|
root_id,
|
||||||
|
selected_items: HashSet::new(),
|
||||||
|
target_container: None,
|
||||||
|
command_receiver,
|
||||||
|
command_sender,
|
||||||
|
};
|
||||||
|
|
||||||
|
res.populate();
|
||||||
|
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eframe::App for HierarchicalDragAndDrop {
|
||||||
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
self.ui(ui);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Data stuff
|
||||||
|
//
|
||||||
|
impl HierarchicalDragAndDrop {
|
||||||
|
/// Add a bunch of items in the hierarchy.
|
||||||
|
fn populate(&mut self) {
|
||||||
|
let c1 = self.add_container(self.root_id);
|
||||||
|
let c2 = self.add_container(self.root_id);
|
||||||
|
let c3 = self.add_container(self.root_id);
|
||||||
|
self.add_leaf(self.root_id);
|
||||||
|
self.add_leaf(self.root_id);
|
||||||
|
|
||||||
|
let c11 = self.add_container(c1);
|
||||||
|
let c12 = self.add_container(c1);
|
||||||
|
self.add_leaf(c11);
|
||||||
|
self.add_leaf(c11);
|
||||||
|
self.add_leaf(c12);
|
||||||
|
self.add_leaf(c12);
|
||||||
|
|
||||||
|
self.add_leaf(c2);
|
||||||
|
self.add_leaf(c2);
|
||||||
|
|
||||||
|
self.add_leaf(c3);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn container(&self, id: ItemId) -> Option<&Vec<ItemId>> {
|
||||||
|
match self.items.get(&id) {
|
||||||
|
Some(Item::Container(children)) => Some(children),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Does some container contain the given item?
|
||||||
|
///
|
||||||
|
/// Used to test if a target location is suitable for a given dragged item.
|
||||||
|
fn contains(&self, container_id: ItemId, item_id: ItemId) -> bool {
|
||||||
|
if let Some(children) = self.container(container_id) {
|
||||||
|
if container_id == item_id {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if children.contains(&item_id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for child_id in children {
|
||||||
|
if self.contains(*child_id, item_id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move item `item_id` to `container_id` at position `pos`.
|
||||||
|
fn move_item(&mut self, item_id: ItemId, container_id: ItemId, mut pos: usize) {
|
||||||
|
println!("Moving {item_id:?} to {container_id:?} at position {pos:?}");
|
||||||
|
|
||||||
|
// Remove the item from its current location. Note: we must adjust the target position if the item is
|
||||||
|
// moved within the same container, as the removal might shift the positions by one.
|
||||||
|
if let Some((source_parent_id, source_pos)) = self.parent_and_pos(item_id) {
|
||||||
|
if let Some(Item::Container(children)) = self.items.get_mut(&source_parent_id) {
|
||||||
|
children.remove(source_pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
if source_parent_id == container_id && source_pos < pos {
|
||||||
|
pos -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(Item::Container(children)) = self.items.get_mut(&container_id) {
|
||||||
|
children.insert(pos.at_most(children.len()), item_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the parent of an item, and the index of that item within the parent's children.
|
||||||
|
fn parent_and_pos(&self, id: ItemId) -> Option<(ItemId, usize)> {
|
||||||
|
if id == self.root_id {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.parent_and_pos_impl(id, self.root_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parent_and_pos_impl(&self, id: ItemId, container_id: ItemId) -> Option<(ItemId, usize)> {
|
||||||
|
if let Some(children) = self.container(container_id) {
|
||||||
|
for (idx, child_id) in children.iter().enumerate() {
|
||||||
|
if child_id == &id {
|
||||||
|
return Some((container_id, idx));
|
||||||
|
} else if self.container(*child_id).is_some() {
|
||||||
|
let res = self.parent_and_pos_impl(id, *child_id);
|
||||||
|
if res.is_some() {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_container(&mut self, parent_id: ItemId) -> ItemId {
|
||||||
|
let id = ItemId::new();
|
||||||
|
let item = Item::Container(Vec::new());
|
||||||
|
|
||||||
|
self.items.insert(id, item);
|
||||||
|
|
||||||
|
if let Some(Item::Container(children)) = self.items.get_mut(&parent_id) {
|
||||||
|
children.push(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_leaf(&mut self, parent_id: ItemId) {
|
||||||
|
let id = ItemId::new();
|
||||||
|
let item = Item::Leaf(format!("Item {id:?}"));
|
||||||
|
|
||||||
|
self.items.insert(id, item);
|
||||||
|
|
||||||
|
if let Some(Item::Container(children)) = self.items.get_mut(&parent_id) {
|
||||||
|
children.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected(&self, id: ItemId) -> bool {
|
||||||
|
self.selected_items.contains(&id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_command(&self, command: Command) {
|
||||||
|
// The only way this can fail is if the receiver has been dropped.
|
||||||
|
self.command_sender.send(command).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// UI stuff
|
||||||
|
//
|
||||||
|
impl HierarchicalDragAndDrop {
|
||||||
|
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||||
|
if let Some(top_level_items) = self.container(self.root_id) {
|
||||||
|
self.container_children_ui(ui, top_level_items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// always reset the target container
|
||||||
|
self.target_container = None;
|
||||||
|
|
||||||
|
while let Ok(command) = self.command_receiver.try_recv() {
|
||||||
|
//println!("Received command: {command:?}");
|
||||||
|
match command {
|
||||||
|
Command::SetSelection(item_id) => {
|
||||||
|
self.selected_items.clear();
|
||||||
|
self.selected_items.insert(item_id);
|
||||||
|
}
|
||||||
|
Command::ToggleSelected(item_id) => {
|
||||||
|
if self.selected_items.contains(&item_id) {
|
||||||
|
self.selected_items.remove(&item_id);
|
||||||
|
} else {
|
||||||
|
self.selected_items.insert(item_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::MoveItem {
|
||||||
|
moved_item_id,
|
||||||
|
target_container_id,
|
||||||
|
target_position_index,
|
||||||
|
} => self.move_item(moved_item_id, target_container_id, target_position_index),
|
||||||
|
Command::HighlightTargetContainer(item_id) => {
|
||||||
|
self.target_container = Some(item_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn container_ui(&self, ui: &mut egui::Ui, item_id: ItemId, children: &Vec<ItemId>) {
|
||||||
|
let (_, header_resp, body_resp) =
|
||||||
|
egui::collapsing_header::CollapsingState::load_with_default_open(
|
||||||
|
ui.ctx(),
|
||||||
|
item_id.into(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.show_header(ui, |ui| {
|
||||||
|
ui.add(
|
||||||
|
egui::Label::new(format!("Container {item_id:?}"))
|
||||||
|
.selectable(false)
|
||||||
|
.sense(egui::Sense::drag()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.body(|ui| {
|
||||||
|
self.container_children_ui(ui, children);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.handle_interaction(
|
||||||
|
ui,
|
||||||
|
item_id,
|
||||||
|
true,
|
||||||
|
&header_resp.inner,
|
||||||
|
body_resp.as_ref().map(|r| &r.response),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn container_children_ui(&self, ui: &mut egui::Ui, children: &Vec<ItemId>) {
|
||||||
|
for child_id in children {
|
||||||
|
match self.items.get(child_id) {
|
||||||
|
Some(Item::Container(children)) => {
|
||||||
|
self.container_ui(ui, *child_id, children);
|
||||||
|
}
|
||||||
|
Some(Item::Leaf(label)) => {
|
||||||
|
self.leaf_ui(ui, *child_id, label);
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn leaf_ui(&self, ui: &mut egui::Ui, item_id: ItemId, label: &str) {
|
||||||
|
let response = ui.add(
|
||||||
|
egui::Label::new(label)
|
||||||
|
.selectable(false)
|
||||||
|
.sense(egui::Sense::drag()),
|
||||||
|
);
|
||||||
|
// let response = re_ui
|
||||||
|
// .list_item(label)
|
||||||
|
// .selected(self.selected(item_id))
|
||||||
|
// .draggable(true)
|
||||||
|
// .show(ui);
|
||||||
|
|
||||||
|
self.handle_interaction(ui, item_id, false, &response, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_interaction(
|
||||||
|
&self,
|
||||||
|
ui: &egui::Ui,
|
||||||
|
item_id: ItemId,
|
||||||
|
is_container: bool,
|
||||||
|
response: &egui::Response,
|
||||||
|
body_response: Option<&egui::Response>,
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
// basic selection management
|
||||||
|
//
|
||||||
|
|
||||||
|
if response.clicked() {
|
||||||
|
if ui.input(|i| i.modifiers.command) {
|
||||||
|
self.send_command(Command::ToggleSelected(item_id));
|
||||||
|
} else {
|
||||||
|
self.send_command(Command::SetSelection(item_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// handle drag
|
||||||
|
//
|
||||||
|
|
||||||
|
if response.drag_started() {
|
||||||
|
// Here, we support dragging a single item at a time, so we set the selection to the dragged item
|
||||||
|
// if/when we're dragging it proper.
|
||||||
|
self.send_command(Command::SetSelection(item_id));
|
||||||
|
|
||||||
|
egui::DragAndDrop::set_payload(ui.ctx(), item_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// handle drop
|
||||||
|
//
|
||||||
|
|
||||||
|
// find the item being dragged
|
||||||
|
let Some(dragged_item_id) = egui::DragAndDrop::payload(ui.ctx()).map(|payload| (*payload))
|
||||||
|
else {
|
||||||
|
// nothing is being dragged, we're done here
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing);
|
||||||
|
|
||||||
|
let Some((parent_id, position_index_in_parent)) = self.parent_and_pos(item_id) else {
|
||||||
|
// this shouldn't happen
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let previous_container_id = if position_index_in_parent > 0 {
|
||||||
|
self.container(parent_id)
|
||||||
|
.map(|c| c[position_index_in_parent - 1])
|
||||||
|
.filter(|id| self.container(*id).is_some())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let item_desc = crate::drag_and_drop::DropItemDescription {
|
||||||
|
id: item_id,
|
||||||
|
is_container,
|
||||||
|
parent_id,
|
||||||
|
position_index_in_parent,
|
||||||
|
previous_container_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
let drop_target = crate::drag_and_drop::find_drop_target(
|
||||||
|
ui,
|
||||||
|
&item_desc,
|
||||||
|
response.rect,
|
||||||
|
body_response.map(|r| r.rect),
|
||||||
|
response.rect.height(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(drop_target) = drop_target {
|
||||||
|
// We cannot allow the target location to be "inside" the dragged item, because that would amount moving
|
||||||
|
// myself inside of me.
|
||||||
|
|
||||||
|
if self.contains(dragged_item_id, drop_target.target_parent_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.painter().hline(
|
||||||
|
drop_target.indicator_span_x,
|
||||||
|
drop_target.indicator_position_y,
|
||||||
|
//TODO: use style
|
||||||
|
(2.0, egui::Color32::BLACK),
|
||||||
|
);
|
||||||
|
|
||||||
|
// note: can't use `response.drag_released()` because we not the item which
|
||||||
|
// started the drag
|
||||||
|
if ui.input(|i| i.pointer.any_released()) {
|
||||||
|
self.send_command(Command::MoveItem {
|
||||||
|
moved_item_id: dragged_item_id,
|
||||||
|
target_container_id: drop_target.target_parent_id,
|
||||||
|
target_position_index: drop_target.target_position_index,
|
||||||
|
});
|
||||||
|
|
||||||
|
egui::DragAndDrop::clear_payload(ui.ctx());
|
||||||
|
} else {
|
||||||
|
self.send_command(Command::HighlightTargetContainer(
|
||||||
|
drop_target.target_parent_id,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
311
examples/hierachical_list_drag_and_drop/src/drag_and_drop.rs
Normal file
311
examples/hierachical_list_drag_and_drop/src/drag_and_drop.rs
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
//! Helpers for drag and drop support. Works well in combination with [`crate::list_item::ListItem`].
|
||||||
|
|
||||||
|
use eframe::egui;
|
||||||
|
|
||||||
|
/// Context information related to a candidate drop target, used by [`find_drop_target`] to compute the [`DropTarget`],
|
||||||
|
/// if any.
|
||||||
|
pub struct DropItemDescription<ItemId: Copy> {
|
||||||
|
/// ID of the item being hovered during drag
|
||||||
|
pub id: ItemId,
|
||||||
|
|
||||||
|
/// Can this item "contain" the currently dragged item?
|
||||||
|
pub is_container: bool,
|
||||||
|
|
||||||
|
/// ID of the parent if this item.
|
||||||
|
pub parent_id: ItemId,
|
||||||
|
|
||||||
|
/// Position of this item within its parent.
|
||||||
|
pub position_index_in_parent: usize,
|
||||||
|
|
||||||
|
/// ID of the container just before this item within the parent, if such a container exists.
|
||||||
|
pub previous_container_id: Option<ItemId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drop target information, including where to draw the drop indicator and where to insert the dragged item.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct DropTarget<ItemId: Copy> {
|
||||||
|
/// Range of X coordinates for the drag target indicator
|
||||||
|
pub indicator_span_x: egui::Rangef,
|
||||||
|
|
||||||
|
/// Y coordinate for drag target indicator
|
||||||
|
pub indicator_position_y: f32,
|
||||||
|
|
||||||
|
/// Destination container ID
|
||||||
|
pub target_parent_id: ItemId,
|
||||||
|
|
||||||
|
/// Destination position within the container
|
||||||
|
pub target_position_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<ItemId: Copy> DropTarget<ItemId> {
|
||||||
|
pub fn new(
|
||||||
|
indicator_span_x: egui::Rangef,
|
||||||
|
indicator_position_y: f32,
|
||||||
|
target_parent_id: ItemId,
|
||||||
|
target_position_index: usize,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
indicator_span_x,
|
||||||
|
indicator_position_y,
|
||||||
|
target_parent_id,
|
||||||
|
target_position_index,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute the geometry of the drag cursor and where the dragged item should be inserted.
|
||||||
|
///
|
||||||
|
/// This function implements the following logic:
|
||||||
|
/// ```text
|
||||||
|
///
|
||||||
|
/// insert insert last in container before me
|
||||||
|
/// before me (if any) or insert before me
|
||||||
|
/// │ │
|
||||||
|
/// ╔═══▼═════════════════════════════▼══════════════════╗
|
||||||
|
/// ║ │ ║
|
||||||
|
/// leaf item ║ ─────┴──────────────────────────────────────────── ║
|
||||||
|
/// ║ ║
|
||||||
|
/// ╚═════════════════════▲══════════════════════════════╝
|
||||||
|
/// │
|
||||||
|
/// insert after me
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// insert insert last in container before me
|
||||||
|
/// before me (if any) or insert before me
|
||||||
|
/// │ │
|
||||||
|
/// ╔═══▼═════════════════════════════▼══════════════════╗
|
||||||
|
/// leaf item ║ │ ║
|
||||||
|
/// with body ║ ─────┴──────────────────────────────────────────── ║
|
||||||
|
/// ║ ║
|
||||||
|
/// ╚══════╦══════════════════════════════════════▲══════╣ ─┐
|
||||||
|
/// │ ║ │ ║ │
|
||||||
|
/// │ ║ insert ║ │
|
||||||
|
/// │ ║ after me ║ │
|
||||||
|
/// │ ╠══ ══╣ │
|
||||||
|
/// │ ║ no insertion possible ║ │
|
||||||
|
/// │ ║ here by definition of ║ │ body
|
||||||
|
/// │ ║ parent being a leaf ║ │
|
||||||
|
/// │ ╠══ ══╣ │
|
||||||
|
/// │ ║ ║ │
|
||||||
|
/// │ ║ ║ │
|
||||||
|
/// │ ║ ║ │
|
||||||
|
/// └──▲── ╚══════════════════════════▲══════════════════╝ ─┘
|
||||||
|
/// │ │
|
||||||
|
/// insert insert
|
||||||
|
/// after me after me
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// insert insert last in container before me
|
||||||
|
/// before me (if any) or insert before me
|
||||||
|
/// │ │
|
||||||
|
/// ╔═══▼═════════════════════════════▼══════════════════╗
|
||||||
|
/// container item ║ │ ║
|
||||||
|
/// (empty/collapsed ║ ─────┼──────────────────────────────────────────── ║
|
||||||
|
/// body) ║ │ ║
|
||||||
|
/// ╚═══▲═════════════════════════════▲══════════════════╝
|
||||||
|
/// │ │
|
||||||
|
/// insert insert inside me
|
||||||
|
/// after me at pos = 0
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// insert insert last in container before me
|
||||||
|
/// before me (if any) or insert before me
|
||||||
|
/// │ │
|
||||||
|
/// ╔═══▼═════════════════════════════▼══════════════════╗
|
||||||
|
/// container item ║ │ ║
|
||||||
|
/// with body ║ ─────┴──────────────────────────────────────────── ║
|
||||||
|
/// ║ ║
|
||||||
|
/// ╚═▲════╦═════════════════════════════════════════════╣ ─┐
|
||||||
|
/// │ ║ ║ │
|
||||||
|
/// insert ║ ║ │
|
||||||
|
/// inside me ║ ║ │
|
||||||
|
/// at pos = 0 ╠══ ══╣ │
|
||||||
|
/// ║ same logic ║ │
|
||||||
|
/// ║ recursively ║ │ body
|
||||||
|
/// insert ║ applied here ║ │
|
||||||
|
/// after me ╠══ ══╣ │
|
||||||
|
/// │ ║ ║ │
|
||||||
|
/// ┌─▼─── ║ ║ │
|
||||||
|
/// │ ║ ║ │
|
||||||
|
/// └───── ╚═════════════════════════════════════════════╝ ─┘
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Here are a few observations of the above that help navigate the "if-statement-of-death"
|
||||||
|
/// in the implementation:
|
||||||
|
/// - The top parts of the item are treated the same in all four cases.
|
||||||
|
/// - Handling of the body can be simplified by making the sensitive area either a small
|
||||||
|
/// corner (container case), or the entire body (leaf case). Then, that area always maps
|
||||||
|
/// to "insert after me".
|
||||||
|
/// - The bottom parts have the most difference between cases and need case-by-case handling.
|
||||||
|
/// In both leaf item cases, the entire bottom part maps to "insert after me", though.
|
||||||
|
///
|
||||||
|
/// **Note**: in debug builds, press `Alt` to visualize the drag zones while dragging.
|
||||||
|
pub fn find_drop_target<ItemId: Copy>(
|
||||||
|
ui: &egui::Ui,
|
||||||
|
item_desc: &DropItemDescription<ItemId>,
|
||||||
|
item_rect: egui::Rect,
|
||||||
|
body_rect: Option<egui::Rect>,
|
||||||
|
item_height: f32,
|
||||||
|
) -> Option<DropTarget<ItemId>> {
|
||||||
|
let indent = ui.spacing().indent;
|
||||||
|
let item_id = item_desc.id;
|
||||||
|
let is_container = item_desc.is_container;
|
||||||
|
let parent_id = item_desc.parent_id;
|
||||||
|
let pos_in_parent = item_desc.position_index_in_parent;
|
||||||
|
|
||||||
|
// For both leaf and containers we have two drag zones on the upper half of the item.
|
||||||
|
let (top, mut bottom) = item_rect.split_top_bottom_at_fraction(0.5);
|
||||||
|
let (left_top, top) = top.split_left_right_at_x(top.left() + indent);
|
||||||
|
|
||||||
|
// For the lower part of the item, the story is more complicated:
|
||||||
|
// - for leaf item, we have a single drag zone on the entire lower half
|
||||||
|
// - for container item, we must distinguish between the indent part and the rest, plus check some area in the
|
||||||
|
// body
|
||||||
|
let mut left_bottom = egui::Rect::NOTHING;
|
||||||
|
if is_container {
|
||||||
|
(left_bottom, bottom) = bottom.split_left_right_at_x(bottom.left() + indent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For the body area we have two cases:
|
||||||
|
// - container item: it's handled recursively by the nested items, so we only need to check a small area down
|
||||||
|
// left, which maps to "insert after me"
|
||||||
|
// - leaf item: the entire body area, if any, cannot receive a drag (by definition) and thus homogeneously maps
|
||||||
|
// to "insert after me"
|
||||||
|
let body_insert_after_me_area = if let Some(body_rect) = body_rect {
|
||||||
|
if item_desc.is_container {
|
||||||
|
egui::Rect::from_two_pos(
|
||||||
|
body_rect.left_bottom() + egui::vec2(indent, -item_height / 2.0),
|
||||||
|
body_rect.left_bottom(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
body_rect
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
egui::Rect::NOTHING
|
||||||
|
};
|
||||||
|
|
||||||
|
// body rect, if any AND it actually contains something
|
||||||
|
let non_empty_body_rect = body_rect.filter(|r| r.height() > 0.0);
|
||||||
|
|
||||||
|
// visualize the drag zones in debug builds, when the `Alt` key is pressed during drag
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
// Visualize the drag zones
|
||||||
|
if ui.input(|i| i.modifiers.alt) {
|
||||||
|
ui.ctx()
|
||||||
|
.debug_painter()
|
||||||
|
.debug_rect(top, egui::Color32::RED, "t");
|
||||||
|
ui.ctx()
|
||||||
|
.debug_painter()
|
||||||
|
.debug_rect(bottom, egui::Color32::GREEN, "b");
|
||||||
|
|
||||||
|
ui.ctx().debug_painter().debug_rect(
|
||||||
|
left_top,
|
||||||
|
egui::Color32::RED.gamma_multiply(0.5),
|
||||||
|
"lt",
|
||||||
|
);
|
||||||
|
ui.ctx().debug_painter().debug_rect(
|
||||||
|
left_bottom,
|
||||||
|
egui::Color32::GREEN.gamma_multiply(0.5),
|
||||||
|
"lb",
|
||||||
|
);
|
||||||
|
ui.ctx().debug_painter().debug_rect(
|
||||||
|
body_insert_after_me_area,
|
||||||
|
egui::Color32::YELLOW,
|
||||||
|
"bdy",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== TOP SECTIONS (same leaf/container items) ==== */
|
||||||
|
if ui.rect_contains_pointer(left_top) {
|
||||||
|
// insert before me
|
||||||
|
Some(DropTarget::new(
|
||||||
|
item_rect.x_range(),
|
||||||
|
top.top(),
|
||||||
|
parent_id,
|
||||||
|
pos_in_parent,
|
||||||
|
))
|
||||||
|
} else if ui.rect_contains_pointer(top) {
|
||||||
|
// insert last in the previous container if any, else insert before me
|
||||||
|
if let Some(previous_container_id) = item_desc.previous_container_id {
|
||||||
|
Some(DropTarget::new(
|
||||||
|
(item_rect.left() + indent..=item_rect.right()).into(),
|
||||||
|
top.top(),
|
||||||
|
previous_container_id,
|
||||||
|
usize::MAX,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Some(DropTarget::new(
|
||||||
|
item_rect.x_range(),
|
||||||
|
top.top(),
|
||||||
|
parent_id,
|
||||||
|
pos_in_parent,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* ==== BODY SENSE AREA ==== */
|
||||||
|
else if ui.rect_contains_pointer(body_insert_after_me_area) {
|
||||||
|
// insert after me in my parent
|
||||||
|
Some(DropTarget::new(
|
||||||
|
item_rect.x_range(),
|
||||||
|
body_insert_after_me_area.bottom(),
|
||||||
|
parent_id,
|
||||||
|
pos_in_parent + 1,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
/* ==== BOTTOM SECTIONS (leaf item) ==== */
|
||||||
|
else if !is_container {
|
||||||
|
if ui.rect_contains_pointer(bottom) {
|
||||||
|
let position_y = if let Some(non_empty_body_rect) = non_empty_body_rect {
|
||||||
|
non_empty_body_rect.bottom()
|
||||||
|
} else {
|
||||||
|
bottom.bottom()
|
||||||
|
};
|
||||||
|
|
||||||
|
// insert after me
|
||||||
|
Some(DropTarget::new(
|
||||||
|
item_rect.x_range(),
|
||||||
|
position_y,
|
||||||
|
parent_id,
|
||||||
|
pos_in_parent + 1,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* ==== BOTTOM SECTIONS (container item) ==== */
|
||||||
|
else if let Some(non_empty_body_rect) = non_empty_body_rect {
|
||||||
|
if ui.rect_contains_pointer(left_bottom) || ui.rect_contains_pointer(bottom) {
|
||||||
|
// insert at pos = 0 inside me
|
||||||
|
Some(DropTarget::new(
|
||||||
|
(non_empty_body_rect.left() + indent..=non_empty_body_rect.right()).into(),
|
||||||
|
left_bottom.bottom(),
|
||||||
|
item_id,
|
||||||
|
0,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else if ui.rect_contains_pointer(left_bottom) {
|
||||||
|
// insert after me in my parent
|
||||||
|
Some(DropTarget::new(
|
||||||
|
item_rect.x_range(),
|
||||||
|
left_bottom.bottom(),
|
||||||
|
parent_id,
|
||||||
|
pos_in_parent + 1,
|
||||||
|
))
|
||||||
|
} else if ui.rect_contains_pointer(bottom) {
|
||||||
|
// insert at pos = 0 inside me
|
||||||
|
Some(DropTarget::new(
|
||||||
|
(item_rect.left() + indent..=item_rect.right()).into(),
|
||||||
|
bottom.bottom(),
|
||||||
|
item_id,
|
||||||
|
0,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
/* ==== Who knows where else the mouse cursor might wander… ¯\_(ツ)_/¯ ==== */
|
||||||
|
else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
20
examples/hierachical_list_drag_and_drop/src/main.rs
Normal file
20
examples/hierachical_list_drag_and_drop/src/main.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
mod drag_and_drop;
|
||||||
|
|
||||||
|
use crate::app::HierarchicalDragAndDrop;
|
||||||
|
use eframe::egui;
|
||||||
|
|
||||||
|
fn main() -> Result<(), eframe::Error> {
|
||||||
|
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
|
||||||
|
let options = eframe::NativeOptions {
|
||||||
|
viewport: egui::ViewportBuilder::default().with_inner_size([320.0, 240.0]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
eframe::run_native(
|
||||||
|
"My egui App",
|
||||||
|
options,
|
||||||
|
Box::new(|cc| Box::<HierarchicalDragAndDrop>::default()),
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user