Skip to content

Commit

Permalink
Add helpers to enable stable and controllable collapsed state in hier…
Browse files Browse the repository at this point in the history
…archical lists (#5362)

### What

This PR introduces a set of helper objects to assist in producing
predictable (and thus stable) `egui::Id` for various objects of our data
model (container, space view, data result, etc.). Since the collapsed
state is in general not shared across different UI area, this PR
proposes a scoping mechanism (implemented with the type system), so the
id for a given item is different depending on, e.g., its appearance in
the blueprint tree or the streams tree.

This improves the collapsed state in hierarchical lists in two ways:
- The collapsed state is now preserved when objects move in the
hierarchy (e.g. a space view is moved from a container to another).
Fixes #5208
- It is now possible to "remotely" change the collapsed state of a given
item, in a given scope.

⚠️⚠️ Demo "Expend all/Collapse all" buttons are included in the
container selection panel, to be reverted before merging.


https://github.com/rerun-io/rerun/assets/49431240/e0d6c47f-59f6-4be1-96d9-03d38d20a580

Blocked by:
- #5371

To address in follow-up PRs:
- ~~fine-tune the DataResult representation such as to ensure sufficient
specificity in case the DataResult appears multiple times in the space
view blueprint sub-tree / cc @Wumpf~~
- add "collapse all"/"expand all" context menu actions
- improve "double-click focus" to uncollapse the focused items

### Checklist
* [x] I have read and agree to [Contributor
Guide](https://github.com/rerun-io/rerun/blob/main/CONTRIBUTING.md) and
the [Code of
Conduct](https://github.com/rerun-io/rerun/blob/main/CODE_OF_CONDUCT.md)
* [x] I've included a screenshot or gif (if applicable)
* [x] I have tested the web demo (if applicable):
* Using newly built examples:
[app.rerun.io](https://app.rerun.io/pr/5362/index.html)
* Using examples from latest `main` build:
[app.rerun.io](https://app.rerun.io/pr/5362/index.html?manifest_url=https://app.rerun.io/version/main/examples_manifest.json)
* Using full set of examples from `nightly` build:
[app.rerun.io](https://app.rerun.io/pr/5362/index.html?manifest_url=https://app.rerun.io/version/nightly/examples_manifest.json)
* [x] The PR title and labels are set such as to maximize their
usefulness for the next release's CHANGELOG
* [x] If applicable, add a new check to the [release
checklist](https://github.com/rerun-io/rerun/blob/main/tests/python/release_checklist)!

- [PR Build Summary](https://build.rerun.io/pr/5362)
- [Docs
preview](https://rerun.io/preview/250f8e69ccbf28a29980a114ec8fa7ed535c05a8/docs)
<!--DOCS-PREVIEW-->
- [Examples
preview](https://rerun.io/preview/250f8e69ccbf28a29980a114ec8fa7ed535c05a8/examples)
<!--EXAMPLES-PREVIEW-->
- [Recent benchmark results](https://build.rerun.io/graphs/crates.html)
- [Wasm size tracking](https://build.rerun.io/graphs/sizes.html)
  • Loading branch information
abey79 authored Mar 2, 2024
1 parent f7ea27e commit f719d7e
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 82 deletions.
30 changes: 17 additions & 13 deletions crates/re_time_panel/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use re_log_types::{
};
use re_ui::list_item::{ListItem, WidthAllocationMode};
use re_viewer_context::{
HoverHighlight, Item, RecordingConfig, TimeControl, TimeView, ViewerContext,
CollapseScope, HoverHighlight, Item, RecordingConfig, TimeControl, TimeView, ViewerContext,
};

use time_axis::TimelineAxis;
Expand Down Expand Up @@ -555,7 +555,6 @@ impl TimePanel {
show_root_as.to_owned()
};

let collapsing_header_id = ui.make_persistent_id(&tree.path);
let default_open = tree.path.len() <= 1 && !tree.is_leaf();

let item = TimePanelItem::entity_path(tree.path.clone());
Expand All @@ -578,17 +577,22 @@ impl TimePanel {
.width_allocation_mode(WidthAllocationMode::Compact)
.selected(is_selected)
.force_hovered(is_item_hovered)
.show_collapsing(ui, collapsing_header_id, default_open, |_, ui| {
self.show_children(
ctx,
time_ctrl,
time_area_response,
time_area_painter,
tree_max_y,
tree,
ui,
);
});
.show_collapsing(
ui,
CollapseScope::StreamsTree.entity(tree.path.clone()),
default_open,
|_, ui| {
self.show_children(
ctx,
time_ctrl,
time_area_response,
time_area_painter,
tree_max_y,
tree,
ui,
);
},
);

ui.set_clip_rect(clip_rect_save);

Expand Down
25 changes: 10 additions & 15 deletions crates/re_ui/examples/re_ui_example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,20 +418,15 @@ impl eframe::App for ExampleApp {
self.re_ui
.list_item("Collapsing list item with icon")
.with_icon(&re_ui::icons::SPACE_VIEW_2D)
.show_collapsing(
ui,
"collapsing example".into(),
true,
|_re_ui, ui| {
self.re_ui.list_item("Sub-item").show(ui);
self.re_ui.list_item("Sub-item").show(ui);
self.re_ui
.list_item("Sub-item with icon")
.with_icon(&re_ui::icons::SPACE_VIEW_TEXT)
.show(ui);
self.re_ui.list_item("Sub-item").show(ui);
},
);
.show_collapsing(ui, "collapsing example", true, |_re_ui, ui| {
self.re_ui.list_item("Sub-item").show(ui);
self.re_ui.list_item("Sub-item").show(ui);
self.re_ui
.list_item("Sub-item with icon")
.with_icon(&re_ui::icons::SPACE_VIEW_TEXT)
.show(ui);
self.re_ui.list_item("Sub-item").show(ui);
});
});
});
});
Expand Down Expand Up @@ -1055,7 +1050,7 @@ mod hierarchical_drag_and_drop {
.selected(self.selected(item_id))
.draggable(true)
.drop_target_style(self.target_container == Some(item_id))
.show_collapsing(ui, item_id.into(), true, |re_ui, ui| {
.show_collapsing(ui, item_id, true, |re_ui, ui| {
self.container_children_ui(re_ui, ui, children);
});

Expand Down
4 changes: 3 additions & 1 deletion crates/re_ui/src/list_item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,10 +296,12 @@ impl<'a> ListItem<'a> {
pub fn show_collapsing<R>(
mut self,
ui: &mut Ui,
id: egui::Id,
id: impl Into<egui::Id>,
default_open: bool,
add_body: impl FnOnce(&ReUi, &mut egui::Ui) -> R,
) -> ShowCollapsingResponse<R> {
let id = id.into();

let mut state = egui::collapsing_header::CollapsingState::load_with_default_open(
ui.ctx(),
id,
Expand Down
118 changes: 118 additions & 0 deletions crates/re_viewer_context/src/collapsed_id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//! Helper types for producing stable [`egui::Id`] for the purpose of handling collapsed state of
//! various UI elements.
use std::hash::Hash;

use re_log_types::EntityPath;

use crate::{ContainerId, SpaceViewId};

/// The various scopes for which we want to track collapsed state.
#[derive(Debug, Clone, Copy, Hash)]
pub enum CollapseScope {
/// Stream tree from the time panel
StreamsTree,

/// Blueprint tree from the blueprint panel
BlueprintTree,
}

impl CollapseScope {
const ALL: [CollapseScope; 2] = [CollapseScope::StreamsTree, CollapseScope::BlueprintTree];

// convenience functions

/// Create a [`CollapsedId`] for a container in this scope.
pub fn container(self, container_id: ContainerId) -> CollapsedId {
CollapsedId {
item: CollapseItem::Container(container_id),
scope: self,
}
}

/// Create a [`CollapsedId`] for a space view in this scope.
pub fn space_view(self, space_view_id: SpaceViewId) -> CollapsedId {
CollapsedId {
item: CollapseItem::SpaceView(space_view_id),
scope: self,
}
}

/// Create a [`CollapsedId`] for a data result in this scope.
pub fn data_result(self, space_view_id: SpaceViewId, entity_path: EntityPath) -> CollapsedId {
CollapsedId {
item: CollapseItem::DataResult(space_view_id, entity_path),
scope: self,
}
}

/// Create a [`CollapsedId`] for an entity in this scope.
pub fn entity(self, entity_path: EntityPath) -> CollapsedId {
CollapsedId {
item: CollapseItem::Entity(entity_path),
scope: self,
}
}
}

/// The various kinds of items that may be represented and for which we want to track the collapsed
/// state.
#[derive(Debug, Clone, Hash)]
pub enum CollapseItem {
Container(ContainerId),
SpaceView(SpaceViewId),
DataResult(SpaceViewId, EntityPath),
Entity(EntityPath),
}

impl CollapseItem {
/// Set the collapsed state for the given item in every available scopes.
pub fn set_open_all(&self, ctx: &egui::Context, open: bool) {
for scope in CollapseScope::ALL {
let id = CollapsedId {
item: self.clone(),
scope,
};
id.set_open(ctx, open);
}
}
}

/// A collapsed identifier.
///
/// A `CollapsedId` resolves into a stable [`egui::Id`] for a given item and scope.
#[derive(Debug, Clone, Hash)]
pub struct CollapsedId {
item: CollapseItem,
scope: CollapseScope,
}

impl From<CollapsedId> for egui::Id {
fn from(id: CollapsedId) -> Self {
egui::Id::new(id)
}
}

impl CollapsedId {
/// Convert to an [`egui::Id`].
pub fn egui_id(&self) -> egui::Id {
self.clone().into()
}

/// Check the collapsed state for the given [`CollapsedId`].
pub fn is_open(&self, ctx: &egui::Context) -> Option<bool> {
egui::collapsing_header::CollapsingState::load(ctx, self.egui_id())
.map(|state| state.is_open())
}

/// Set the collapsed state for the given [`CollapsedId`].
pub fn set_open(&self, ctx: &egui::Context, open: bool) {
let mut collapsing_state = egui::collapsing_header::CollapsingState::load_with_default_open(
ctx,
self.egui_id(),
false,
);
collapsing_state.set_open(open);
collapsing_state.store(ctx);
}
}
2 changes: 2 additions & 0 deletions crates/re_viewer_context/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod app_options;
mod blueprint_helpers;
mod blueprint_id;
mod caches;
mod collapsed_id;
mod command_sender;
mod component_ui_registry;
mod item;
Expand All @@ -31,6 +32,7 @@ pub use app_options::AppOptions;
pub use blueprint_helpers::{blueprint_timeline, blueprint_timepoint_for_writes};
pub use blueprint_id::{BlueprintId, BlueprintIdRegistry, ContainerId, DataQueryId, SpaceViewId};
pub use caches::{Cache, Caches};
pub use collapsed_id::{CollapseItem, CollapseScope, CollapsedId};
pub use command_sender::{
command_channel, CommandReceiver, CommandSender, SystemCommand, SystemCommandSender,
};
Expand Down
23 changes: 23 additions & 0 deletions crates/re_viewport/src/viewport_blueprint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,29 @@ impl ViewportBlueprint {
)
}

/// 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.
pub fn visit_contents_in_container(
&self,
container_id: &ContainerId,
visitor: &mut impl FnMut(&Contents),
) {
visitor(&Contents::Container(*container_id));
if let Some(container) = self.container(container_id) {
for contents in &container.contents {
visitor(contents);
match contents {
Contents::Container(container_id) => {
self.visit_contents_in_container(container_id, visitor);
}
Contents::SpaceView(_) => {}
}
}
}
}

/// Given a predicate, finds the (first) matching contents by recursively walking from the root
/// container.
pub fn find_contents_by(&self, predicate: &impl Fn(&Contents) -> bool) -> Option<Contents> {
Expand Down
Loading

0 comments on commit f719d7e

Please sign in to comment.