Skip to content

Commit

Permalink
More compact selection panel when multiple items selected (#8351)
Browse files Browse the repository at this point in the history
### Related
* Closes #8336

### What
When you have more than one item selected at once, you get a list of all
items. Clicking one will select just that.

<img width="308" alt="multiple-selected-entities"
src="https://github.com/user-attachments/assets/67174eed-373a-486c-a2b1-b97a08136805">

### Implementation
A bit messy, to be honest. I leveraged the existing `ItemTitle` for some
items, and hand-rolled others.

It would be nice with something similar to `DataUi`, but for titles
(with or without breadcrumbs).

But the amount of messy code is low and low-impact, so unless someone
has a good suggestion or strongly objects, I suggestion calling this
"good enough for now".
  • Loading branch information
emilk authored Dec 9, 2024
1 parent b45cde6 commit d69fbc6
Show file tree
Hide file tree
Showing 20 changed files with 261 additions and 123 deletions.
1 change: 1 addition & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6143,6 +6143,7 @@ dependencies = [
"re_context_menu",
"re_data_ui",
"re_entity_db",
"re_format",
"re_log",
"re_log_types",
"re_space_view",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ fn edit_multiline_string_impl(
*value = edit_name.into();
response
} else {
UiLayout::SelectionPanelFull.data_label(ui, value.as_str())
UiLayout::SelectionPanel.data_label(ui, value.as_str())
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/viewer/re_component_ui/src/geo_line_string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ fn multiline_view_geo_line_string(
// TODO(andreas): Editing this would be nice!
let value = value.as_ref();

UiLayout::SelectionPanelFull
UiLayout::SelectionPanel
.table(ui)
.resizable(true)
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
Expand Down
4 changes: 2 additions & 2 deletions crates/viewer/re_component_ui/src/line_strip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ fn multiline_view_line_strip_3d(

// TODO(andreas): Is it really a good idea to always have the full table here?
// Can we use the ui stack to know where we are and do the right thing instead?
UiLayout::SelectionPanelFull
UiLayout::SelectionPanel
.table(ui)
.resizable(true)
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
Expand Down Expand Up @@ -89,7 +89,7 @@ fn multiline_view_line_strip_2d(

// TODO(andreas): Is it really a good idea to always have the full table here?
// Can we use the ui stack to know where we are and do the right thing instead?
UiLayout::SelectionPanelFull
UiLayout::SelectionPanel
.table(ui)
.resizable(true)
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,28 +36,25 @@ impl ContextMenuAction for AddEntitiesToNewSpaceViewAction {
.collect();

ui.menu_button("Add to new space view", |ui| {
let buttons_for_space_view_classes = |ui: &mut egui::Ui,
space_view_classes: &IntSet<
SpaceViewClassIdentifier,
>| {
for (identifier, class) in space_view_classes
.iter()
.map(|identifier| {
(
identifier,
space_view_class_registry.get_class_or_log_error(*identifier),
)
})
.sorted_by_key(|(_, class)| class.display_name().to_owned())
{
let btn =
egui::Button::image_and_text(class.icon().as_image(), class.display_name());
if ui.add(btn).clicked() {
create_space_view_for_selected_entities(ctx, *identifier);
ui.close_menu();
let buttons_for_space_view_classes =
|ui: &mut egui::Ui, space_view_classes: &IntSet<SpaceViewClassIdentifier>| {
for (identifier, class) in space_view_classes
.iter()
.map(|identifier| {
(
identifier,
space_view_class_registry.get_class_or_log_error(*identifier),
)
})
.sorted_by_key(|(_, class)| class.display_name().to_owned())
{
let btn = egui::Button::image_and_text(class.icon(), class.display_name());
if ui.add(btn).clicked() {
create_space_view_for_selected_entities(ctx, *identifier);
ui.close_menu();
}
}
}
};
};

ui.label(egui::WidgetText::from("Recommended:").italics());
if recommended_space_view_classes.is_empty() {
Expand Down
13 changes: 3 additions & 10 deletions crates/viewer/re_data_ui/src/annotation_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ impl DataUi for AnnotationContext {
};
ui_layout.label(ui, text);
}
UiLayout::SelectionPanelLimitHeight | UiLayout::SelectionPanelFull => {
UiLayout::SelectionPanel => {
ui.vertical(|ui| {
ui.maybe_collapsing_header(true, "Classes", true, |ui| {
let annotation_infos = self
Expand All @@ -153,7 +153,7 @@ impl DataUi for AnnotationContext {

fn class_description_ui(
ui: &mut egui::Ui,
mut ui_layout: UiLayout,
ui_layout: UiLayout,
class: &ClassDescription,
id: re_types::datatypes::ClassId,
) {
Expand All @@ -163,14 +163,7 @@ fn class_description_ui(

re_tracing::profile_function!();

let use_collapsible = ui_layout == UiLayout::SelectionPanelLimitHeight
|| ui_layout == UiLayout::SelectionPanelFull;

// We use collapsible header as a means for the user to limit the height, so the annotation info
// tables can be fully unrolled.
if ui_layout == UiLayout::SelectionPanelLimitHeight {
ui_layout = UiLayout::SelectionPanelFull;
}
let use_collapsible = ui_layout == UiLayout::SelectionPanel;

let row_height = DesignTokens::table_line_height();
if !class.keypoint_annotations.is_empty() {
Expand Down
8 changes: 3 additions & 5 deletions crates/viewer/re_data_ui/src/app_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,8 @@ impl crate::DataUi for ApplicationId {
let active_blueprint = ctx.store_context.blueprint;
let default_blueprint = ctx.store_context.hub.default_blueprint_for_app(self);

let button = egui::Button::image_and_text(
re_ui::icons::RESET.as_image(),
"Reset to default blueprint",
);
let button =
egui::Button::image_and_text(&re_ui::icons::RESET, "Reset to default blueprint");

let is_same_as_default = default_blueprint.map_or(false, |default_blueprint| {
default_blueprint.latest_row_id() == active_blueprint.latest_row_id()
Expand All @@ -95,7 +93,7 @@ impl crate::DataUi for ApplicationId {
}

if ui.add(egui::Button::image_and_text(
re_ui::icons::RESET.as_image(),
&re_ui::icons::RESET,
"Reset to heuristic blueprint",
)).on_hover_text("Clear both active and default blueprint, and auto-generate a new blueprint based on heuristics").clicked() {
ctx.command_sender
Expand Down
2 changes: 1 addition & 1 deletion crates/viewer/re_data_ui/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ impl DataUi for ComponentPathLatestAtResults<'_> {
let max_row = match ui_layout {
UiLayout::List => 0,
UiLayout::Tooltip => num_instances.at_most(4), // includes "…x more" if any
UiLayout::SelectionPanelLimitHeight | UiLayout::SelectionPanelFull => num_instances,
UiLayout::SelectionPanel => num_instances,
};

let engine = db.storage_engine();
Expand Down
2 changes: 1 addition & 1 deletion crates/viewer/re_data_ui/src/instance_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ fn component_list_ui(
// types of components).
indicator_count == components.len()
}
UiLayout::SelectionPanelLimitHeight | UiLayout::SelectionPanelFull => true,
UiLayout::SelectionPanel => true,
UiLayout::List => false, // unreachable
};

Expand Down
6 changes: 3 additions & 3 deletions crates/viewer/re_selection_panel/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ workspace = true
all-features = true

[dependencies]
re_chunk_store.workspace = true
re_chunk.workspace = true
re_context_menu.workspace = true
re_chunk_store.workspace = true
re_data_ui.workspace = true
re_entity_db.workspace = true
re_format.workspace = true
re_log_types.workspace = true
re_log.workspace = true
re_space_view.workspace = true
re_tracing.workspace = true
# TODO(jleibs): Remove this once VisualizerOverrides is gone
re_types_blueprint.workspace = true
re_types_blueprint.workspace = true # TODO(jleibs): Remove this once VisualizerOverrides is gone
re_types_core.workspace = true
re_types.workspace = true
re_ui.workspace = true
Expand Down
113 changes: 113 additions & 0 deletions crates/viewer/re_selection_panel/src/item_heading_no_breadcrumbs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
use egui::WidgetText;

use re_data_ui::item_ui::{cursor_interact_with_selectable, guess_instance_path_icon};
use re_log_types::ComponentPath;
use re_ui::{icons, list_item, Icon, SyntaxHighlighting, UiExt as _};
use re_viewer_context::{Item, ViewerContext};
use re_viewport_blueprint::ViewportBlueprint;

use crate::{
item_heading_with_breadcrumbs::separator_icon_ui,
item_title::{is_component_static, ItemTitle},
};

/// Just the title of the item; for when multiple items are selected
pub fn item_title_list_item(
ctx: &ViewerContext<'_>,
viewport: &ViewportBlueprint,
ui: &mut egui::Ui,
item: &Item,
) {
let response = ui
.list_item()
.with_height(re_ui::DesignTokens::list_item_height())
.interactive(true)
.show_flat(
ui,
list_item::CustomContent::new(|ui, context| {
ui.allocate_new_ui(
egui::UiBuilder::new()
.max_rect(context.rect)
.layout(egui::Layout::left_to_right(egui::Align::Center)),
|ui| {
ui.spacing_mut().item_spacing.x = 4.0;
ui.style_mut().interaction.selectable_labels = false;
item_heading_no_breadcrumbs(ctx, viewport, ui, item);
},
);
}),
);
cursor_interact_with_selectable(ctx, response, item.clone());
}

/// Fully descriptive heading for an item, without any breadcrumbs.
fn item_heading_no_breadcrumbs(
ctx: &ViewerContext<'_>,
viewport: &ViewportBlueprint,
ui: &mut egui::Ui,
item: &Item,
) {
match item {
Item::AppId(_)
| Item::DataSource(_)
| Item::StoreId(_)
| Item::Container(_)
| Item::SpaceView(_) => {
let ItemTitle {
icon,
label,
label_style: _, // no label
tooltip: _,
} = ItemTitle::from_item(ctx, viewport, ui.style(), item);

icon_and_title(ui, icon, label);
}
Item::InstancePath(instance_path) => {
icon_and_title(
ui,
guess_instance_path_icon(ctx, instance_path),
instance_path.syntax_highlighted(ui.style()),
);
}
Item::ComponentPath(component_path) => {
let is_component_static = is_component_static(ctx, component_path);

// Break up into entity path and component name:
let ComponentPath {
entity_path,
component_name,
} = component_path;

item_heading_no_breadcrumbs(ctx, viewport, ui, &Item::from(entity_path.clone()));

separator_icon_ui(ui);

let component_icon = if is_component_static {
&icons::COMPONENT_STATIC
} else {
&icons::COMPONENT_TEMPORAL
};
icon_and_title(
ui,
component_icon,
component_name.syntax_highlighted(ui.style()),
);
}
Item::DataResult(view_id, instance_path) => {
// Break up into view and instance path:
item_heading_no_breadcrumbs(ctx, viewport, ui, &Item::SpaceView(*view_id));
separator_icon_ui(ui);
item_heading_no_breadcrumbs(
ctx,
viewport,
ui,
&Item::InstancePath(instance_path.clone()),
);
}
}
}

fn icon_and_title(ui: &mut egui::Ui, icon: &Icon, title: impl Into<WidgetText>) {
ui.add(icon.as_image());
ui.label(title);
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ use re_viewport_blueprint::ViewportBlueprint;

use crate::item_title::ItemTitle;

const ICON_SCALE: f32 = 0.5; // Because we save all icons as 2x

/// We show this above each item section
pub fn item_heading_with_breadcrumbs(
ctx: &ViewerContext<'_>,
Expand Down Expand Up @@ -207,8 +205,7 @@ fn last_part_of_item_heading(
};

let button = if with_icon {
egui::Button::image_and_text(icon.as_image().fit_to_original_size(ICON_SCALE), label)
.image_tint_follows_text_color(true)
egui::Button::image_and_text(icon.as_image(), label).image_tint_follows_text_color(true)
} else {
egui::Button::new(label)
};
Expand Down Expand Up @@ -240,22 +237,20 @@ fn viewport_breadcrumbs(
tooltip,
} = ItemTitle::from_contents(ctx, viewport, &contents);

let mut response = ui.add(
egui::Button::image(icon.as_image().fit_to_original_size(ICON_SCALE))
.image_tint_follows_text_color(true),
);
let mut response =
ui.add(egui::Button::image(icon.as_image()).image_tint_follows_text_color(true));
if let Some(tooltip) = tooltip {
response = response.on_hover_text(tooltip);
}
cursor_interact_with_selectable(ctx, response, item);

separator_icon_ui(ui, icons::BREADCRUMBS_SEPARATOR);
separator_icon_ui(ui);
}

fn separator_icon_ui(ui: &mut egui::Ui, icon: re_ui::Icon) {
pub fn separator_icon_ui(ui: &mut egui::Ui) {
ui.add(
icon.as_image()
.fit_to_original_size(ICON_SCALE)
icons::BREADCRUMBS_SEPARATOR
.as_image()
.tint(ui.visuals().text_color().gamma_multiply(0.65)),
);
}
Expand Down Expand Up @@ -297,8 +292,7 @@ fn entity_path_breadcrumbs(
// just to make it clear that this is a different kind of hierarchy.
&icons::RECORDING // streams hierarchy
};
egui::Button::image(icon.as_image().fit_to_original_size(ICON_SCALE))
.image_tint_follows_text_color(true)
egui::Button::image(icon.as_image()).image_tint_follows_text_color(true)
};

let response = ui.add(button);
Expand All @@ -313,5 +307,5 @@ fn entity_path_breadcrumbs(
};
cursor_interact_with_selectable(ctx, response, item);

separator_icon_ui(ui, icons::BREADCRUMBS_SEPARATOR);
separator_icon_ui(ui);
}
Loading

0 comments on commit d69fbc6

Please sign in to comment.