Skip to content

Commit

Permalink
Allow drag-and-dropping multiple containers and views in the blueprin…
Browse files Browse the repository at this point in the history
…t tree (#8334)

### Related

* Closes #8276
* Closes #8275
* Part of #8266
* Part of #8267
* Related to #7108

### What

This PR makes it possible to drag multi-selection of views and
containers within the blueprint tree. It lays the foundation of a system
that will be extended to other drag payload and UI sections.

Specifically:
- `ctx.handle_select_hover_drag_interactions()` (formerly
`ctx.select_hovered_on_click()`) is now able to initiate drag
interactions. This is opt-in for now, as dragging from most place isn't
supported yet (to implemented in follow-up PRs).
- Introduce a new `DragAndDropPayload` type to interoperate between
various part of the UI. This type also enforce the grouping of items
that can meaningfully be dragged together (e.g. it's ok to drag a view
and a container together, because there exist somewhere they can be
dropped to, but it's not ok to drag a view and an entity together).
- When a drag is successfully initiated, a "pill" is displayed along the
cursor which indicates the content of what's being dragged.
- Introduces a _very_ hack mechanism for a black list of undraggable
items (aka the root container).
- Update blueprint tree to support multiple selection and the new drag
and drop payload type.
- Updates egui to latest `master`.

---------

Co-authored-by: Emil Ernerfeldt <[email protected]>
  • Loading branch information
abey79 and emilk authored Dec 11, 2024
1 parent 388c8aa commit 704438f
Show file tree
Hide file tree
Showing 32 changed files with 403 additions and 111 deletions.
22 changes: 11 additions & 11 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1934,7 +1934,7 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
[[package]]
name = "ecolor"
version = "0.29.1"
source = "git+https://github.com/emilk/egui.git?rev=577ee8d22810752540636febac5660a5119c6550#577ee8d22810752540636febac5660a5119c6550"
source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040"
dependencies = [
"bytemuck",
"color-hex",
Expand All @@ -1951,7 +1951,7 @@ checksum = "18aade80d5e09429040243ce1143ddc08a92d7a22820ac512610410a4dd5214f"
[[package]]
name = "eframe"
version = "0.29.1"
source = "git+https://github.com/emilk/egui.git?rev=577ee8d22810752540636febac5660a5119c6550#577ee8d22810752540636febac5660a5119c6550"
source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040"
dependencies = [
"ahash",
"bytemuck",
Expand Down Expand Up @@ -1990,7 +1990,7 @@ dependencies = [
[[package]]
name = "egui"
version = "0.29.1"
source = "git+https://github.com/emilk/egui.git?rev=577ee8d22810752540636febac5660a5119c6550#577ee8d22810752540636febac5660a5119c6550"
source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040"
dependencies = [
"accesskit",
"ahash",
Expand All @@ -2007,7 +2007,7 @@ dependencies = [
[[package]]
name = "egui-wgpu"
version = "0.29.1"
source = "git+https://github.com/emilk/egui.git?rev=577ee8d22810752540636febac5660a5119c6550#577ee8d22810752540636febac5660a5119c6550"
source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040"
dependencies = [
"ahash",
"bytemuck",
Expand All @@ -2026,7 +2026,7 @@ dependencies = [
[[package]]
name = "egui-winit"
version = "0.29.1"
source = "git+https://github.com/emilk/egui.git?rev=577ee8d22810752540636febac5660a5119c6550#577ee8d22810752540636febac5660a5119c6550"
source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040"
dependencies = [
"accesskit_winit",
"ahash",
Expand Down Expand Up @@ -2068,7 +2068,7 @@ dependencies = [
[[package]]
name = "egui_extras"
version = "0.29.1"
source = "git+https://github.com/emilk/egui.git?rev=577ee8d22810752540636febac5660a5119c6550#577ee8d22810752540636febac5660a5119c6550"
source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040"
dependencies = [
"ahash",
"egui",
Expand All @@ -2085,7 +2085,7 @@ dependencies = [
[[package]]
name = "egui_glow"
version = "0.29.1"
source = "git+https://github.com/emilk/egui.git?rev=577ee8d22810752540636febac5660a5119c6550#577ee8d22810752540636febac5660a5119c6550"
source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040"
dependencies = [
"ahash",
"bytemuck",
Expand All @@ -2103,7 +2103,7 @@ dependencies = [
[[package]]
name = "egui_kittest"
version = "0.29.1"
source = "git+https://github.com/emilk/egui.git?rev=577ee8d22810752540636febac5660a5119c6550#577ee8d22810752540636febac5660a5119c6550"
source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040"
dependencies = [
"dify",
"egui",
Expand Down Expand Up @@ -2172,7 +2172,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "emath"
version = "0.29.1"
source = "git+https://github.com/emilk/egui.git?rev=577ee8d22810752540636febac5660a5119c6550#577ee8d22810752540636febac5660a5119c6550"
source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040"
dependencies = [
"bytemuck",
"serde",
Expand Down Expand Up @@ -2288,7 +2288,7 @@ dependencies = [
[[package]]
name = "epaint"
version = "0.29.1"
source = "git+https://github.com/emilk/egui.git?rev=577ee8d22810752540636febac5660a5119c6550#577ee8d22810752540636febac5660a5119c6550"
source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040"
dependencies = [
"ab_glyph",
"ahash",
Expand All @@ -2307,7 +2307,7 @@ dependencies = [
[[package]]
name = "epaint_default_fonts"
version = "0.29.1"
source = "git+https://github.com/emilk/egui.git?rev=577ee8d22810752540636febac5660a5119c6550#577ee8d22810752540636febac5660a5119c6550"
source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040"

[[package]]
name = "equivalent"
Expand Down
14 changes: 7 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -560,13 +560,13 @@ significant_drop_tightening = "allow" # An update of parking_lot made this trigg
# As a last resport, patch with a commit to our own repository.
# ALWAYS document what PR the commit hash is part of, or when it was merged into the upstream trunk.

ecolor = { git = "https://github.com/emilk/egui.git", rev = "577ee8d22810752540636febac5660a5119c6550" } # egui master 2024-12-04
eframe = { git = "https://github.com/emilk/egui.git", rev = "577ee8d22810752540636febac5660a5119c6550" } # egui master 2024-12-04
egui = { git = "https://github.com/emilk/egui.git", rev = "577ee8d22810752540636febac5660a5119c6550" } # egui master 2024-12-04
egui_extras = { git = "https://github.com/emilk/egui.git", rev = "577ee8d22810752540636febac5660a5119c6550" } # egui master 2024-12-04
egui_kittest = { git = "https://github.com/emilk/egui.git", rev = "577ee8d22810752540636febac5660a5119c6550" } # egui master 2024-12-04
egui-wgpu = { git = "https://github.com/emilk/egui.git", rev = "577ee8d22810752540636febac5660a5119c6550" } # egui master 2024-12-04
emath = { git = "https://github.com/emilk/egui.git", rev = "577ee8d22810752540636febac5660a5119c6550" } # egui master 2024-12-04
ecolor = { git = "https://github.com/emilk/egui.git", rev = "13352d606496d7b1c5fd6fcfbe3c85baae39c040" } # egui master 2024-12-09
eframe = { git = "https://github.com/emilk/egui.git", rev = "13352d606496d7b1c5fd6fcfbe3c85baae39c040" } # egui master 2024-12-09
egui = { git = "https://github.com/emilk/egui.git", rev = "13352d606496d7b1c5fd6fcfbe3c85baae39c040" } # egui master 2024-12-09
egui_extras = { git = "https://github.com/emilk/egui.git", rev = "13352d606496d7b1c5fd6fcfbe3c85baae39c040" } # egui master 2024-12-09
egui_kittest = { git = "https://github.com/emilk/egui.git", rev = "13352d606496d7b1c5fd6fcfbe3c85baae39c040" } # egui master 2024-12-09
egui-wgpu = { git = "https://github.com/emilk/egui.git", rev = "13352d606496d7b1c5fd6fcfbe3c85baae39c040" } # egui master 2024-12-09
emath = { git = "https://github.com/emilk/egui.git", rev = "13352d606496d7b1c5fd6fcfbe3c85baae39c040" } # egui master 2024-12-09

# Useful while developing:
# ecolor = { path = "../../egui/crates/ecolor" }
Expand Down
96 changes: 53 additions & 43 deletions crates/viewer/re_blueprint_tree/src/blueprint_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use re_types::blueprint::components::Visible;
use re_ui::{drag_and_drop::DropTarget, list_item, ContextExt as _, DesignTokens, UiExt as _};
use re_viewer_context::{
contents_name_style, icon_for_container_kind, CollapseScope, Contents, DataResultNodeOrPath,
SystemCommandSender,
DragAndDropPayload, SystemCommandSender,
};
use re_viewer_context::{
ContainerId, DataQueryResult, DataResultNode, HoverHighlight, Item, ViewId, ViewerContext,
Expand Down Expand Up @@ -168,7 +168,7 @@ impl BlueprintTree {
let item_response = ui
.list_item()
.selected(ctx.selection().contains_item(&item))
.draggable(false)
.draggable(true) // allowed for consistency but results in an invalid drag
.drop_target_style(self.is_candidate_drop_parent_container(&container_id))
.show_flat(
ui,
Expand All @@ -189,7 +189,7 @@ impl BlueprintTree {
SelectionUpdateBehavior::UseSelection,
);
self.scroll_to_me_if_needed(ui, &item, &item_response);
ctx.select_hovered_on_click(&item_response, item);
ctx.handle_select_hover_drag_interactions(&item_response, item, true);

self.handle_root_container_drag_and_drop_interaction(
viewport,
Expand Down Expand Up @@ -270,12 +270,11 @@ impl BlueprintTree {
SelectionUpdateBehavior::UseSelection,
);
self.scroll_to_me_if_needed(ui, &item, &response);
ctx.select_hovered_on_click(&response, item);
ctx.handle_select_hover_drag_interactions(&response, item, true);

viewport.set_content_visibility(ctx, &content, visible);

self.handle_drag_and_drop_interaction(
ctx,
viewport,
ui,
content,
Expand Down Expand Up @@ -406,13 +405,12 @@ impl BlueprintTree {
SelectionUpdateBehavior::UseSelection,
);
self.scroll_to_me_if_needed(ui, &item, &response);
ctx.select_hovered_on_click(&response, item);
ctx.handle_select_hover_drag_interactions(&response, item, true);

let content = Contents::View(*view_id);

viewport.set_content_visibility(ctx, &content, visible);
self.handle_drag_and_drop_interaction(
ctx,
viewport,
ui,
content,
Expand Down Expand Up @@ -494,6 +492,7 @@ impl BlueprintTree {

let list_item = ui
.list_item()
.draggable(true)
.selected(is_selected)
.force_hovered(is_item_hovered);

Expand Down Expand Up @@ -596,7 +595,7 @@ impl BlueprintTree {
SelectionUpdateBehavior::UseSelection,
);
self.scroll_to_me_if_needed(ui, &item, &response);
ctx.select_hovered_on_click(&response, item);
ctx.handle_select_hover_drag_interactions(&response, item, true);
}

/// Add a button to trigger the addition of a new view or container.
Expand Down Expand Up @@ -636,16 +635,21 @@ impl BlueprintTree {
response: &egui::Response,
) {
//
// check if a drag is in progress and set the cursor accordingly
// check if a drag with acceptable content is in progress
//

let Some(dragged_item_id) = egui::DragAndDrop::payload(ui.ctx()).map(|payload| *payload)
let Some(dragged_payload) = egui::DragAndDrop::payload::<DragAndDropPayload>(ui.ctx())
else {
// nothing is being dragged, so nothing to do
return;
};

ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing);
let DragAndDropPayload::Contents {
contents: dragged_contents,
} = dragged_payload.as_ref()
else {
// nothing we care about is being dragged
return;
};

//
// find the drop target
Expand All @@ -668,39 +672,34 @@ impl BlueprintTree {
);

if let Some(drop_target) = drop_target {
self.handle_drop_target(viewport, ui, dragged_item_id, &drop_target);
self.handle_contents_drop_target(viewport, ui, dragged_contents, &drop_target);
}
}

fn handle_drag_and_drop_interaction(
&mut self,
ctx: &ViewerContext<'_>,
viewport: &ViewportBlueprint,
ui: &egui::Ui,
contents: Contents,
response: &egui::Response,
body_response: Option<&egui::Response>,
) {
//
// initiate drag and force single-selection
//

if response.drag_started() {
ctx.selection_state().set_selection(contents.as_item());
egui::DragAndDrop::set_payload(ui.ctx(), contents);
}

//
// check if a drag is in progress and set the cursor accordingly
// check if a drag with acceptable content is in progress
//

let Some(dragged_item_id) = egui::DragAndDrop::payload(ui.ctx()).map(|payload| *payload)
let Some(dragged_payload) = egui::DragAndDrop::payload::<DragAndDropPayload>(ui.ctx())
else {
// nothing is being dragged, so nothing to do
return;
};

ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing);
let DragAndDropPayload::Contents {
contents: dragged_contents,
} = dragged_payload.as_ref()
else {
// nothing we care about is being dragged
return;
};

//
// find our parent, our position within parent, and the previous container (if any)
Expand Down Expand Up @@ -752,7 +751,7 @@ impl BlueprintTree {
);

if let Some(drop_target) = drop_target {
self.handle_drop_target(viewport, ui, dragged_item_id, &drop_target);
self.handle_contents_drop_target(viewport, ui, dragged_contents, &drop_target);
}
}

Expand All @@ -763,16 +762,21 @@ impl BlueprintTree {
empty_space: egui::Rect,
) {
//
// check if a drag is in progress and set the cursor accordingly
// check if a drag with acceptable content is in progress
//

let Some(dragged_item_id) = egui::DragAndDrop::payload(ui.ctx()).map(|payload| *payload)
let Some(dragged_payload) = egui::DragAndDrop::payload::<DragAndDropPayload>(ui.ctx())
else {
// nothing is being dragged, so nothing to do
return;
};

ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing);
let DragAndDropPayload::Contents {
contents: dragged_contents,
} = dragged_payload.as_ref()
else {
// nothing we care about is being dragged
return;
};

//
// prepare a drop target corresponding to "insert last in root container"
Expand All @@ -788,25 +792,31 @@ impl BlueprintTree {
usize::MAX,
);

self.handle_drop_target(viewport, ui, dragged_item_id, &drop_target);
self.handle_contents_drop_target(viewport, ui, dragged_contents, &drop_target);
}
}

fn handle_drop_target(
fn handle_contents_drop_target(
&mut self,
viewport: &ViewportBlueprint,
ui: &Ui,
dragged_item_id: Contents,
dragged_contents: &[Contents],
drop_target: &DropTarget<Contents>,
) {
// We cannot allow the target location to be "inside" the dragged item, because that would amount moving
// myself inside of me.
if let Contents::Container(dragged_container_id) = &dragged_item_id {
if viewport
.is_contents_in_container(&drop_target.target_parent_id, dragged_container_id)
{
return;
// We cannot allow the target location to be "inside" any of the dragged items, because that
// would amount to moving myself inside of me.
let parent_contains_dragged_content = |content: &Contents| {
if let Contents::Container(dragged_container_id) = content {
if viewport
.is_contents_in_container(&drop_target.target_parent_id, dragged_container_id)
{
return true;
}
}
false
};
if dragged_contents.iter().any(parent_contains_dragged_content) {
return;
}

ui.painter().hline(
Expand All @@ -822,7 +832,7 @@ impl BlueprintTree {

if ui.input(|i| i.pointer.any_released()) {
viewport.move_contents(
dragged_item_id,
dragged_contents.to_vec(),
target_container_id,
drop_target.target_position_index,
);
Expand Down
2 changes: 1 addition & 1 deletion crates/viewer/re_data_ui/src/instance_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ fn component_list_ui(
});

if interactive {
ctx.select_hovered_on_click(&response, item);
ctx.handle_select_hover_drag_interactions(&response, item, false);
}
}
});
Expand Down
2 changes: 1 addition & 1 deletion crates/viewer/re_data_ui/src/item_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,7 @@ pub fn cursor_interact_with_selectable(
let is_item_hovered =
ctx.selection_state().highlight_for_ui_element(&item) == HoverHighlight::Hovered;

ctx.select_hovered_on_click(&response, item);
ctx.handle_select_hover_drag_interactions(&response, item, false);
// TODO(andreas): How to deal with shift click for selecting ranges?

if is_item_hovered {
Expand Down
4 changes: 2 additions & 2 deletions crates/viewer/re_selection_panel/src/selection_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,7 @@ fn list_existing_data_blueprints(

// We don't use item_ui::cursor_interact_with_selectable here because the forced
// hover background is distracting and not useful.
ctx.select_hovered_on_click(&response, item);
ctx.handle_select_hover_drag_interactions(&response, item, false);
}
}
}
Expand Down Expand Up @@ -926,7 +926,7 @@ fn show_list_item_for_container_child(
&response,
SelectionUpdateBehavior::Ignore,
);
ctx.select_hovered_on_click(&response, item);
ctx.handle_select_hover_drag_interactions(&response, item, false);

if remove_contents {
viewport.mark_user_interaction(ctx);
Expand Down
Loading

0 comments on commit 704438f

Please sign in to comment.