Skip to content

Filter entities in the UI (part 3): Move action to a menu in the blueprint panel and keep default blueprint when using heuristics #8672

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

Merged
merged 8 commits into from
Jan 14, 2025
148 changes: 92 additions & 56 deletions crates/viewer/re_blueprint_tree/src/blueprint_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,25 @@ impl BlueprintTree {
ui: &mut egui::Ui,
) {
ui.panel_content(|ui| {
ui.panel_title_bar_with_buttons(
"Blueprint",
Some("The blueprint is where you can configure the Rerun Viewer"),
|ui| {
self.add_new_view_button_ui(ctx, viewport, ui);
reset_blueprint_button_ui(ctx, ui);
},
);
ui.full_span_separator();
ui.add_space(-1.);

ui.list_item_scope("blueprint section title", |ui| {
ui.list_item_flat_noninteractive(
list_item::CustomContent::new(|ui, _| {
ui.strong("Blueprint").on_hover_text(
"The blueprint is where you can configure the Rerun Viewer",
);
})
.menu_button(&re_ui::icons::MORE, |ui| {
add_new_view_or_container_menu_button(ctx, viewport, ui);
set_blueprint_to_default_menu_buttons(ctx, ui);
set_blueprint_to_auto_menu_button(ctx, viewport, ui);
}),
);
});

ui.full_span_separator();
});

// This call is excluded from `panel_content` because it has a ScrollArea, which should not be
Expand Down Expand Up @@ -602,32 +613,6 @@ impl BlueprintTree {
ctx.handle_select_hover_drag_interactions(&response, item, true);
}

/// Add a button to trigger the addition of a new view or container.
#[allow(clippy::unused_self)]
fn add_new_view_button_ui(
&self,
ctx: &ViewerContext<'_>,
viewport: &ViewportBlueprint,
ui: &mut egui::Ui,
) {
if ui
.small_icon_button(&re_ui::icons::ADD)
.on_hover_text("Add a new view or container")
.clicked()
{
// If a single container is selected, we use it as target. Otherwise, we target the
// root container.
let target_container_id =
if let Some(Item::Container(container_id)) = ctx.selection().single_item() {
*container_id
} else {
viewport.root_container
};

show_add_view_or_container_modal(target_container_id);
}
}

// ----------------------------------------------------------------------------
// drag and drop support

Expand Down Expand Up @@ -868,46 +853,97 @@ impl BlueprintTree {

// ----------------------------------------------------------------------------

fn reset_blueprint_button_ui(ctx: &ViewerContext<'_>, ui: &mut egui::Ui) {
/// Add a button to trigger the addition of a new view or container.
fn add_new_view_or_container_menu_button(
ctx: &ViewerContext<'_>,
viewport: &ViewportBlueprint,
ui: &mut egui::Ui,
) {
if ui
.add(egui::Button::image_and_text(
&re_ui::icons::ADD,
"Add view or container…",
))
.clicked()
{
ui.close_menu();

// If a single container is selected, we use it as target. Otherwise, we target the
// root container.
let target_container_id =
if let Some(Item::Container(container_id)) = ctx.selection().single_item() {
*container_id
} else {
viewport.root_container
};

show_add_view_or_container_modal(target_container_id);
}
}

fn set_blueprint_to_default_menu_buttons(ctx: &ViewerContext<'_>, ui: &mut egui::Ui) {
let default_blueprint_id = ctx
.store_context
.hub
.default_blueprint_id_for_app(&ctx.store_context.app_id);

let default_blueprint = default_blueprint_id.and_then(|id| ctx.store_context.bundle.get(id));

let mut disabled_reason = None;

if let Some(default_blueprint) = default_blueprint {
let active_is_clone_of_default = Some(default_blueprint.store_id()).as_ref()
== ctx.store_context.blueprint.cloned_from();
let last_modified_at_the_same_time =
default_blueprint.latest_row_id() == ctx.store_context.blueprint.latest_row_id();
if active_is_clone_of_default && last_modified_at_the_same_time {
disabled_reason = Some("No modifications have been made");
let disabled_reason = match default_blueprint {
None => Some("No default blueprint is set for this app"),
Some(default_blueprint) => {
let active_is_clone_of_default = Some(default_blueprint.store_id()).as_ref()
== ctx.store_context.blueprint.cloned_from();
let last_modified_at_the_same_time =
default_blueprint.latest_row_id() == ctx.store_context.blueprint.latest_row_id();
if active_is_clone_of_default && last_modified_at_the_same_time {
Some("No modifications have been made")
} else {
None // it is valid to reset to default
}
}
}
};

let enabled = disabled_reason.is_none();
let response = ui.add_enabled(enabled, ui.small_icon_button_widget(&re_ui::icons::RESET));

let response = if let Some(disabled_reason) = disabled_reason {
response.on_disabled_hover_text(disabled_reason)
} else {
let hover_text = if default_blueprint_id.is_some() {
"Reset to the default blueprint for this app"
} else {
"Re-populate viewport with automatically chosen views"
};
response.on_hover_text(hover_text)
let mut response = ui
.add_enabled(
enabled,
egui::Button::image_and_text(&re_ui::icons::RESET, "Reset to default blueprint"),
)
.on_hover_text("Reset to the default blueprint for this app");

if let Some(disabled_reason) = disabled_reason {
response = response.on_disabled_hover_text(disabled_reason);
};

if response.clicked() {
ui.close_menu();
ctx.command_sender
.send_system(re_viewer_context::SystemCommand::ClearActiveBlueprint);
}
}

fn set_blueprint_to_auto_menu_button(
ctx: &ViewerContext<'_>,
viewport: &ViewportBlueprint,
ui: &mut egui::Ui,
) {
let enabled = !viewport.auto_layout() || !viewport.auto_views();

if ui
.add_enabled(
enabled,
egui::Button::image_and_text(&re_ui::icons::RESET, "Reset to heuristic blueprint"),
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if we can come up with better copy here. "Create automatic blueprint"? Maybe @gavrelina has ideas

Copy link
Member Author

Choose a reason for hiding this comment

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

We discussed that and decided to stick with "heuristics" for now.

)
.on_hover_text("Re-populate viewport with automatically chosen views")
.clicked()
{
ui.close_menu();
ctx.command_sender
.send_system(re_viewer_context::SystemCommand::ClearAndGenerateBlueprint);
}
}

/// Expand all required items and compute which item we should scroll to.
fn handle_focused_item(
ctx: &ViewerContext<'_>,
Expand Down
2 changes: 2 additions & 0 deletions crates/viewer/re_data_ui/src/app_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ impl crate::DataUi for ApplicationId {
}
}

// TODO(ab): this should ideally be disabled if the blueprint is already on auto mode,
// but that would require access to the `ViewportBlueprint`
if ui.add(egui::Button::image_and_text(
&re_ui::icons::RESET,
"Reset to heuristic blueprint",
Expand Down
10 changes: 10 additions & 0 deletions crates/viewer/re_ui/src/ui_ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,16 @@ pub trait UiExt {
self.ui().painter().add(shadow);
}

/// Convenience function to create a [`list_item::list_item_scope`].
#[inline]
fn list_item_scope<R>(
&mut self,
id_salt: impl std::hash::Hash,
content: impl FnOnce(&mut egui::Ui) -> R,
) -> R {
list_item::list_item_scope(self.ui_mut(), id_salt, content)
}

/// Convenience function to create a [`list_item::ListItem`].
#[allow(clippy::unused_self)]
fn list_item(&self) -> list_item::ListItem {
Expand Down
1 change: 1 addition & 0 deletions crates/viewer/re_view_graph/tests/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ fn run_graph_view_and_save_snapshot(
bundle: &Default::default(),
caches: &Default::default(),
hub: &Default::default(),
should_enable_heuristics: false,
};

// Execute the queries for every `View`
Expand Down
6 changes: 2 additions & 4 deletions crates/viewer/re_viewer/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,10 +526,8 @@ impl App {
SystemCommand::ResetViewer => self.reset_viewer(store_hub, egui_ctx),
SystemCommand::ClearAndGenerateBlueprint => {
re_log::debug!("Clear and generate new blueprint");
// By clearing the default blueprint and the active blueprint
// it will be re-generated based on the default auto behavior.
store_hub.clear_default_blueprint();
store_hub.clear_active_blueprint();
store_hub.clear_active_blueprint_and_generate();
egui_ctx.request_repaint(); // Many changes take a frame delay to show up.
}
SystemCommand::ClearActiveBlueprint => {
// By clearing the blueprint the default blueprint will be restored
Expand Down
7 changes: 7 additions & 0 deletions crates/viewer/re_viewer/src/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,13 @@ impl AppState {
drag_and_drop_manager: &drag_and_drop_manager,
};

// enable the heuristics if we must this frame
if store_context.should_enable_heuristics {
viewport_ui.blueprint.set_auto_layout(true, &ctx);
viewport_ui.blueprint.set_auto_views(true, &ctx);
egui_ctx.request_repaint();
}

// We move the time at the very start of the frame,
// so that we always show the latest data when we're in "follow" mode.
move_time(&ctx, recording, rx);
Expand Down
5 changes: 4 additions & 1 deletion crates/viewer/re_viewer_context/src/command_sender.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ pub enum SystemCommand {
/// Reset the `Blueprint` to the default state
ClearActiveBlueprint,

/// Clear the blueprint and generate a new one
/// Clear the blueprint and enable heuristics.
///
/// Note: this engages the heuristics even if a default blueprint exists, so the default
/// blueprint may be restored.
Copy link
Member

Choose a reason for hiding this comment

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

I don't follow this. Why do we need to explicitly engage the heuristics to restore the default blueprint? Is the heuristic bools not part of the actual blueprint? If not, why aren't they reset automatically when there is no blueprint (i.e. after a reset)?

Copy link
Member Author

Choose a reason for hiding this comment

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

Bad wording on my end, I meant to imply that the default blueprint (if any) is left alone, so the user may restore it later on. I've rewritten entirely that (and other) comment to clarify this.

ClearAndGenerateBlueprint,

/// If this is a recording, switch to it.
Expand Down
3 changes: 3 additions & 0 deletions crates/viewer/re_viewer_context/src/store_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ pub struct StoreContext<'a> {

/// The store hub, which keeps track of all the default and active blueprints, among other things.
pub hub: &'a StoreHub,

/// Should we enable the heuristics during this frame?
pub should_enable_heuristics: bool,
}

impl StoreContext<'_> {
Expand Down
39 changes: 28 additions & 11 deletions crates/viewer/re_viewer_context/src/store_hub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ pub struct StoreHub {
active_blueprint_by_app_id: HashMap<ApplicationId, StoreId>,
store_bundle: StoreBundle,

/// These applications should enable the heuristics early next frame.
should_enable_heuristics_by_app_id: HashSet<ApplicationId>,

/// Things that need caching.
caches_per_recording: HashMap<StoreId, Caches>,

Expand Down Expand Up @@ -143,6 +146,8 @@ impl StoreHub {
active_blueprint_by_app_id: Default::default(),
store_bundle,

should_enable_heuristics_by_app_id: Default::default(),

caches_per_recording: Default::default(),
blueprint_last_save: Default::default(),
blueprint_last_gc: Default::default(),
Expand Down Expand Up @@ -184,8 +189,11 @@ impl StoreHub {
}
}

// If there's no active blueprint for this app, try to make the current default one active.
if !self.active_blueprint_by_app_id.contains_key(&app_id) {
// If there's no active blueprint for this app, we must use the default blueprint, UNLESS
// we're about to enable heuristics for this app.
if !self.active_blueprint_by_app_id.contains_key(&app_id)
&& !self.should_enable_heuristics_by_app_id.contains(&app_id)
{
if let Some(blueprint_id) = self.default_blueprint_by_app_id.get(&app_id).cloned() {
self.set_cloned_blueprint_active_for_app(&app_id, &blueprint_id)
.unwrap_or_else(|err| {
Expand Down Expand Up @@ -220,6 +228,7 @@ impl StoreHub {
self.active_rec_id = None;
}

let should_enable_heuristics = self.should_enable_heuristics_by_app_id.remove(&app_id);
let caches = self.active_caches();

Some(StoreContext {
Expand All @@ -230,6 +239,7 @@ impl StoreHub {
bundle: &self.store_bundle,
caches: caches.unwrap_or(&EMPTY_CACHES),
hub: self,
should_enable_heuristics,
})
}

Expand Down Expand Up @@ -497,15 +507,6 @@ impl StoreHub {
Ok(())
}

/// Clear the current default blueprint
pub fn clear_default_blueprint(&mut self) {
if let Some(app_id) = &self.active_application_id {
if let Some(blueprint_id) = self.default_blueprint_by_app_id.remove(app_id) {
self.remove(&blueprint_id);
}
}
}

// ---------------------
// Active blueprint

Expand Down Expand Up @@ -578,6 +579,22 @@ impl StoreHub {
}
}

/// Clear the currently active blueprint and enable the heuristics to generate a new one.
///
/// These keeps the default blueprint as is, so the user may reset to the default blueprint
/// afterward.
pub fn clear_active_blueprint_and_generate(&mut self) {
self.clear_active_blueprint();

// Simply clearing the default blueprint would trigger a reset to default. Instead, we must
// actively set the blueprint to use the heuristics, so we store a flag so we do that early
// next frame.
if let Some(app_id) = self.active_app() {
self.should_enable_heuristics_by_app_id
.insert(app_id.clone());
}
}

// ---------------------
// Misc operations

Expand Down
1 change: 1 addition & 0 deletions crates/viewer/re_viewer_context/src/test_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ impl TestContext {
bundle: &Default::default(),
caches: &Default::default(),
hub: &Default::default(),
should_enable_heuristics: false,
};

let drag_and_drop_manager = crate::DragAndDropManager::new(ItemCollection::default());
Expand Down
1 change: 1 addition & 0 deletions crates/viewer/re_viewport_blueprint/src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,7 @@ mod tests {
bundle: &Default::default(),
caches: &Default::default(),
hub: &re_viewer_context::StoreHub::test_hub(),
should_enable_heuristics: false,
};

let mut query_result = view.contents.execute_query(
Expand Down
1 change: 1 addition & 0 deletions crates/viewer/re_viewport_blueprint/src/view_contents.rs
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,7 @@ mod tests {
bundle: &Default::default(),
caches: &Default::default(),
hub: &StoreHub::test_hub(),
should_enable_heuristics: false,
};

struct Scenario {
Expand Down
Loading