From a2cece84f1e37d5594d3e8562b2628939eda18a9 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Wed, 4 Dec 2024 21:56:28 +0100 Subject: [PATCH 01/14] - initiate drag from inside `ctx.select_hovered_on_click()` - add `undraggable_items` to `ViewerContext` and populate with root container --- Cargo.lock | 22 ++-- Cargo.toml | 14 +-- .../re_blueprint_tree/src/blueprint_tree.rs | 109 ++++++++++-------- crates/viewer/re_data_ui/src/instance_path.rs | 2 +- crates/viewer/re_data_ui/src/item_ui.rs | 2 +- .../re_selection_panel/src/selection_panel.rs | 4 +- .../src/space_view_class.rs | 1 + .../src/dataframe_ui.rs | 3 +- .../viewer/re_space_view_graph/src/ui/draw.rs | 5 +- .../re_space_view_map/src/map_space_view.rs | 1 + .../re_space_view_spatial/src/picking_ui.rs | 2 +- .../src/space_view_class.rs | 2 +- crates/viewer/re_time_panel/src/lib.rs | 4 +- crates/viewer/re_viewer/src/app_state.rs | 14 ++- .../re_viewer/src/ui/recordings_panel.rs | 2 +- .../viewer/re_viewer_context/src/contents.rs | 10 +- .../re_viewer_context/src/drag_and_drop.rs | 105 +++++++++++++++++ crates/viewer/re_viewer_context/src/lib.rs | 2 + .../re_viewer_context/src/selection_state.rs | 14 +++ .../src/space_view/view_context.rs | 10 -- .../re_viewer_context/src/test_context.rs | 3 + .../re_viewer_context/src/viewer_context.rs | 40 ++++++- crates/viewer/re_viewport/src/viewport_ui.rs | 2 +- .../src/color_coordinates_space_view.rs | 6 +- 24 files changed, 282 insertions(+), 97 deletions(-) create mode 100644 crates/viewer/re_viewer_context/src/drag_and_drop.rs diff --git a/Cargo.lock b/Cargo.lock index 13ed1656b5ba..5f7eabcdb372 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1923,7 +1923,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=f687b27efc590b9584bfa6dd0c2f22edaf8c8cef#f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" dependencies = [ "bytemuck", "color-hex", @@ -1940,7 +1940,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=f687b27efc590b9584bfa6dd0c2f22edaf8c8cef#f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" dependencies = [ "ahash", "bytemuck", @@ -1979,7 +1979,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=f687b27efc590b9584bfa6dd0c2f22edaf8c8cef#f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" dependencies = [ "accesskit", "ahash", @@ -1996,7 +1996,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=f687b27efc590b9584bfa6dd0c2f22edaf8c8cef#f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" dependencies = [ "ahash", "bytemuck", @@ -2015,7 +2015,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=f687b27efc590b9584bfa6dd0c2f22edaf8c8cef#f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" dependencies = [ "accesskit_winit", "ahash", @@ -2057,7 +2057,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=f687b27efc590b9584bfa6dd0c2f22edaf8c8cef#f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" dependencies = [ "ahash", "egui", @@ -2074,7 +2074,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=f687b27efc590b9584bfa6dd0c2f22edaf8c8cef#f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" dependencies = [ "ahash", "bytemuck", @@ -2092,7 +2092,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=f687b27efc590b9584bfa6dd0c2f22edaf8c8cef#f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" dependencies = [ "dify", "egui", @@ -2161,7 +2161,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=f687b27efc590b9584bfa6dd0c2f22edaf8c8cef#f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" dependencies = [ "bytemuck", "serde", @@ -2277,7 +2277,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=f687b27efc590b9584bfa6dd0c2f22edaf8c8cef#f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" dependencies = [ "ab_glyph", "ahash", @@ -2296,7 +2296,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=f687b27efc590b9584bfa6dd0c2f22edaf8c8cef#f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" [[package]] name = "equivalent" diff --git a/Cargo.toml b/Cargo.toml index 3a0ad88ae8cd..05ddd0ae4b8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -559,13 +559,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 = "f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" } # egui master 2024-12-04 +eframe = { git = "https://github.com/emilk/egui.git", rev = "f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" } # egui master 2024-12-04 +egui = { git = "https://github.com/emilk/egui.git", rev = "f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" } # egui master 2024-12-04 +egui_extras = { git = "https://github.com/emilk/egui.git", rev = "f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" } # egui master 2024-12-04 +egui_kittest = { git = "https://github.com/emilk/egui.git", rev = "f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" } # egui master 2024-12-04 +egui-wgpu = { git = "https://github.com/emilk/egui.git", rev = "f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" } # egui master 2024-12-04 +emath = { git = "https://github.com/emilk/egui.git", rev = "f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" } # egui master 2024-12-04 # Useful while developing: # ecolor = { path = "../../egui/crates/ecolor" } diff --git a/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs b/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs index b6ea2ae356d4..62efc89ede41 100644 --- a/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs +++ b/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs @@ -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, SpaceViewId, ViewerContext, @@ -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, @@ -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.select_hovered_on_click(&item_response, item, true); self.handle_root_container_drag_and_drop_interaction( viewport, @@ -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.select_hovered_on_click(&response, item, true); viewport.set_content_visibility(ctx, &content, visible); self.handle_drag_and_drop_interaction( - ctx, viewport, ui, content, @@ -410,13 +409,12 @@ impl BlueprintTree { SelectionUpdateBehavior::UseSelection, ); self.scroll_to_me_if_needed(ui, &item, &response); - ctx.select_hovered_on_click(&response, item); + ctx.select_hovered_on_click(&response, item, true); let content = Contents::SpaceView(*space_view_id); viewport.set_content_visibility(ctx, &content, visible); self.handle_drag_and_drop_interaction( - ctx, viewport, ui, content, @@ -498,6 +496,7 @@ impl BlueprintTree { let list_item = ui .list_item() + .draggable(true) .selected(is_selected) .force_hovered(is_item_hovered); @@ -603,7 +602,7 @@ impl BlueprintTree { SelectionUpdateBehavior::UseSelection, ); self.scroll_to_me_if_needed(ui, &item, &response); - ctx.select_hovered_on_click(&response, item); + ctx.select_hovered_on_click(&response, item, true); } /// Add a button to trigger the addition of a new space view or container. @@ -643,16 +642,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::(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 @@ -675,13 +679,12 @@ 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, @@ -689,25 +692,21 @@ impl BlueprintTree { 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::(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) @@ -759,7 +758,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); } } @@ -770,16 +769,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::(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" @@ -795,25 +799,30 @@ 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, ) { - // 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. + if dragged_contents.iter().any(|dragged_contents| { + if let Contents::Container(dragged_container_id) = dragged_contents { + if viewport + .is_contents_in_container(&drop_target.target_parent_id, dragged_container_id) + { + return true; + } } + false + }) { + return; } ui.painter().hline( @@ -828,11 +837,17 @@ impl BlueprintTree { }; if ui.input(|i| i.pointer.any_released()) { - viewport.move_contents( - dragged_item_id, - target_container_id, - drop_target.target_position_index, - ); + for (idx, content) in dragged_contents.iter().enumerate() { + // TODO: this is not strictly correct when dragging multiple items inside the same + // container, as the position are all over the place when moving items incrementally. + // We probably need a `viewport.move_multiple_contents()` method which handles this + // correctly. + viewport.move_contents( + *content, + target_container_id, + drop_target.target_position_index + idx, + ); + } egui::DragAndDrop::clear_payload(ui.ctx()); } else { diff --git a/crates/viewer/re_data_ui/src/instance_path.rs b/crates/viewer/re_data_ui/src/instance_path.rs index 7dd2d6b9026d..887b02a4df5f 100644 --- a/crates/viewer/re_data_ui/src/instance_path.rs +++ b/crates/viewer/re_data_ui/src/instance_path.rs @@ -243,7 +243,7 @@ fn component_list_ui( }); if interactive { - ctx.select_hovered_on_click(&response, item); + ctx.select_hovered_on_click(&response, item, false); } } }); diff --git a/crates/viewer/re_data_ui/src/item_ui.rs b/crates/viewer/re_data_ui/src/item_ui.rs index 0bafd68873a2..12625645bb3c 100644 --- a/crates/viewer/re_data_ui/src/item_ui.rs +++ b/crates/viewer/re_data_ui/src/item_ui.rs @@ -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.select_hovered_on_click(&response, item, false); // TODO(andreas): How to deal with shift click for selecting ranges? if is_item_hovered { diff --git a/crates/viewer/re_selection_panel/src/selection_panel.rs b/crates/viewer/re_selection_panel/src/selection_panel.rs index 0930de663823..aede775a7118 100644 --- a/crates/viewer/re_selection_panel/src/selection_panel.rs +++ b/crates/viewer/re_selection_panel/src/selection_panel.rs @@ -656,7 +656,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.select_hovered_on_click(&response, item, false); } } } @@ -907,7 +907,7 @@ fn show_list_item_for_container_child( &response, SelectionUpdateBehavior::Ignore, ); - ctx.select_hovered_on_click(&response, item); + ctx.select_hovered_on_click(&response, item, false); if remove_contents { viewport.mark_user_interaction(ctx); diff --git a/crates/viewer/re_space_view_bar_chart/src/space_view_class.rs b/crates/viewer/re_space_view_bar_chart/src/space_view_class.rs index bb55311d7e39..729d23ac4e4f 100644 --- a/crates/viewer/re_space_view_bar_chart/src/space_view_class.rs +++ b/crates/viewer/re_space_view_bar_chart/src/space_view_class.rs @@ -248,6 +248,7 @@ Display a 1D tensor as a bar chart. query.space_view_id, entity_path.clone().into(), ), + false, ); } }); diff --git a/crates/viewer/re_space_view_dataframe/src/dataframe_ui.rs b/crates/viewer/re_space_view_dataframe/src/dataframe_ui.rs index 45956a268792..88f1f4d849a0 100644 --- a/crates/viewer/re_space_view_dataframe/src/dataframe_ui.rs +++ b/crates/viewer/re_space_view_dataframe/src/dataframe_ui.rs @@ -261,7 +261,7 @@ impl egui_table::TableDelegate for DataframeTableDelegate<'_> { egui::Rect::from_min_size(pos, size), egui::SelectableLabel::new(is_selected, galley), ); - self.ctx.select_hovered_on_click(&response, item); + self.ctx.select_hovered_on_click(&response, item, false); // TODO(emilk): expand column(s) to make sure the text fits (requires egui_table fix). } @@ -324,6 +324,7 @@ impl egui_table::TableDelegate for DataframeTableDelegate<'_> { re_viewer_context::Item::ComponentPath( component_column_descriptor.component_path(), ), + false, ); } } diff --git a/crates/viewer/re_space_view_graph/src/ui/draw.rs b/crates/viewer/re_space_view_graph/src/ui/draw.rs index 21ab773bafa0..24e393687677 100644 --- a/crates/viewer/re_space_view_graph/src/ui/draw.rs +++ b/crates/viewer/re_space_view_graph/src/ui/draw.rs @@ -289,7 +289,7 @@ pub fn draw_graph( InstancePath::instance(entity_path.clone(), instance.instance_index); ctx.select_hovered_on_click( &response, - Item::DataResult(query.space_view_id, instance_path.clone()), + Item::DataResult(query.space_view_id, instance_path.clone(), false), ); response = response.on_hover_ui_at_pointer(|ui| { @@ -336,7 +336,8 @@ pub fn draw_graph( let instance_path = InstancePath::entity_all(entity_path.clone()); ctx.select_hovered_on_click( &resp, - vec![(Item::DataResult(query.space_view_id, instance_path), None)].into_iter(), + Item::DataResult(query.space_view_id, instance_path), + false, ); } } diff --git a/crates/viewer/re_space_view_map/src/map_space_view.rs b/crates/viewer/re_space_view_map/src/map_space_view.rs index 2700a4b3b93d..481b55748324 100644 --- a/crates/viewer/re_space_view_map/src/map_space_view.rs +++ b/crates/viewer/re_space_view_map/src/map_space_view.rs @@ -484,6 +484,7 @@ fn handle_ui_interactions( ctx.select_hovered_on_click( &map_response, Item::DataResult(query.space_view_id, instance_path.clone()), + false, ); // double click selects the entire entity diff --git a/crates/viewer/re_space_view_spatial/src/picking_ui.rs b/crates/viewer/re_space_view_spatial/src/picking_ui.rs index bfd8e096fb88..e9962df58a26 100644 --- a/crates/viewer/re_space_view_spatial/src/picking_ui.rs +++ b/crates/viewer/re_space_view_spatial/src/picking_ui.rs @@ -209,7 +209,7 @@ pub fn picking( }); }; - ctx.select_hovered_on_click(&response, hovered_items.into_iter()); + ctx.select_hovered_on_click(&response, hovered_items.into_iter(), false); Ok(response) } diff --git a/crates/viewer/re_space_view_time_series/src/space_view_class.rs b/crates/viewer/re_space_view_time_series/src/space_view_class.rs index 2b1b8581ca1d..c01ab1ece767 100644 --- a/crates/viewer/re_space_view_time_series/src/space_view_class.rs +++ b/crates/viewer/re_space_view_time_series/src/space_view_class.rs @@ -536,7 +536,7 @@ Display time series data in a plot. } }) { - ctx.select_hovered_on_click(&response, hovered); + ctx.select_hovered_on_click(&response, hovered, false); } } diff --git a/crates/viewer/re_time_panel/src/lib.rs b/crates/viewer/re_time_panel/src/lib.rs index 8cb1807522a6..39dd59764cdb 100644 --- a/crates/viewer/re_time_panel/src/lib.rs +++ b/crates/viewer/re_time_panel/src/lib.rs @@ -726,7 +726,7 @@ impl TimePanel { &response, SelectionUpdateBehavior::UseSelection, ); - ctx.select_hovered_on_click(&response, item.to_item()); + ctx.select_hovered_on_click(&response, item.to_item(), false); let is_closed = body_response.is_none(); let response_rect = response.rect; @@ -854,7 +854,7 @@ impl TimePanel { &response, SelectionUpdateBehavior::UseSelection, ); - ctx.select_hovered_on_click(&response, item.to_item()); + ctx.select_hovered_on_click(&response, item.to_item(), false); let response_rect = response.rect; diff --git a/crates/viewer/re_viewer/src/app_state.rs b/crates/viewer/re_viewer/src/app_state.rs index 36d6d5cebe78..63514bf11c64 100644 --- a/crates/viewer/re_viewer/src/app_state.rs +++ b/crates/viewer/re_viewer/src/app_state.rs @@ -8,9 +8,10 @@ use re_smart_channel::ReceiveSet; use re_types::blueprint::components::PanelState; use re_ui::{ContextExt as _, DesignTokens}; use re_viewer_context::{ - AppOptions, ApplicationSelectionState, BlueprintUndoState, CommandSender, ComponentUiRegistry, - PlayState, RecordingConfig, SpaceViewClassExt as _, SpaceViewClassRegistry, StoreContext, - StoreHub, SystemCommandSender as _, ViewStates, ViewerContext, + drag_and_drop_payload_cursor_ui, AppOptions, ApplicationSelectionState, BlueprintUndoState, + CommandSender, ComponentUiRegistry, PlayState, RecordingConfig, SpaceViewClassExt as _, + SpaceViewClassRegistry, StoreContext, StoreHub, SystemCommandSender as _, ViewStates, + ViewerContext, }; use re_viewport::ViewportUi; use re_viewport_blueprint::ui::add_space_view_or_container_modal_ui; @@ -214,6 +215,10 @@ impl AppState { )), ); + // The root container cannot be dragged. + let undraggable_items = + re_viewer_context::Item::Container(viewport_ui.blueprint.root_container).into(); + let applicable_entities_per_visualizer = space_view_class_registry .applicable_entities_for_visualizer_systems(&recording.store_id()); let indicated_entities_per_visualizer = @@ -271,6 +276,7 @@ impl AppState { render_ctx: Some(render_ctx), command_sender, focused_item, + undraggable_items: &undraggable_items, }; // We move the time at the very start of the frame, @@ -343,6 +349,7 @@ impl AppState { render_ctx: Some(render_ctx), command_sender, focused_item, + undraggable_items: &undraggable_items, }; if *show_settings_ui { @@ -509,6 +516,7 @@ impl AppState { // add_space_view_or_container_modal_ui(&ctx, &viewport_ui.blueprint, ui); + drag_and_drop_payload_cursor_ui(ui); // Process deferred layout operations and apply updates back to blueprint: viewport_ui.save_to_blueprint_store(&ctx, space_view_class_registry); diff --git a/crates/viewer/re_viewer/src/ui/recordings_panel.rs b/crates/viewer/re_viewer/src/ui/recordings_panel.rs index 49457eda91a4..1180a2a3bf99 100644 --- a/crates/viewer/re_viewer/src/ui/recordings_panel.rs +++ b/crates/viewer/re_viewer/src/ui/recordings_panel.rs @@ -222,7 +222,7 @@ fn app_and_its_recordings_ui( app_id.data_ui_recording(ctx, ui, UiLayout::Tooltip); }); - ctx.select_hovered_on_click(&item_response, app_item); + ctx.select_hovered_on_click(&item_response, app_item, false); if item_response.clicked() { // Switch to this application: diff --git a/crates/viewer/re_viewer_context/src/contents.rs b/crates/viewer/re_viewer_context/src/contents.rs index a0c0d8626f42..e987f7f8df1b 100644 --- a/crates/viewer/re_viewer_context/src/contents.rs +++ b/crates/viewer/re_viewer_context/src/contents.rs @@ -5,7 +5,7 @@ use egui_tiles::TileId; use re_log_types::EntityPath; use crate::item::Item; -use crate::{BlueprintId, BlueprintIdRegistry, ContainerId, SpaceViewId}; +use crate::{BlueprintId, BlueprintIdRegistry, ContainerId, ItemCollection, SpaceViewId}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Contents { @@ -65,6 +65,14 @@ impl Contents { } } +impl TryFrom<&ItemCollection> for Vec { + type Error = (); + + fn try_from(value: &ItemCollection) -> Result { + value.iter().map(|(item, _)| item.try_into()).collect() + } +} + impl TryFrom for Contents { type Error = (); diff --git a/crates/viewer/re_viewer_context/src/drag_and_drop.rs b/crates/viewer/re_viewer_context/src/drag_and_drop.rs new file mode 100644 index 000000000000..b1e5e2ee67e5 --- /dev/null +++ b/crates/viewer/re_viewer_context/src/drag_and_drop.rs @@ -0,0 +1,105 @@ +//! Implement a global drag-and-drop payload type that enable dragging from various parts of the UI +//! (e.g., from the streams tree to the viewport, etc.). + +use std::fmt::Formatter; + +use crate::{Contents, ItemCollection}; + +//TODO(ab): add more type of things we can drag, in particular entity paths +#[derive(Debug)] +pub enum DragAndDropPayload { + /// The dragged content is made only of [`Contents`]. + Contents { contents: Vec }, + + /// The dragged content is made of a collection of [`Item`]s we do know how to handle. + Invalid { items: crate::ItemCollection }, +} + +impl DragAndDropPayload { + pub fn from_items(selected_items: ItemCollection) -> Self { + if let Ok(contents) = (&selected_items).try_into() { + Self::Contents { contents } + } else { + Self::Invalid { + items: selected_items, + } + } + } +} + +impl std::fmt::Display for DragAndDropPayload { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Contents { contents } => { + let (container_cnt, view_cnt) = contents.iter().fold( + (0, 0), + |(container_cnt, view_cnt), content| match content { + Contents::Container(_) => (container_cnt + 1, view_cnt), + Contents::SpaceView(_) => (container_cnt, view_cnt + 1), + }, + ); + + match (container_cnt, view_cnt) { + (0, 0) => write!(f, "empty"), // should not happen + (n, 0) => write!(f, "{n} container{}", if n > 1 { "s" } else { "" }), + (0, n) => write!(f, "{n} view{}", if n > 1 { "s" } else { "" }), + (container_cnt, view_cnt) => { + write!( + f, + "{container_cnt} container{}, {view_cnt} view{}", + if container_cnt > 1 { "s" } else { "" }, + if view_cnt > 1 { "s" } else { "" } + ) + } + } + } + Self::Invalid { .. } => { + //TODO + write!(f, "invalid") + } + } + } +} + +/// Display the currently dragged payload as a pill in the UI. +/// +/// This should be called once per frame. +pub fn drag_and_drop_payload_cursor_ui(ui: &mut egui::Ui) { + if let Some(payload) = egui::DragAndDrop::payload::(ui.ctx()) { + if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() { + let layer_id = egui::LayerId::new( + egui::Order::Tooltip, + egui::Id::new("drag_and_drop_payload_layer"), + ); + let response = ui + .scope_builder(egui::UiBuilder::new().layer_id(layer_id), |ui| { + //TODO: adjust look based on design + egui::Frame { + rounding: egui::Rounding::same(100.0), // max the rounding for a pill look + fill: ui.visuals().widgets.active.text_color(), + inner_margin: egui::Margin { + left: 8.0, + right: 8.0, + top: 4.0, + bottom: 3.0, + }, + ..Default::default() + } + .show(ui, |ui| { + ui.label( + egui::RichText::new(payload.to_string()).color(ui.visuals().panel_fill), + ) + }) + .inner + }) + .response; + + //TODO: adjust geometry + let delta = pointer_pos - response.rect.right_bottom(); + ui.ctx() + .transform_layer_shapes(layer_id, emath::TSTransform::from_translation(delta)); + } + } +} + +//TODO: capture escape key to cancel drag diff --git a/crates/viewer/re_viewer_context/src/lib.rs b/crates/viewer/re_viewer_context/src/lib.rs index d52ce46456b5..4bd7630919d2 100644 --- a/crates/viewer/re_viewer_context/src/lib.rs +++ b/crates/viewer/re_viewer_context/src/lib.rs @@ -13,6 +13,7 @@ mod component_fallbacks; mod component_ui_registry; mod contents; mod data_result_node_or_path; +mod drag_and_drop; mod file_dialog; mod image_info; mod item; @@ -52,6 +53,7 @@ pub use self::{ component_ui_registry::{ComponentUiRegistry, ComponentUiTypes, UiLayout}, contents::{blueprint_id_to_tile_id, Contents, ContentsName}, data_result_node_or_path::DataResultNodeOrPath, + drag_and_drop::{drag_and_drop_payload_cursor_ui, DragAndDropPayload}, file_dialog::santitize_file_name, image_info::{ColormapWithRange, ImageInfo}, item::Item, diff --git a/crates/viewer/re_viewer_context/src/selection_state.rs b/crates/viewer/re_viewer_context/src/selection_state.rs index 202427cac510..c4d83a532447 100644 --- a/crates/viewer/re_viewer_context/src/selection_state.rs +++ b/crates/viewer/re_viewer_context/src/selection_state.rs @@ -104,6 +104,15 @@ where } } +impl IntoIterator for ItemCollection { + type Item = (Item, Option); + type IntoIter = indexmap::map::IntoIter>; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + impl ItemCollection { /// For each item in this selection, if it refers to the first element of an instance with a /// single element, resolve it to a unindexed entity path. @@ -261,6 +270,11 @@ impl ApplicationSelectionState { *self.selection_this_frame.lock() = items.into(); } + /// Extend the selection with the provided items. + pub fn extend_selection(&self, items: impl Into) { + self.selection_this_frame.lock().extend(items.into()); + } + /// Returns the current selection. pub fn selected_items(&self) -> &ItemCollection { &self.selection_previous_frame diff --git a/crates/viewer/re_viewer_context/src/space_view/view_context.rs b/crates/viewer/re_viewer_context/src/space_view/view_context.rs index 6ed6a3e30595..a8e996fdd4b9 100644 --- a/crates/viewer/re_viewer_context/src/space_view/view_context.rs +++ b/crates/viewer/re_viewer_context/src/space_view/view_context.rs @@ -87,16 +87,6 @@ impl<'a> ViewContext<'a> { self.viewer_ctx.current_query() } - /// Set hover/select/focus for a given selection based on an egui response. - #[inline] - pub fn select_hovered_on_click( - &self, - response: &egui::Response, - selection: impl Into, - ) { - self.viewer_ctx.select_hovered_on_click(response, selection); - } - #[inline] pub fn lookup_query_result(&self, id: SpaceViewId) -> &DataQueryResult { self.viewer_ctx.lookup_query_result(id) diff --git a/crates/viewer/re_viewer_context/src/test_context.rs b/crates/viewer/re_viewer_context/src/test_context.rs index c2c75e74babb..7485309683c1 100644 --- a/crates/viewer/re_viewer_context/src/test_context.rs +++ b/crates/viewer/re_viewer_context/src/test_context.rs @@ -90,6 +90,8 @@ impl TestContext { hub: &Default::default(), }; + let undraggable_items = Default::default(); + let ctx = ViewerContext { app_options: &Default::default(), cache: &Default::default(), @@ -108,6 +110,7 @@ impl TestContext { render_ctx: None, command_sender: &self.command_sender, focused_item: &None, + undraggable_items: &undraggable_items, }; func(&ctx); diff --git a/crates/viewer/re_viewer_context/src/viewer_context.rs b/crates/viewer/re_viewer_context/src/viewer_context.rs index 5e8cd4f8ce07..e40716c1a020 100644 --- a/crates/viewer/re_viewer_context/src/viewer_context.rs +++ b/crates/viewer/re_viewer_context/src/viewer_context.rs @@ -5,6 +5,7 @@ use re_chunk_store::LatestAtQuery; use re_entity_db::entity_db::EntityDb; use re_query::StorageEngineReadGuard; +use crate::drag_and_drop::DragAndDropPayload; use crate::{ query_context::DataQueryResult, AppOptions, ApplicableEntities, ApplicationSelectionState, Caches, CommandSender, ComponentUiRegistry, IndicatedEntities, ItemCollection, PerVisualizer, @@ -79,6 +80,9 @@ pub struct ViewerContext<'a> { /// The focused item is cleared every frame, but views may react with side-effects /// that last several frames. pub focused_item: &'a Option, + + /// If a selection contains any `undraggable_items`, it may not be dragged. + pub undraggable_items: &'a ItemCollection, } impl ViewerContext<'_> { @@ -131,11 +135,13 @@ impl ViewerContext<'_> { self.rec_cfg.time_ctrl.read().current_query() } + //TODO: rename this method /// Set hover/select/focus for a given selection based on an egui response. pub fn select_hovered_on_click( &self, response: &egui::Response, selection: impl Into, + can_be_dragged: bool, ) { re_tracing::profile_function!(); @@ -146,14 +152,40 @@ impl ViewerContext<'_> { selection_state.set_hovered(selection.clone()); } - if response.double_clicked() { + if can_be_dragged && response.drag_started() { + let mut selected_items = selection_state.selected_items().clone(); + let is_already_selected = selection + .iter() + .all(|(item, _)| selected_items.contains_item(item)); + if !is_already_selected { + if response.ctx.input(|i| i.modifiers.command) { + selected_items.extend(selection); + } else { + selected_items = selection; + } + selection_state.set_selection(selected_items.clone()); + } + + let is_selection_draggable = self + .undraggable_items + .iter_items() + .all(|item| !selected_items.contains_item(item)); + + let payload = if is_selection_draggable { + DragAndDropPayload::from_items(selected_items) + } else { + DragAndDropPayload::Invalid { + items: selected_items, + } + }; + + egui::DragAndDrop::set_payload(&response.ctx, payload); + } else if response.double_clicked() { if let Some(item) = selection.first_item() { self.command_sender .send_system(crate::SystemCommand::SetFocus(item.clone())); } - } - - if response.clicked() { + } else if response.clicked() { if response.ctx.input(|i| i.modifiers.command) { selection_state.toggle_selection(selection); } else { diff --git a/crates/viewer/re_viewport/src/viewport_ui.rs b/crates/viewer/re_viewport/src/viewport_ui.rs index ff03ad6cc05a..37104f81f024 100644 --- a/crates/viewer/re_viewport/src/viewport_ui.rs +++ b/crates/viewer/re_viewport/src/viewport_ui.rs @@ -597,7 +597,7 @@ impl<'a> egui_tiles::Behavior for TilesDelegate<'a, '_> { &response, SelectionUpdateBehavior::OverrideSelection, ); - self.ctx.select_hovered_on_click(&response, item); + self.ctx.select_hovered_on_click(&response, item, false); } response diff --git a/examples/rust/custom_space_view/src/color_coordinates_space_view.rs b/examples/rust/custom_space_view/src/color_coordinates_space_view.rs index 9bcec0ca9ec1..7b78fbc17b55 100644 --- a/examples/rust/custom_space_view/src/color_coordinates_space_view.rs +++ b/examples/rust/custom_space_view/src/color_coordinates_space_view.rs @@ -279,7 +279,11 @@ fn color_space_ui( ctx.recording(), ); }); - ctx.select_hovered_on_click(&interact, Item::DataResult(query.space_view_id, instance)); + ctx.select_hovered_on_click( + &interact, + Item::DataResult(query.space_view_id, instance), + false, + ); } } From 9a92effac550a3503b527e61af9243038b202e8a Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Thu, 5 Dec 2024 15:20:25 +0100 Subject: [PATCH 02/14] post rebase --- Cargo.lock | 1 + crates/build/re_types_builder/src/codegen/rust/api.rs | 2 +- crates/viewer/re_space_view_graph/src/ui/draw.rs | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5f7eabcdb372..227a9ebb4ad8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6587,6 +6587,7 @@ dependencies = [ "egui_extras", "egui_kittest", "egui_tiles", + "nohash-hasher", "once_cell", "parking_lot", "rand", diff --git a/crates/build/re_types_builder/src/codegen/rust/api.rs b/crates/build/re_types_builder/src/codegen/rust/api.rs index 90b01d69e1a2..0f5ba7c5f8a4 100644 --- a/crates/build/re_types_builder/src/codegen/rust/api.rs +++ b/crates/build/re_types_builder/src/codegen/rust/api.rs @@ -1085,7 +1085,7 @@ fn quote_trait_impls_for_archetype(obj: &Object) -> TokenStream { let quoted_field_names = obj .fields - .iter() + .ite .map(|field| format_ident!("{}", field.name)) .collect::>(); diff --git a/crates/viewer/re_space_view_graph/src/ui/draw.rs b/crates/viewer/re_space_view_graph/src/ui/draw.rs index 24e393687677..e0f132cf6e4f 100644 --- a/crates/viewer/re_space_view_graph/src/ui/draw.rs +++ b/crates/viewer/re_space_view_graph/src/ui/draw.rs @@ -289,7 +289,8 @@ pub fn draw_graph( InstancePath::instance(entity_path.clone(), instance.instance_index); ctx.select_hovered_on_click( &response, - Item::DataResult(query.space_view_id, instance_path.clone(), false), + Item::DataResult(query.space_view_id, instance_path.clone()), + false, ); response = response.on_hover_ui_at_pointer(|ui| { From ded726527a345cb77396c100e9e671b391994341 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Thu, 5 Dec 2024 19:14:23 +0100 Subject: [PATCH 03/14] Better display of the drag item --- Cargo.lock | 1 - .../re_types_builder/src/codegen/rust/api.rs | 2 +- .../viewer/re_ui/data/icons/dnd_add_new.png | Bin 0 -> 430 bytes .../re_ui/data/icons/dnd_add_to_existing.png | Bin 0 -> 557 bytes .../re_ui/data/icons/dnd_cannot_use.png | Bin 0 -> 685 bytes crates/viewer/re_ui/data/icons/dnd_move.png | Bin 0 -> 450 bytes crates/viewer/re_ui/src/design_tokens.rs | 2 +- crates/viewer/re_ui/src/icons.rs | 6 + crates/viewer/re_ui/src/ui_ext.rs | 15 ++ .../re_viewer_context/src/drag_and_drop.rs | 162 +++++++++++++----- 10 files changed, 139 insertions(+), 49 deletions(-) create mode 100644 crates/viewer/re_ui/data/icons/dnd_add_new.png create mode 100644 crates/viewer/re_ui/data/icons/dnd_add_to_existing.png create mode 100644 crates/viewer/re_ui/data/icons/dnd_cannot_use.png create mode 100644 crates/viewer/re_ui/data/icons/dnd_move.png diff --git a/Cargo.lock b/Cargo.lock index 227a9ebb4ad8..5f7eabcdb372 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6587,7 +6587,6 @@ dependencies = [ "egui_extras", "egui_kittest", "egui_tiles", - "nohash-hasher", "once_cell", "parking_lot", "rand", diff --git a/crates/build/re_types_builder/src/codegen/rust/api.rs b/crates/build/re_types_builder/src/codegen/rust/api.rs index 0f5ba7c5f8a4..90b01d69e1a2 100644 --- a/crates/build/re_types_builder/src/codegen/rust/api.rs +++ b/crates/build/re_types_builder/src/codegen/rust/api.rs @@ -1085,7 +1085,7 @@ fn quote_trait_impls_for_archetype(obj: &Object) -> TokenStream { let quoted_field_names = obj .fields - .ite + .iter() .map(|field| format_ident!("{}", field.name)) .collect::>(); diff --git a/crates/viewer/re_ui/data/icons/dnd_add_new.png b/crates/viewer/re_ui/data/icons/dnd_add_new.png new file mode 100644 index 0000000000000000000000000000000000000000..ae2caea2210872e2935c9992651373d10e150ccf GIT binary patch literal 430 zcmV;f0a5;mP)c=iRF+qfCsC~sW7kUkqC24#bfeqs% zn3vV${%JOb1I{}X0!MIQKT^6^f`QH9p?tj~P}ngNr+UcC*~IkElH(D41nH|b4R|rz zvFbmR1*>3|#J?ieWuZlv&DrQr>23%Tr1fyD!g^?yah{x9!jbK!l$qaDzS2Tnp73Pa ztih3#cyHS~mD}_)k_+`(e?RW1z*VV+1E#orp?7wudyfz0z@+;bg6A{V{meSLhn_$A Y1n>CaR2}S07*qoM6N<$f?Yea>;M1& literal 0 HcmV?d00001 diff --git a/crates/viewer/re_ui/data/icons/dnd_add_to_existing.png b/crates/viewer/re_ui/data/icons/dnd_add_to_existing.png new file mode 100644 index 0000000000000000000000000000000000000000..815fd872b22a319f96b4500aefb8c5b1b7f75879 GIT binary patch literal 557 zcmV+|0@D47P)IvWmlpEj)=n>!q;7X|`ETKlIo5J{SZAgg0XlhBWpg6!a(5vHiUsu)iEyFQSE5)tK^5@t*_4-r0EI>FKlPhzIPt05CW(* zN~3@L{5#)_Eh8fi%_jga5^EZ;%6`@lI`vC=B{TPDZ;#!c91-QbI)wmuH5AfvZf_@y zpC!do)BHA>Ad)@(1g=EM%d+MMaD(2U;RK0Kka7YhH{b>_D+XuLvPznnAajGTV_MMT zeg2uY#)_5D?)uF*QZ#z{q)+chM31;lgI<{V ztkX(3otcOg52PEFLz$RmMVGtsvtN!Uq}BVQ=ivoyOE_Ja5nJwqgqg=rp6RoLFkIc9 z^JQj&4mMM4Do{!d4_|~aZHn}A`7xb;=XAb2J2$55WAk;k9OgD+>zbpR2B(*1K&(#W zRPoWvy?*^v&c(Fmyd$>hVmWG>^N*x(4zhd>ve&)6A$bCu6q_OJlp;|?ohnvu4trs; z)-au$iO6DT1Aq)tLQ?$46<`hdCiS^@B!vTr9+61med)JPRw;rjfRjl#$`x}FiWG0r z?!v{lB&z_AMvK^W5_ZTQOLO59#Oug_($TUVBnlTHlZN}ZdSn~f4OC{!tkg*81Anq+ z+&v-l4`~9rCo(0e&szHFCR7S}Flfpwo07rf8F^N7;w03%@Xo7Ww*Nn5K!^TBXi-Vl zPOJjII%Pemec=lHo%(=cidoU*`q~bJY_*Mpl!^pWuHJbWph6UtV)K)=Dbg4gs1VF{ zoKL7KKnrjMdHu{4v16AaYYQ-TDFqdUH6yn1E_IT676Q@lD>@aT6Usx1KVZYIHCvw; zMY*C;p41B#L8>uIt-k>*vrPhr*s|gZspa-zS`v(RJ8Q{HX8#esOz!ci~ zGVOa`N|ccD>*wL^b3UeoWMqs7)>aE>D4)fB*pG514*aplV=%$#@pplpMWdZiT9V zP1x;dn1|mn573ut_NZCwLUWSr=C3CAbT=9nyMF=t`Z2>T?k{CEpOn@2gc4$|(zZE0 z5KXNufJ50Qhb>@eYIZcBmf^bQSj%KhSgiC@b8~fB#QHSr0~XTk(*WhQu0&O^hiWCJ zYyYi1pXeSR=;aJey}Ed?>|V{-kW~Yl!7S8` pub const BREADCRUMBS_SEPARATOR: Icon = icon_from_path!("../data/icons/breadcrumbs_separator.png"); diff --git a/crates/viewer/re_ui/src/ui_ext.rs b/crates/viewer/re_ui/src/ui_ext.rs index d791d2edb20f..210814ac99c6 100644 --- a/crates/viewer/re_ui/src/ui_ext.rs +++ b/crates/viewer/re_ui/src/ui_ext.rs @@ -131,6 +131,21 @@ pub trait UiExt { ) } + /// Adds a non-interactive, optionally tinted small icon. + /// + /// Uses [`DesignTokens::small_icon_size`]. Returns the rect where the icon was painted. + fn small_icon(&mut self, icon: &Icon, tint: Option) -> egui::Rect { + let ui = self.ui_mut(); + let (_, rect) = ui.allocate_space(DesignTokens::small_icon_size()); + let mut image = icon.as_image(); + if let Some(tint) = tint { + image = image.tint(tint); + } + image.paint_at(ui, rect); + + rect + } + fn medium_icon_toggle_button(&mut self, icon: &Icon, selected: &mut bool) -> egui::Response { let size_points = egui::Vec2::splat(16.0); // TODO(emilk): get from design tokens diff --git a/crates/viewer/re_viewer_context/src/drag_and_drop.rs b/crates/viewer/re_viewer_context/src/drag_and_drop.rs index b1e5e2ee67e5..1feb9c73f617 100644 --- a/crates/viewer/re_viewer_context/src/drag_and_drop.rs +++ b/crates/viewer/re_viewer_context/src/drag_and_drop.rs @@ -3,7 +3,15 @@ use std::fmt::Formatter; -use crate::{Contents, ItemCollection}; +use itertools::Itertools; + +use re_ui::{ + ColorToken, Hue, + Shade::{S325, S375}, + UiExt, +}; + +use crate::{Contents, Item, ItemCollection}; //TODO(ab): add more type of things we can drag, in particular entity paths #[derive(Debug)] @@ -30,33 +38,16 @@ impl DragAndDropPayload { impl std::fmt::Display for DragAndDropPayload { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - Self::Contents { contents } => { - let (container_cnt, view_cnt) = contents.iter().fold( - (0, 0), - |(container_cnt, view_cnt), content| match content { - Contents::Container(_) => (container_cnt + 1, view_cnt), - Contents::SpaceView(_) => (container_cnt, view_cnt + 1), - }, - ); - - match (container_cnt, view_cnt) { - (0, 0) => write!(f, "empty"), // should not happen - (n, 0) => write!(f, "{n} container{}", if n > 1 { "s" } else { "" }), - (0, n) => write!(f, "{n} view{}", if n > 1 { "s" } else { "" }), - (container_cnt, view_cnt) => { - write!( - f, - "{container_cnt} container{}, {view_cnt} view{}", - if container_cnt > 1 { "s" } else { "" }, - if view_cnt > 1 { "s" } else { "" } - ) - } - } - } - Self::Invalid { .. } => { - //TODO - write!(f, "invalid") - } + Self::Contents { contents } => items_to_string( + contents + .iter() + .map(|content| content.as_item()) + .collect_vec() + .iter(), + ) + .fmt(f), + + Self::Invalid { items } => items_to_string(items.iter_items()).fmt(f), } } } @@ -67,34 +58,35 @@ impl std::fmt::Display for DragAndDropPayload { pub fn drag_and_drop_payload_cursor_ui(ui: &mut egui::Ui) { if let Some(payload) = egui::DragAndDrop::payload::(ui.ctx()) { if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() { + let icon = match payload.as_ref() { + DragAndDropPayload::Contents { .. } => &re_ui::icons::DND_MOVE, + DragAndDropPayload::Invalid { .. } => &re_ui::icons::DND_CANNOT_USE, + }; + let layer_id = egui::LayerId::new( egui::Order::Tooltip, egui::Id::new("drag_and_drop_payload_layer"), ); let response = ui .scope_builder(egui::UiBuilder::new().layer_id(layer_id), |ui| { - //TODO: adjust look based on design - egui::Frame { - rounding: egui::Rounding::same(100.0), // max the rounding for a pill look - fill: ui.visuals().widgets.active.text_color(), - inner_margin: egui::Margin { - left: 8.0, - right: 8.0, - top: 4.0, - bottom: 3.0, - }, - ..Default::default() - } + drag_pill_frame(matches!( + payload.as_ref(), + &DragAndDropPayload::Invalid { .. } + )) .show(ui, |ui| { - ui.label( - egui::RichText::new(payload.to_string()).color(ui.visuals().panel_fill), - ) + let text_color = ui.visuals().widgets.inactive.text_color(); + + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 2.0; + + ui.small_icon(icon, Some(text_color)); + ui.label(egui::RichText::new(payload.to_string()).color(text_color)); + }); }) - .inner + .response }) .response; - //TODO: adjust geometry let delta = pointer_pos - response.rect.right_bottom(); ui.ctx() .transform_layer_shapes(layer_id, emath::TSTransform::from_translation(delta)); @@ -102,4 +94,82 @@ pub fn drag_and_drop_payload_cursor_ui(ui: &mut egui::Ui) { } } -//TODO: capture escape key to cancel drag +fn drag_pill_frame(error_state: bool) -> egui::Frame { + let hue = if error_state { Hue::Red } else { Hue::Blue }; + + egui::Frame { + fill: re_ui::design_tokens().color(ColorToken::new(hue, S325)), + stroke: egui::Stroke::new( + 1.0, + re_ui::design_tokens().color(ColorToken::new(hue, S375)), + ), + rounding: (2.0).into(), + inner_margin: egui::Margin { + left: 6.0, + right: 9.0, + top: 5.0, + bottom: 4.0, + }, + ..Default::default() + } +} + +fn items_to_string<'a>(items: impl Iterator) -> String { + let mut container_cnt = 0u32; + let mut view_cnt = 0u32; + let mut app_cnt = 0u32; + let mut data_source_cnt = 0u32; + let mut store_cnt = 0u32; + let mut instance_cnt = 0u32; + let mut component_cnt = 0u32; + + for item in items { + match item { + Item::Container(_) => container_cnt += 1, + Item::SpaceView(_) => view_cnt += 1, + Item::AppId(_) => app_cnt += 1, + Item::DataSource(_) => data_source_cnt += 1, + Item::StoreId(_) => store_cnt += 1, + Item::InstancePath(_) => instance_cnt += 1, + Item::ComponentPath(_) => component_cnt += 1, + Item::DataResult(_, _) => instance_cnt += 1, + } + } + + let counts = [ + &container_cnt, + &view_cnt, + &app_cnt, + &data_source_cnt, + &store_cnt, + &instance_cnt, + &component_cnt, + ]; + + let names = [ + "container", + "view", + "app", + "data source", + "store", + "instance", + "component", + ]; + + counts + .iter() + .zip(names.iter()) + .filter_map(|(&&cnt, &name)| { + if cnt > 0 { + Some(format!( + "{} {}{}", + cnt, + name, + if cnt > 1 { "s" } else { "" } + )) + } else { + None + } + }) + .join(", ") +} From 0f9b14e2d94834988856d9ddf2d61b324cc924dc Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Fri, 6 Dec 2024 10:33:23 +0100 Subject: [PATCH 04/14] Renamed `select_hovered_on_click` to `handle_select_hover_drag_interactions` --- crates/viewer/re_blueprint_tree/src/blueprint_tree.rs | 8 ++++---- crates/viewer/re_data_ui/src/instance_path.rs | 2 +- crates/viewer/re_data_ui/src/item_ui.rs | 2 +- .../viewer/re_selection_panel/src/selection_panel.rs | 4 ++-- .../re_space_view_bar_chart/src/space_view_class.rs | 2 +- .../re_space_view_dataframe/src/dataframe_ui.rs | 5 +++-- crates/viewer/re_space_view_graph/src/ui/draw.rs | 4 ++-- crates/viewer/re_space_view_map/src/map_space_view.rs | 2 +- crates/viewer/re_space_view_spatial/src/picking_ui.rs | 2 +- .../re_space_view_time_series/src/space_view_class.rs | 2 +- crates/viewer/re_time_panel/src/lib.rs | 4 ++-- crates/viewer/re_viewer/src/ui/recordings_panel.rs | 2 +- crates/viewer/re_viewer_context/src/viewer_context.rs | 11 ++++++----- crates/viewer/re_viewport/src/viewport_ui.rs | 3 ++- .../src/color_coordinates_space_view.rs | 2 +- 15 files changed, 29 insertions(+), 26 deletions(-) diff --git a/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs b/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs index 62efc89ede41..8554f490dc4b 100644 --- a/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs +++ b/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs @@ -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, true); + ctx.handle_select_hover_drag_interactions(&item_response, item, true); self.handle_root_container_drag_and_drop_interaction( viewport, @@ -270,7 +270,7 @@ impl BlueprintTree { SelectionUpdateBehavior::UseSelection, ); self.scroll_to_me_if_needed(ui, &item, &response); - ctx.select_hovered_on_click(&response, item, true); + ctx.handle_select_hover_drag_interactions(&response, item, true); viewport.set_content_visibility(ctx, &content, visible); @@ -409,7 +409,7 @@ impl BlueprintTree { SelectionUpdateBehavior::UseSelection, ); self.scroll_to_me_if_needed(ui, &item, &response); - ctx.select_hovered_on_click(&response, item, true); + ctx.handle_select_hover_drag_interactions(&response, item, true); let content = Contents::SpaceView(*space_view_id); @@ -602,7 +602,7 @@ impl BlueprintTree { SelectionUpdateBehavior::UseSelection, ); self.scroll_to_me_if_needed(ui, &item, &response); - ctx.select_hovered_on_click(&response, item, true); + ctx.handle_select_hover_drag_interactions(&response, item, true); } /// Add a button to trigger the addition of a new space view or container. diff --git a/crates/viewer/re_data_ui/src/instance_path.rs b/crates/viewer/re_data_ui/src/instance_path.rs index 887b02a4df5f..2b294084b2f9 100644 --- a/crates/viewer/re_data_ui/src/instance_path.rs +++ b/crates/viewer/re_data_ui/src/instance_path.rs @@ -243,7 +243,7 @@ fn component_list_ui( }); if interactive { - ctx.select_hovered_on_click(&response, item, false); + ctx.handle_select_hover_drag_interactions(&response, item, false); } } }); diff --git a/crates/viewer/re_data_ui/src/item_ui.rs b/crates/viewer/re_data_ui/src/item_ui.rs index 12625645bb3c..b68e2753f3d3 100644 --- a/crates/viewer/re_data_ui/src/item_ui.rs +++ b/crates/viewer/re_data_ui/src/item_ui.rs @@ -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, false); + ctx.handle_select_hover_drag_interactions(&response, item, false); // TODO(andreas): How to deal with shift click for selecting ranges? if is_item_hovered { diff --git a/crates/viewer/re_selection_panel/src/selection_panel.rs b/crates/viewer/re_selection_panel/src/selection_panel.rs index aede775a7118..0729cc5e103c 100644 --- a/crates/viewer/re_selection_panel/src/selection_panel.rs +++ b/crates/viewer/re_selection_panel/src/selection_panel.rs @@ -656,7 +656,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, false); + ctx.handle_select_hover_drag_interactions(&response, item, false); } } } @@ -907,7 +907,7 @@ fn show_list_item_for_container_child( &response, SelectionUpdateBehavior::Ignore, ); - ctx.select_hovered_on_click(&response, item, false); + ctx.handle_select_hover_drag_interactions(&response, item, false); if remove_contents { viewport.mark_user_interaction(ctx); diff --git a/crates/viewer/re_space_view_bar_chart/src/space_view_class.rs b/crates/viewer/re_space_view_bar_chart/src/space_view_class.rs index 729d23ac4e4f..139953eddc32 100644 --- a/crates/viewer/re_space_view_bar_chart/src/space_view_class.rs +++ b/crates/viewer/re_space_view_bar_chart/src/space_view_class.rs @@ -242,7 +242,7 @@ Display a 1D tensor as a bar chart. if let Some(entity_path) = hovered_plot_item .and_then(|hovered_plot_item| plot_item_id_to_entity_path.get(&hovered_plot_item)) { - ctx.select_hovered_on_click( + ctx.handle_select_hover_drag_interactions( &response, re_viewer_context::Item::DataResult( query.space_view_id, diff --git a/crates/viewer/re_space_view_dataframe/src/dataframe_ui.rs b/crates/viewer/re_space_view_dataframe/src/dataframe_ui.rs index 88f1f4d849a0..a25f1d77ed11 100644 --- a/crates/viewer/re_space_view_dataframe/src/dataframe_ui.rs +++ b/crates/viewer/re_space_view_dataframe/src/dataframe_ui.rs @@ -261,7 +261,8 @@ impl egui_table::TableDelegate for DataframeTableDelegate<'_> { egui::Rect::from_min_size(pos, size), egui::SelectableLabel::new(is_selected, galley), ); - self.ctx.select_hovered_on_click(&response, item, false); + self.ctx + .handle_select_hover_drag_interactions(&response, item, false); // TODO(emilk): expand column(s) to make sure the text fits (requires egui_table fix). } @@ -319,7 +320,7 @@ impl egui_table::TableDelegate for DataframeTableDelegate<'_> { } } ColumnDescriptor::Component(component_column_descriptor) => { - self.ctx.select_hovered_on_click( + self.ctx.handle_select_hover_drag_interactions( &response, re_viewer_context::Item::ComponentPath( component_column_descriptor.component_path(), diff --git a/crates/viewer/re_space_view_graph/src/ui/draw.rs b/crates/viewer/re_space_view_graph/src/ui/draw.rs index e0f132cf6e4f..8cf368411e33 100644 --- a/crates/viewer/re_space_view_graph/src/ui/draw.rs +++ b/crates/viewer/re_space_view_graph/src/ui/draw.rs @@ -287,7 +287,7 @@ pub fn draw_graph( let instance_path = InstancePath::instance(entity_path.clone(), instance.instance_index); - ctx.select_hovered_on_click( + ctx.handle_select_hover_drag_interactions( &response, Item::DataResult(query.space_view_id, instance_path.clone()), false, @@ -335,7 +335,7 @@ pub fn draw_graph( let resp = draw_entity_rect(ui, *rect, entity_path, &query.highlights); current_rect = current_rect.union(resp.rect); let instance_path = InstancePath::entity_all(entity_path.clone()); - ctx.select_hovered_on_click( + ctx.handle_select_hover_drag_interactions( &resp, Item::DataResult(query.space_view_id, instance_path), false, diff --git a/crates/viewer/re_space_view_map/src/map_space_view.rs b/crates/viewer/re_space_view_map/src/map_space_view.rs index 481b55748324..e84ddf2a5712 100644 --- a/crates/viewer/re_space_view_map/src/map_space_view.rs +++ b/crates/viewer/re_space_view_map/src/map_space_view.rs @@ -481,7 +481,7 @@ fn handle_ui_interactions( }); }); - ctx.select_hovered_on_click( + ctx.handle_select_hover_drag_interactions( &map_response, Item::DataResult(query.space_view_id, instance_path.clone()), false, diff --git a/crates/viewer/re_space_view_spatial/src/picking_ui.rs b/crates/viewer/re_space_view_spatial/src/picking_ui.rs index e9962df58a26..62a168f6f0aa 100644 --- a/crates/viewer/re_space_view_spatial/src/picking_ui.rs +++ b/crates/viewer/re_space_view_spatial/src/picking_ui.rs @@ -209,7 +209,7 @@ pub fn picking( }); }; - ctx.select_hovered_on_click(&response, hovered_items.into_iter(), false); + ctx.handle_select_hover_drag_interactions(&response, hovered_items.into_iter(), false); Ok(response) } diff --git a/crates/viewer/re_space_view_time_series/src/space_view_class.rs b/crates/viewer/re_space_view_time_series/src/space_view_class.rs index c01ab1ece767..40f6747bcc28 100644 --- a/crates/viewer/re_space_view_time_series/src/space_view_class.rs +++ b/crates/viewer/re_space_view_time_series/src/space_view_class.rs @@ -536,7 +536,7 @@ Display time series data in a plot. } }) { - ctx.select_hovered_on_click(&response, hovered, false); + ctx.handle_select_hover_drag_interactions(&response, hovered, false); } } diff --git a/crates/viewer/re_time_panel/src/lib.rs b/crates/viewer/re_time_panel/src/lib.rs index 39dd59764cdb..ec4c9bf5b170 100644 --- a/crates/viewer/re_time_panel/src/lib.rs +++ b/crates/viewer/re_time_panel/src/lib.rs @@ -726,7 +726,7 @@ impl TimePanel { &response, SelectionUpdateBehavior::UseSelection, ); - ctx.select_hovered_on_click(&response, item.to_item(), false); + ctx.handle_select_hover_drag_interactions(&response, item.to_item(), false); let is_closed = body_response.is_none(); let response_rect = response.rect; @@ -854,7 +854,7 @@ impl TimePanel { &response, SelectionUpdateBehavior::UseSelection, ); - ctx.select_hovered_on_click(&response, item.to_item(), false); + ctx.handle_select_hover_drag_interactions(&response, item.to_item(), false); let response_rect = response.rect; diff --git a/crates/viewer/re_viewer/src/ui/recordings_panel.rs b/crates/viewer/re_viewer/src/ui/recordings_panel.rs index 1180a2a3bf99..378d0c34f9fa 100644 --- a/crates/viewer/re_viewer/src/ui/recordings_panel.rs +++ b/crates/viewer/re_viewer/src/ui/recordings_panel.rs @@ -222,7 +222,7 @@ fn app_and_its_recordings_ui( app_id.data_ui_recording(ctx, ui, UiLayout::Tooltip); }); - ctx.select_hovered_on_click(&item_response, app_item, false); + ctx.handle_select_hover_drag_interactions(&item_response, app_item, false); if item_response.clicked() { // Switch to this application: diff --git a/crates/viewer/re_viewer_context/src/viewer_context.rs b/crates/viewer/re_viewer_context/src/viewer_context.rs index e40716c1a020..72748e05fb6c 100644 --- a/crates/viewer/re_viewer_context/src/viewer_context.rs +++ b/crates/viewer/re_viewer_context/src/viewer_context.rs @@ -135,13 +135,14 @@ impl ViewerContext<'_> { self.rec_cfg.time_ctrl.read().current_query() } - //TODO: rename this method - /// Set hover/select/focus for a given selection based on an egui response. - pub fn select_hovered_on_click( + /// Consistently handle the selection, hover, drag start interactions for a given set of items. + /// + /// The drag start interaction is optional and controlled by the `draggable` parameter. + pub fn handle_select_hover_drag_interactions( &self, response: &egui::Response, selection: impl Into, - can_be_dragged: bool, + draggable: bool, ) { re_tracing::profile_function!(); @@ -152,7 +153,7 @@ impl ViewerContext<'_> { selection_state.set_hovered(selection.clone()); } - if can_be_dragged && response.drag_started() { + if draggable && response.drag_started() { let mut selected_items = selection_state.selected_items().clone(); let is_already_selected = selection .iter() diff --git a/crates/viewer/re_viewport/src/viewport_ui.rs b/crates/viewer/re_viewport/src/viewport_ui.rs index 37104f81f024..d4253995ff2d 100644 --- a/crates/viewer/re_viewport/src/viewport_ui.rs +++ b/crates/viewer/re_viewport/src/viewport_ui.rs @@ -597,7 +597,8 @@ impl<'a> egui_tiles::Behavior for TilesDelegate<'a, '_> { &response, SelectionUpdateBehavior::OverrideSelection, ); - self.ctx.select_hovered_on_click(&response, item, false); + self.ctx + .handle_select_hover_drag_interactions(&response, item, false); } response diff --git a/examples/rust/custom_space_view/src/color_coordinates_space_view.rs b/examples/rust/custom_space_view/src/color_coordinates_space_view.rs index 7b78fbc17b55..11641c56c7cf 100644 --- a/examples/rust/custom_space_view/src/color_coordinates_space_view.rs +++ b/examples/rust/custom_space_view/src/color_coordinates_space_view.rs @@ -279,7 +279,7 @@ fn color_space_ui( ctx.recording(), ); }); - ctx.select_hovered_on_click( + ctx.handle_select_hover_drag_interactions( &interact, Item::DataResult(query.space_view_id, instance), false, From 7b15fed3d6b47d6ac5606ddd95eb29af8739ef0b Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Fri, 6 Dec 2024 15:22:34 +0100 Subject: [PATCH 05/14] 70% opacity for the bluepill --- crates/viewer/re_viewer_context/src/drag_and_drop.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/viewer/re_viewer_context/src/drag_and_drop.rs b/crates/viewer/re_viewer_context/src/drag_and_drop.rs index 1feb9c73f617..5ec33e27f1f6 100644 --- a/crates/viewer/re_viewer_context/src/drag_and_drop.rs +++ b/crates/viewer/re_viewer_context/src/drag_and_drop.rs @@ -69,6 +69,8 @@ pub fn drag_and_drop_payload_cursor_ui(ui: &mut egui::Ui) { ); let response = ui .scope_builder(egui::UiBuilder::new().layer_id(layer_id), |ui| { + ui.set_opacity(0.7); + drag_pill_frame(matches!( payload.as_ref(), &DragAndDropPayload::Invalid { .. } From 1c09ca9960da79727c529d4d9b8f569650038d41 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Fri, 6 Dec 2024 16:53:37 +0100 Subject: [PATCH 06/14] post rebase --- crates/viewer/re_viewer_context/src/drag_and_drop.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/viewer/re_viewer_context/src/drag_and_drop.rs b/crates/viewer/re_viewer_context/src/drag_and_drop.rs index 5ec33e27f1f6..3aa2546bdcbd 100644 --- a/crates/viewer/re_viewer_context/src/drag_and_drop.rs +++ b/crates/viewer/re_viewer_context/src/drag_and_drop.rs @@ -7,7 +7,7 @@ use itertools::Itertools; use re_ui::{ ColorToken, Hue, - Shade::{S325, S375}, + Scale::{S325, S375}, UiExt, }; From 306ff860aa3492d3719fbfdb38ee157e716ea29a Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Mon, 9 Dec 2024 11:02:50 +0100 Subject: [PATCH 07/14] Properly deal with entity vs. instance --- .../re_viewer_context/src/drag_and_drop.rs | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/crates/viewer/re_viewer_context/src/drag_and_drop.rs b/crates/viewer/re_viewer_context/src/drag_and_drop.rs index 3aa2546bdcbd..41ce7a00a838 100644 --- a/crates/viewer/re_viewer_context/src/drag_and_drop.rs +++ b/crates/viewer/re_viewer_context/src/drag_and_drop.rs @@ -122,6 +122,7 @@ fn items_to_string<'a>(items: impl Iterator) -> String { let mut app_cnt = 0u32; let mut data_source_cnt = 0u32; let mut store_cnt = 0u32; + let mut entity_cnt = 0u32; let mut instance_cnt = 0u32; let mut component_cnt = 0u32; @@ -132,9 +133,14 @@ fn items_to_string<'a>(items: impl Iterator) -> String { Item::AppId(_) => app_cnt += 1, Item::DataSource(_) => data_source_cnt += 1, Item::StoreId(_) => store_cnt += 1, - Item::InstancePath(_) => instance_cnt += 1, + Item::InstancePath(instance_path) | Item::DataResult(_, instance_path) => { + if instance_path.is_all() { + entity_cnt += 1; + } else { + instance_cnt += 1; + } + } Item::ComponentPath(_) => component_cnt += 1, - Item::DataResult(_, _) => instance_cnt += 1, } } @@ -144,18 +150,20 @@ fn items_to_string<'a>(items: impl Iterator) -> String { &app_cnt, &data_source_cnt, &store_cnt, + &entity_cnt, &instance_cnt, &component_cnt, ]; let names = [ - "container", - "view", - "app", - "data source", - "store", - "instance", - "component", + ("container", "containers"), + ("view", "views"), + ("app", "apps"), + ("data source", "data sources"), + ("store", "stores"), + ("entity", "entities"), + ("instance", "instances"), + ("component", "components"), ]; counts @@ -164,10 +172,9 @@ fn items_to_string<'a>(items: impl Iterator) -> String { .filter_map(|(&&cnt, &name)| { if cnt > 0 { Some(format!( - "{} {}{}", + "{} {}", cnt, - name, - if cnt > 1 { "s" } else { "" } + if cnt == 1 { name.0 } else { name.1 }, )) } else { None From d97bdaef5d84fccd0a7fba382cd74d335e90284e Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Mon, 9 Dec 2024 11:10:57 +0100 Subject: [PATCH 08/14] No longer draw the pill with invalid selection --- crates/viewer/re_viewer_context/src/drag_and_drop.rs | 12 ++++++------ .../viewer/re_viewer_context/src/viewer_context.rs | 8 +++----- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/crates/viewer/re_viewer_context/src/drag_and_drop.rs b/crates/viewer/re_viewer_context/src/drag_and_drop.rs index 41ce7a00a838..adc34b13917a 100644 --- a/crates/viewer/re_viewer_context/src/drag_and_drop.rs +++ b/crates/viewer/re_viewer_context/src/drag_and_drop.rs @@ -20,7 +20,7 @@ pub enum DragAndDropPayload { Contents { contents: Vec }, /// The dragged content is made of a collection of [`Item`]s we do know how to handle. - Invalid { items: crate::ItemCollection }, + Invalid, } impl DragAndDropPayload { @@ -28,9 +28,7 @@ impl DragAndDropPayload { if let Ok(contents) = (&selected_items).try_into() { Self::Contents { contents } } else { - Self::Invalid { - items: selected_items, - } + Self::Invalid } } } @@ -47,7 +45,8 @@ impl std::fmt::Display for DragAndDropPayload { ) .fmt(f), - Self::Invalid { items } => items_to_string(items.iter_items()).fmt(f), + // this is not used in the UI + Self::Invalid => "invalid selection".fmt(f), } } } @@ -60,7 +59,8 @@ pub fn drag_and_drop_payload_cursor_ui(ui: &mut egui::Ui) { if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() { let icon = match payload.as_ref() { DragAndDropPayload::Contents { .. } => &re_ui::icons::DND_MOVE, - DragAndDropPayload::Invalid { .. } => &re_ui::icons::DND_CANNOT_USE, + // don't draw anything for invalid selection + DragAndDropPayload::Invalid => return, }; let layer_id = egui::LayerId::new( diff --git a/crates/viewer/re_viewer_context/src/viewer_context.rs b/crates/viewer/re_viewer_context/src/viewer_context.rs index 72748e05fb6c..3fe195d45411 100644 --- a/crates/viewer/re_viewer_context/src/viewer_context.rs +++ b/crates/viewer/re_viewer_context/src/viewer_context.rs @@ -167,17 +167,15 @@ impl ViewerContext<'_> { selection_state.set_selection(selected_items.clone()); } - let is_selection_draggable = self + let selection_contains_undraggable_items = self .undraggable_items .iter_items() .all(|item| !selected_items.contains_item(item)); - let payload = if is_selection_draggable { + let payload = if selection_contains_undraggable_items { DragAndDropPayload::from_items(selected_items) } else { - DragAndDropPayload::Invalid { - items: selected_items, - } + DragAndDropPayload::Invalid }; egui::DragAndDrop::set_payload(&response.ctx, payload); From 4cede3e7ff2bd205ca249cb1028a02a0d7f3e384 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Mon, 9 Dec 2024 16:06:54 +0100 Subject: [PATCH 09/14] Cleanup moving code --- Cargo.lock | 22 +++++++++---------- Cargo.toml | 14 ++++++------ .../re_blueprint_tree/src/blueprint_tree.rs | 16 +++++--------- .../re_viewer_context/src/drag_and_drop.rs | 4 ++-- .../re_viewer_context/src/viewer_context.rs | 2 +- crates/viewer/re_viewport/src/viewport_ui.rs | 22 ++++++++++++------- .../src/viewport_blueprint.rs | 4 ++-- .../src/viewport_command.rs | 2 +- 8 files changed, 43 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 188ca2f77f5f..f9f9b46952e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1923,7 +1923,7 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "ecolor" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=f687b27efc590b9584bfa6dd0c2f22edaf8c8cef#f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" +source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040" dependencies = [ "bytemuck", "color-hex", @@ -1940,7 +1940,7 @@ checksum = "18aade80d5e09429040243ce1143ddc08a92d7a22820ac512610410a4dd5214f" [[package]] name = "eframe" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=f687b27efc590b9584bfa6dd0c2f22edaf8c8cef#f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" +source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040" dependencies = [ "ahash", "bytemuck", @@ -1979,7 +1979,7 @@ dependencies = [ [[package]] name = "egui" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=f687b27efc590b9584bfa6dd0c2f22edaf8c8cef#f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" +source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040" dependencies = [ "accesskit", "ahash", @@ -1996,7 +1996,7 @@ dependencies = [ [[package]] name = "egui-wgpu" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=f687b27efc590b9584bfa6dd0c2f22edaf8c8cef#f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" +source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040" dependencies = [ "ahash", "bytemuck", @@ -2015,7 +2015,7 @@ dependencies = [ [[package]] name = "egui-winit" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=f687b27efc590b9584bfa6dd0c2f22edaf8c8cef#f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" +source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040" dependencies = [ "accesskit_winit", "ahash", @@ -2057,7 +2057,7 @@ dependencies = [ [[package]] name = "egui_extras" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=f687b27efc590b9584bfa6dd0c2f22edaf8c8cef#f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" +source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040" dependencies = [ "ahash", "egui", @@ -2074,7 +2074,7 @@ dependencies = [ [[package]] name = "egui_glow" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=f687b27efc590b9584bfa6dd0c2f22edaf8c8cef#f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" +source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040" dependencies = [ "ahash", "bytemuck", @@ -2092,7 +2092,7 @@ dependencies = [ [[package]] name = "egui_kittest" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=f687b27efc590b9584bfa6dd0c2f22edaf8c8cef#f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" +source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040" dependencies = [ "dify", "egui", @@ -2161,7 +2161,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "emath" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=f687b27efc590b9584bfa6dd0c2f22edaf8c8cef#f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" +source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040" dependencies = [ "bytemuck", "serde", @@ -2277,7 +2277,7 @@ dependencies = [ [[package]] name = "epaint" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=f687b27efc590b9584bfa6dd0c2f22edaf8c8cef#f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" +source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040" dependencies = [ "ab_glyph", "ahash", @@ -2296,7 +2296,7 @@ dependencies = [ [[package]] name = "epaint_default_fonts" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=f687b27efc590b9584bfa6dd0c2f22edaf8c8cef#f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" +source = "git+https://github.com/emilk/egui.git?rev=13352d606496d7b1c5fd6fcfbe3c85baae39c040#13352d606496d7b1c5fd6fcfbe3c85baae39c040" [[package]] name = "equivalent" diff --git a/Cargo.toml b/Cargo.toml index 05ddd0ae4b8d..17e3ebe2c33b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -559,13 +559,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 = "f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" } # egui master 2024-12-04 -eframe = { git = "https://github.com/emilk/egui.git", rev = "f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" } # egui master 2024-12-04 -egui = { git = "https://github.com/emilk/egui.git", rev = "f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" } # egui master 2024-12-04 -egui_extras = { git = "https://github.com/emilk/egui.git", rev = "f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" } # egui master 2024-12-04 -egui_kittest = { git = "https://github.com/emilk/egui.git", rev = "f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" } # egui master 2024-12-04 -egui-wgpu = { git = "https://github.com/emilk/egui.git", rev = "f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" } # egui master 2024-12-04 -emath = { git = "https://github.com/emilk/egui.git", rev = "f687b27efc590b9584bfa6dd0c2f22edaf8c8cef" } # 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" } diff --git a/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs b/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs index 8554f490dc4b..11ca450124da 100644 --- a/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs +++ b/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs @@ -837,17 +837,11 @@ impl BlueprintTree { }; if ui.input(|i| i.pointer.any_released()) { - for (idx, content) in dragged_contents.iter().enumerate() { - // TODO: this is not strictly correct when dragging multiple items inside the same - // container, as the position are all over the place when moving items incrementally. - // We probably need a `viewport.move_multiple_contents()` method which handles this - // correctly. - viewport.move_contents( - *content, - target_container_id, - drop_target.target_position_index + idx, - ); - } + viewport.move_contents( + dragged_contents, + target_container_id, + drop_target.target_position_index, + ); egui::DragAndDrop::clear_payload(ui.ctx()); } else { diff --git a/crates/viewer/re_viewer_context/src/drag_and_drop.rs b/crates/viewer/re_viewer_context/src/drag_and_drop.rs index adc34b13917a..1849acef1d12 100644 --- a/crates/viewer/re_viewer_context/src/drag_and_drop.rs +++ b/crates/viewer/re_viewer_context/src/drag_and_drop.rs @@ -24,8 +24,8 @@ pub enum DragAndDropPayload { } impl DragAndDropPayload { - pub fn from_items(selected_items: ItemCollection) -> Self { - if let Ok(contents) = (&selected_items).try_into() { + pub fn from_items(selected_items: &ItemCollection) -> Self { + if let Ok(contents) = selected_items.try_into() { Self::Contents { contents } } else { Self::Invalid diff --git a/crates/viewer/re_viewer_context/src/viewer_context.rs b/crates/viewer/re_viewer_context/src/viewer_context.rs index 3fe195d45411..81c93f00d118 100644 --- a/crates/viewer/re_viewer_context/src/viewer_context.rs +++ b/crates/viewer/re_viewer_context/src/viewer_context.rs @@ -173,7 +173,7 @@ impl ViewerContext<'_> { .all(|item| !selected_items.contains_item(item)); let payload = if selection_contains_undraggable_items { - DragAndDropPayload::from_items(selected_items) + DragAndDropPayload::from_items(&selected_items) } else { DragAndDropPayload::Invalid }; diff --git a/crates/viewer/re_viewport/src/viewport_ui.rs b/crates/viewer/re_viewport/src/viewport_ui.rs index d4253995ff2d..558fe202d996 100644 --- a/crates/viewer/re_viewport/src/viewport_ui.rs +++ b/crates/viewer/re_viewport/src/viewport_ui.rs @@ -379,15 +379,21 @@ fn apply_viewport_command( {target_position_in_container}" ); - let contents_tile_id = contents_to_move.as_tile_id(); - let target_container_tile_id = blueprint_id_to_tile_id(&target_container); + // TODO(ab): the `rev()` is better preserve ordering when moving a group of items. There + // remains some ordering (and possibly insertion point error) edge cases when dragging + // multiple item within the same container. This should be addressed by egui_tiles: + // https://github.com/rerun-io/egui_tiles/issues/90 + for contents in contents_to_move.iter().rev() { + let contents_tile_id = contents.as_tile_id(); + let target_container_tile_id = blueprint_id_to_tile_id(&target_container); - bp.tree.move_tile_to_container( - contents_tile_id, - target_container_tile_id, - target_position_in_container, - true, - ); + bp.tree.move_tile_to_container( + contents_tile_id, + target_container_tile_id, + target_position_in_container, + true, + ); + } } ViewportCommand::MoveContentsToNewContainer { diff --git a/crates/viewer/re_viewport_blueprint/src/viewport_blueprint.rs b/crates/viewer/re_viewport_blueprint/src/viewport_blueprint.rs index c8453466ef1f..1416c9eefa8d 100644 --- a/crates/viewer/re_viewport_blueprint/src/viewport_blueprint.rs +++ b/crates/viewer/re_viewport_blueprint/src/viewport_blueprint.rs @@ -583,12 +583,12 @@ impl ViewportBlueprint { /// Move the `contents` container or space view to the specified target container and position. pub fn move_contents( &self, - contents: Contents, + contents: &[Contents], target_container: ContainerId, target_position_in_container: usize, ) { self.enqueue_command(ViewportCommand::MoveContents { - contents_to_move: contents, + contents_to_move: contents.to_vec(), target_container, target_position_in_container, }); diff --git a/crates/viewer/re_viewport_blueprint/src/viewport_command.rs b/crates/viewer/re_viewport_blueprint/src/viewport_command.rs index e4056b51f8a2..0d74b160dd90 100644 --- a/crates/viewer/re_viewport_blueprint/src/viewport_command.rs +++ b/crates/viewer/re_viewport_blueprint/src/viewport_command.rs @@ -40,7 +40,7 @@ pub enum ViewportCommand { /// Move some contents to a different container MoveContents { - contents_to_move: Contents, + contents_to_move: Vec, target_container: ContainerId, target_position_in_container: usize, }, From 0ceb78658f032d47256c61ed81898b44a593d29b Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Mon, 9 Dec 2024 16:19:02 +0100 Subject: [PATCH 10/14] remove unnecessary icon --- crates/viewer/re_ui/data/icons/dnd_cannot_use.png | Bin 685 -> 0 bytes crates/viewer/re_ui/src/icons.rs | 1 - 2 files changed, 1 deletion(-) delete mode 100644 crates/viewer/re_ui/data/icons/dnd_cannot_use.png diff --git a/crates/viewer/re_ui/data/icons/dnd_cannot_use.png b/crates/viewer/re_ui/data/icons/dnd_cannot_use.png deleted file mode 100644 index db1ba198f7441a6b9517e5e662285243f219aefb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 685 zcmV;e0#f~nP)EM%d+MMaD(2U;RK0Kka7YhH{b>_D+XuLvPznnAajGTV_MMT zeg2uY#)_5D?)uF*QZ#z{q)+chM31;lgI<{V ztkX(3otcOg52PEFLz$RmMVGtsvtN!Uq}BVQ=ivoyOE_Ja5nJwqgqg=rp6RoLFkIc9 z^JQj&4mMM4Do{!d4_|~aZHn}A`7xb;=XAb2J2$55WAk;k9OgD+>zbpR2B(*1K&(#W zRPoWvy?*^v&c(Fmyd$>hVmWG>^N*x(4zhd>ve&)6A$bCu6q_OJlp;|?ohnvu4trs; z)-au$iO6DT1Aq)tLQ?$46<`hdCiS^@B!vTr9+61med)JPRw;rjfRjl#$`x}FiWG0r z?!v{lB&z_AMvK^W5_ZTQOLO59#Oug_($TUVBnlTHlZN}ZdSn~f4OC{!tkg*81Anq+ z+&v-l4`~9rCo(0e&szHFCR7S}Flfpwo07rf8F^N7;w03%@Xo7Ww*Nn5K!^TBXi-Vl zPOJjII%Pemec=lHo%(=cidoU*`q~bJY_*Mpl!^pWuHJbWph6UtV)K)=Dbg4gs1VF{ zoKL7KKnrjMdHu{4v16AaYYQ-TDFqdUH6yn1E_IT676Q@lD>@aT6Usx1KVZYIHCvw; zMY*C;p41B` From ba14e6a29609677c5b3b459b4061760bc66fa042 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Wed, 11 Dec 2024 15:56:42 +0100 Subject: [PATCH 11/14] Addressed some review comments --- .../re_blueprint_tree/src/blueprint_tree.rs | 9 ++-- crates/viewer/re_viewer/src/app_state.rs | 2 +- .../re_viewer_context/src/drag_and_drop.rs | 54 ++++++++++--------- .../re_viewer_context/src/viewer_context.rs | 10 +++- .../src/viewport_blueprint.rs | 4 +- 5 files changed, 45 insertions(+), 34 deletions(-) diff --git a/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs b/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs index 86d432262df1..d22b70d8c987 100644 --- a/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs +++ b/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs @@ -805,8 +805,8 @@ impl BlueprintTree { ) { // We cannot allow the target location to be "inside" any of the dragged items, because that // would amount to moving myself inside of me. - if dragged_contents.iter().any(|dragged_contents| { - if let Contents::Container(dragged_container_id) = dragged_contents { + 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) { @@ -814,7 +814,8 @@ impl BlueprintTree { } } false - }) { + }; + if dragged_contents.iter().any(parent_contains_dragged_content) { return; } @@ -831,7 +832,7 @@ impl BlueprintTree { if ui.input(|i| i.pointer.any_released()) { viewport.move_contents( - dragged_contents, + dragged_contents.to_vec(), target_container_id, drop_target.target_position_index, ); diff --git a/crates/viewer/re_viewer/src/app_state.rs b/crates/viewer/re_viewer/src/app_state.rs index 2a8317102370..8e07b53d6c7c 100644 --- a/crates/viewer/re_viewer/src/app_state.rs +++ b/crates/viewer/re_viewer/src/app_state.rs @@ -512,7 +512,7 @@ impl AppState { // add_view_or_container_modal_ui(&ctx, &viewport_ui.blueprint, ui); - drag_and_drop_payload_cursor_ui(ui); + drag_and_drop_payload_cursor_ui(ctx.egui_ctx); // Process deferred layout operations and apply updates back to blueprint: viewport_ui.save_to_blueprint_store(&ctx, view_class_registry); diff --git a/crates/viewer/re_viewer_context/src/drag_and_drop.rs b/crates/viewer/re_viewer_context/src/drag_and_drop.rs index 95f50712dc0c..fe8a4638fa65 100644 --- a/crates/viewer/re_viewer_context/src/drag_and_drop.rs +++ b/crates/viewer/re_viewer_context/src/drag_and_drop.rs @@ -54,9 +54,9 @@ impl std::fmt::Display for DragAndDropPayload { /// Display the currently dragged payload as a pill in the UI. /// /// This should be called once per frame. -pub fn drag_and_drop_payload_cursor_ui(ui: &mut egui::Ui) { - if let Some(payload) = egui::DragAndDrop::payload::(ui.ctx()) { - if let Some(pointer_pos) = ui.ctx().pointer_interact_pos() { +pub fn drag_and_drop_payload_cursor_ui(ctx: &egui::Context) { + if let Some(payload) = egui::DragAndDrop::payload::(ctx) { + if let Some(pointer_pos) = ctx.pointer_interact_pos() { let icon = match payload.as_ref() { DragAndDropPayload::Contents { .. } => &re_ui::icons::DND_MOVE, // don't draw anything for invalid selection @@ -67,31 +67,33 @@ pub fn drag_and_drop_payload_cursor_ui(ui: &mut egui::Ui) { egui::Order::Tooltip, egui::Id::new("drag_and_drop_payload_layer"), ); - let response = ui - .scope_builder(egui::UiBuilder::new().layer_id(layer_id), |ui| { - ui.set_opacity(0.7); - - drag_pill_frame(matches!( - payload.as_ref(), - &DragAndDropPayload::Invalid { .. } - )) - .show(ui, |ui| { - let text_color = ui.visuals().widgets.inactive.text_color(); - - ui.horizontal(|ui| { - ui.spacing_mut().item_spacing.x = 2.0; - - ui.small_icon(icon, Some(text_color)); - ui.label(egui::RichText::new(payload.to_string()).color(text_color)); - }); - }) - .response - }) - .response; + + let mut ui = egui::Ui::new( + ctx.clone(), + egui::Id::new("rerun_drag_and_drop_payload_ui"), + egui::UiBuilder::new().layer_id(layer_id), + ); + + ui.set_opacity(0.7); + + let response = drag_pill_frame(matches!( + payload.as_ref(), + &DragAndDropPayload::Invalid { .. } + )) + .show(&mut ui, |ui| { + let text_color = ui.visuals().widgets.inactive.text_color(); + + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 2.0; + + ui.small_icon(icon, Some(text_color)); + ui.label(egui::RichText::new(payload.to_string()).color(text_color)); + }); + }) + .response; let delta = pointer_pos - response.rect.right_bottom(); - ui.ctx() - .transform_layer_shapes(layer_id, emath::TSTransform::from_translation(delta)); + ctx.transform_layer_shapes(layer_id, emath::TSTransform::from_translation(delta)); } } } diff --git a/crates/viewer/re_viewer_context/src/viewer_context.rs b/crates/viewer/re_viewer_context/src/viewer_context.rs index 48e97a5f6168..f2ba3af23e5a 100644 --- a/crates/viewer/re_viewer_context/src/viewer_context.rs +++ b/crates/viewer/re_viewer_context/src/viewer_context.rs @@ -137,7 +137,15 @@ impl ViewerContext<'_> { /// Consistently handle the selection, hover, drag start interactions for a given set of items. /// - /// The drag start interaction is optional and controlled by the `draggable` parameter. + /// The `draggable` parameter controls whether a drag can be initiated from this item. When a UI + /// element represents an [`Item`], one must make the call whether this element should be + /// meaningfully draggable by the users. This is ultimately a subjective decision, but some here + /// are some guidelines: + /// - Is there a meaningful destination for the dragged payload? For example, dragging stuff out + /// of a modal dialog is by definition meaningless. + /// - Even if a drag destination exists, would that be obvious for the user? + /// - Is it expected for that kind of UI element to be draggable? For example, buttons aren't + /// typically draggable. pub fn handle_select_hover_drag_interactions( &self, response: &egui::Response, diff --git a/crates/viewer/re_viewport_blueprint/src/viewport_blueprint.rs b/crates/viewer/re_viewport_blueprint/src/viewport_blueprint.rs index 81d09f08d844..911e53bc3956 100644 --- a/crates/viewer/re_viewport_blueprint/src/viewport_blueprint.rs +++ b/crates/viewer/re_viewport_blueprint/src/viewport_blueprint.rs @@ -572,12 +572,12 @@ impl ViewportBlueprint { /// Move the `contents` container or view to the specified target container and position. pub fn move_contents( &self, - contents: &[Contents], + contents_to_move: Vec, target_container: ContainerId, target_position_in_container: usize, ) { self.enqueue_command(ViewportCommand::MoveContents { - contents_to_move: contents.to_vec(), + contents_to_move, target_container, target_position_in_container, }); From 5242225f42b97956a7a1f0740a427694bf6cd907 Mon Sep 17 00:00:00 2001 From: Antoine Beyeler <49431240+abey79@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:00:05 +0100 Subject: [PATCH 12/14] Update crates/viewer/re_viewer_context/src/drag_and_drop.rs Co-authored-by: Emil Ernerfeldt --- .../re_viewer_context/src/drag_and_drop.rs | 36 +++++++------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/crates/viewer/re_viewer_context/src/drag_and_drop.rs b/crates/viewer/re_viewer_context/src/drag_and_drop.rs index fe8a4638fa65..3264554b6c4d 100644 --- a/crates/viewer/re_viewer_context/src/drag_and_drop.rs +++ b/crates/viewer/re_viewer_context/src/drag_and_drop.rs @@ -146,32 +146,20 @@ fn items_to_string<'a>(items: impl Iterator) -> String { } } - let counts = [ - &container_cnt, - &view_cnt, - &app_cnt, - &data_source_cnt, - &store_cnt, - &entity_cnt, - &instance_cnt, - &component_cnt, + let count_and_names = [ + (container_cnt, "container", "containers"), + (view_cnt, "view", "views"), + (app_cnt, "app", "apps"), + (data_source_cnt, "data source", "data sources"), + (store_cnt, "store", "stores"), + (entity_cnt, "entity", "entities"), + (instance_cnt, "instance", "instances"), + (component_cnt, "component", "components"), ]; - let names = [ - ("container", "containers"), - ("view", "views"), - ("app", "apps"), - ("data source", "data sources"), - ("store", "stores"), - ("entity", "entities"), - ("instance", "instances"), - ("component", "components"), - ]; - - counts - .iter() - .zip(names.iter()) - .filter_map(|(&&cnt, &name)| { + count_and_names + .into_iter() + .filter_map(|(count, name_singular, name_plural)| { if cnt > 0 { Some(format!( "{} {}", From b6753cd76bf4fbd4f5d338232918c063011df8ae Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Wed, 11 Dec 2024 16:07:15 +0100 Subject: [PATCH 13/14] More review comments --- crates/viewer/re_viewer_context/src/drag_and_drop.rs | 10 +++++++--- crates/viewer/re_viewer_context/src/viewer_context.rs | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/viewer/re_viewer_context/src/drag_and_drop.rs b/crates/viewer/re_viewer_context/src/drag_and_drop.rs index 3264554b6c4d..bec7e7e43a5f 100644 --- a/crates/viewer/re_viewer_context/src/drag_and_drop.rs +++ b/crates/viewer/re_viewer_context/src/drag_and_drop.rs @@ -160,11 +160,15 @@ fn items_to_string<'a>(items: impl Iterator) -> String { count_and_names .into_iter() .filter_map(|(count, name_singular, name_plural)| { - if cnt > 0 { + if count > 0 { Some(format!( "{} {}", - cnt, - if cnt == 1 { name.0 } else { name.1 }, + re_format::format_uint(count), + if count == 1 { + name_singular + } else { + name_plural + }, )) } else { None diff --git a/crates/viewer/re_viewer_context/src/viewer_context.rs b/crates/viewer/re_viewer_context/src/viewer_context.rs index f2ba3af23e5a..9d0a414cdc37 100644 --- a/crates/viewer/re_viewer_context/src/viewer_context.rs +++ b/crates/viewer/re_viewer_context/src/viewer_context.rs @@ -175,12 +175,12 @@ impl ViewerContext<'_> { selection_state.set_selection(selected_items.clone()); } - let selection_contains_undraggable_items = self + let selection_may_be_dragged = self .undraggable_items .iter_items() .all(|item| !selected_items.contains_item(item)); - let payload = if selection_contains_undraggable_items { + let payload = if selection_may_be_dragged { DragAndDropPayload::from_items(&selected_items) } else { DragAndDropPayload::Invalid From 245210ac3c10dbc73ca43f5e41c02d70c6fd64cd Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Wed, 11 Dec 2024 17:32:19 +0100 Subject: [PATCH 14/14] Add TODO + fix doclink --- crates/viewer/re_viewer_context/src/viewer_context.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/viewer/re_viewer_context/src/viewer_context.rs b/crates/viewer/re_viewer_context/src/viewer_context.rs index 9d0a414cdc37..1b1d470dc7c5 100644 --- a/crates/viewer/re_viewer_context/src/viewer_context.rs +++ b/crates/viewer/re_viewer_context/src/viewer_context.rs @@ -82,6 +82,10 @@ pub struct ViewerContext<'a> { pub focused_item: &'a Option, /// If a selection contains any `undraggable_items`, it may not be dragged. + /// + /// This is a rather ugly workaround to handle the case of the root container not being + /// draggable, but also being unknown to the drag-and-drop machinery in `re_viewer_context`. + //TODO(ab): figure out a way to deal with that in a cleaner way. pub undraggable_items: &'a ItemCollection, } @@ -138,7 +142,7 @@ impl ViewerContext<'_> { /// Consistently handle the selection, hover, drag start interactions for a given set of items. /// /// The `draggable` parameter controls whether a drag can be initiated from this item. When a UI - /// element represents an [`Item`], one must make the call whether this element should be + /// element represents an [`crate::Item`], one must make the call whether this element should be /// meaningfully draggable by the users. This is ultimately a subjective decision, but some here /// are some guidelines: /// - Is there a meaningful destination for the dragged payload? For example, dragging stuff out