Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically expand and scroll the blueprint tree when focusing on an item #5482

Merged
merged 7 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/re_viewport/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ nohash-hasher.workspace = true
once_cell.workspace = true
rayon.workspace = true
rmp-serde.workspace = true
smallvec.workspace = true
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ impl ContextMenuAction for CollapseExpandAllAction {

fn process_container(&self, ctx: &ContextMenuContext<'_>, container_id: &ContainerId) {
ctx.viewport_blueprint
.visit_contents_in_container(container_id, &mut |contents| match contents {
.visit_contents_in_container(container_id, &mut |contents, _| match contents {
Contents::Container(container_id) => CollapseScope::BlueprintTree
.container(*container_id)
.set_open(&ctx.egui_context, self.open()),
Expand Down
7 changes: 7 additions & 0 deletions crates/re_viewport/src/viewport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ pub struct ViewportState {
///
/// See [`ViewportState::is_candidate_drop_parent_container`] for details.
candidate_drop_parent_container_id: Option<ContainerId>,

/// The item that should be focused on in the blueprint tree.
///
/// Set at each frame by [`Viewport::tree_ui`]. This is similar to
/// [`ViewerContext::focused_item`] but account for how specifically the blueprint tree should
/// handle the focused item.
pub(crate) blueprint_tree_scroll_to_item: Option<Item>,
}

static DEFAULT_PROPS: Lazy<EntityPropertyMap> = Lazy::<EntityPropertyMap>::new(Default::default);
Expand Down
37 changes: 31 additions & 6 deletions crates/re_viewport/src/viewport_blueprint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ use std::sync::atomic::{AtomicBool, Ordering};

use ahash::HashMap;
use egui_tiles::{SimplificationOptions, TileId};

use nohash_hasher::IntSet;
use smallvec::SmallVec;

use re_data_store::LatestAtQuery;
use re_entity_db::EntityPath;
use re_log_types::hash::Hash64;
Expand Down Expand Up @@ -439,26 +440,50 @@ impl ViewportBlueprint {
)
}

/// Walk the entire [`Contents`] tree, starting from the root container.
///
/// See [`Self::visit_contents_in_container`] for details.
pub fn visit_contents(&self, visitor: &mut impl FnMut(&Contents, &SmallVec<[ContainerId; 4]>)) {
if let Some(root_container) = self.root_container {
self.visit_contents_in_container(&root_container, visitor);
}
}

/// Walk the subtree defined by the provided container id and call `visitor` for each
/// [`Contents`].
///
/// Note: `visitor` is first called for the container passed in argument.
/// Note:
/// - `visitor` is first called for the container passed in argument
/// - `visitor`'s second argument contains the hierarchy leading to the visited contents, up to
/// (and including) the container passed in argument
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// - `visitor`'s second argument contains the hierarchy leading to the visited contents, up to
/// (and including) the container passed in argument
/// - `visitor`'s second argument contains the hierarchy leading to the visited contents, from
/// (and including) the container passed in argument

pub fn visit_contents_in_container(
&self,
container_id: &ContainerId,
visitor: &mut impl FnMut(&Contents),
visitor: &mut impl FnMut(&Contents, &SmallVec<[ContainerId; 4]>),
) {
let mut hierarchy = SmallVec::new();
self.visit_contents_in_container_impl(container_id, &mut hierarchy, visitor);
}

fn visit_contents_in_container_impl(
&self,
container_id: &ContainerId,
hierarchy: &mut SmallVec<[ContainerId; 4]>,
visitor: &mut impl FnMut(&Contents, &SmallVec<[ContainerId; 4]>),
) {
visitor(&Contents::Container(*container_id));
visitor(&Contents::Container(*container_id), hierarchy);
if let Some(container) = self.container(container_id) {
hierarchy.push(*container_id);
for contents in &container.contents {
visitor(contents);
visitor(contents, hierarchy);
match contents {
Contents::Container(container_id) => {
self.visit_contents_in_container(container_id, visitor);
self.visit_contents_in_container_impl(container_id, hierarchy, visitor);
}
Contents::SpaceView(_) => {}
}
}
hierarchy.pop();
}
}

Expand Down
144 changes: 141 additions & 3 deletions crates/re_viewport/src/viewport_blueprint_ui.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use egui::{Response, Ui};
use itertools::Itertools;
use re_data_ui::item_ui::guess_instance_path_icon;
use smallvec::SmallVec;

use re_entity_db::InstancePath;
use re_log_types::EntityPath;
Expand Down Expand Up @@ -54,14 +55,16 @@ impl<'a> DataResultNodeOrPath<'a> {

impl Viewport<'_, '_> {
/// Show the blueprint panel tree view.
pub fn tree_ui(&self, ctx: &ViewerContext<'_>, ui: &mut egui::Ui) {
pub fn tree_ui(&mut self, ctx: &ViewerContext<'_>, ui: &mut egui::Ui) {
re_tracing::profile_function!();

egui::ScrollArea::both()
.id_source("blueprint_tree_scroll_area")
.auto_shrink([true, false])
.show(ui, |ui| {
ctx.re_ui.panel_content(ui, |_, ui| {
self.state.blueprint_tree_scroll_to_item = self.handle_focused_item(ctx, ui);

self.root_container_tree_ui(ctx, ui);

let empty_space_response =
Expand All @@ -81,6 +84,137 @@ impl Viewport<'_, '_> {
});
}

/// Expend all required items and compute which item we should scroll to.
fn handle_focused_item(&self, ctx: &ViewerContext<'_>, ui: &egui::Ui) -> Option<Item> {
ctx.focused_item.as_ref().and_then(|focused_item| {
match focused_item {
Item::Container(container_id) => {
self.expand_all_contents_until(ui.ctx(), &Contents::Container(*container_id));
Some(focused_item.clone())
}
Item::SpaceView(space_view_id) => {
self.expand_all_contents_until(ui.ctx(), &Contents::SpaceView(*space_view_id));
ctx.focused_item.clone()
}
Item::DataResult(space_view_id, instance_path) => {
self.expand_all_contents_until(ui.ctx(), &Contents::SpaceView(*space_view_id));
self.expand_all_data_results_until(
ctx,
ui.ctx(),
space_view_id,
&instance_path.entity_path,
);

ctx.focused_item.clone()
}
Item::InstancePath(instance_path) => {
let space_view_ids =
self.list_space_views_with_entity(ctx, &instance_path.entity_path);

// focus on the first matching data result
let res = space_view_ids
.first()
.map(|id| Item::DataResult(*id, instance_path.clone()));

for space_view_id in space_view_ids {
self.expand_all_contents_until(
ui.ctx(),
&Contents::SpaceView(space_view_id),
);
self.expand_all_data_results_until(
ctx,
ui.ctx(),
&space_view_id,
&instance_path.entity_path,
);
}

res
}

Item::StoreId(_) | Item::ComponentPath(_) => None,
}
})
}

/// Expand all containers until reaching the provided content.
fn expand_all_contents_until(&self, egui_ctx: &egui::Context, focused_contents: &Contents) {
//TODO(ab): this could look nicer if `Contents` was declared in re_view_context :)
let expend_contents = |contents: &Contents| match contents {
Contents::Container(container_id) => CollapseScope::BlueprintTree
.container(*container_id)
.set_open(egui_ctx, true),
Contents::SpaceView(space_view_id) => CollapseScope::BlueprintTree
.space_view(*space_view_id)
.set_open(egui_ctx, true),
};

self.blueprint.visit_contents(&mut |contents, hierarchy| {
if contents == focused_contents {
expend_contents(contents);
for parent in hierarchy {
expend_contents(&Contents::Container(*parent));
}
}
});
}

/// List all space views that have the provided entity as data result.
#[inline]
fn list_space_views_with_entity(
&self,
ctx: &ViewerContext<'_>,
entity_path: &EntityPath,
) -> SmallVec<[SpaceViewId; 4]> {
let mut space_view_ids = SmallVec::new();
self.blueprint.visit_contents(&mut |contents, _| {
if let Contents::SpaceView(space_view_id) = contents {
let result_tree = &ctx.lookup_query_result(*space_view_id).tree;
if result_tree.lookup_node_by_path(entity_path).is_some() {
space_view_ids.push(*space_view_id);
}
}
});
space_view_ids
}

/// Expand data results of the provided space view all the way to the provided entity.
#[allow(clippy::unused_self)]
fn expand_all_data_results_until(
&self,
ctx: &ViewerContext<'_>,
egui_ctx: &egui::Context,
space_view_id: &SpaceViewId,
entity_path: &EntityPath,
) {
let result_tree = &ctx.lookup_query_result(*space_view_id).tree;
if result_tree.lookup_node_by_path(entity_path).is_some() {
EntityPath::incremental_walk(
result_tree
.root_node()
.map(|node| &node.data_result.entity_path),
entity_path,
)
.for_each(|entity_path| {
CollapseScope::BlueprintTree
.data_result(*space_view_id, entity_path)
.set_open(egui_ctx, true);
});
}
}

/// Check if the provided item should be scrolled to.
fn scroll_to_me_if_needed(&self, ui: &egui::Ui, item: &Item, response: &egui::Response) {
if Some(item) == self.state.blueprint_tree_scroll_to_item.as_ref() {
// Scroll only if the entity isn't already visible. This is important because that's what
// happens when double-clicking an entity _in the blueprint tree_. In such case, it would be
// annoying to induce a scroll motion.
if !ui.clip_rect().contains_rect(response.rect) {
response.scroll_to_me(Some(egui::Align::Center));
}
}
}

/// If a group or spaceview has a total of this number of elements, show its subtree by default?
fn default_open_for_data_result(group: &DataResultNode) -> bool {
let num_children = group.children.len();
Expand Down Expand Up @@ -145,6 +279,7 @@ impl Viewport<'_, '_> {
&item_response,
SelectionUpdateBehavior::UseSelection,
);
self.scroll_to_me_if_needed(ui, &item, &item_response);
ctx.select_hovered_on_click(&item_response, item);

self.handle_root_container_drag_and_drop_interaction(
Expand Down Expand Up @@ -218,6 +353,7 @@ impl Viewport<'_, '_> {
&response,
SelectionUpdateBehavior::UseSelection,
);
self.scroll_to_me_if_needed(ui, &item, &response);
ctx.select_hovered_on_click(&response, item);

self.blueprint
Expand Down Expand Up @@ -304,15 +440,15 @@ impl Viewport<'_, '_> {
);

// Show 'projections' if there's any items that weren't part of the tree under origin but are directly included.
// The later is important since `+ image/camera/**` necessarily has `image` and `image/camera` in the data result tree.
// The latter is important since `+ image/camera/**` necessarily has `image` and `image/camera` in the data result tree.
let mut projections = Vec::new();
result_tree.visit(&mut |node| {
if node
.data_result
.entity_path
.starts_with(&space_view.space_origin)
{
false // If its under the origin, we're not interested, stop recursing.
false // If it's under the origin, we're not interested, stop recursing.
} else if node.data_result.tree_prefix_only {
true // Keep recursing until we find a projection.
} else {
Expand Down Expand Up @@ -351,6 +487,7 @@ impl Viewport<'_, '_> {
&response,
SelectionUpdateBehavior::UseSelection,
);
self.scroll_to_me_if_needed(ui, &item, &response);
ctx.select_hovered_on_click(&response, item);

let content = Contents::SpaceView(*space_view_id);
Expand Down Expand Up @@ -526,6 +663,7 @@ impl Viewport<'_, '_> {
&response,
SelectionUpdateBehavior::UseSelection,
);
self.scroll_to_me_if_needed(ui, &item, &response);
ctx.select_hovered_on_click(&response, item);
}

Expand Down
Loading