diff --git a/crates/re_ui/data/icons/add_big.png b/crates/re_ui/data/icons/add_big.png new file mode 100644 index 000000000000..9ce7ee42fb52 Binary files /dev/null and b/crates/re_ui/data/icons/add_big.png differ diff --git a/crates/re_ui/src/icons.rs b/crates/re_ui/src/icons.rs index 6485232e5c4f..006e5bf5ad8b 100644 --- a/crates/re_ui/src/icons.rs +++ b/crates/re_ui/src/icons.rs @@ -57,6 +57,8 @@ pub const VISIBLE: Icon = Icon::new("visible", include_bytes!("../data/icons/vis pub const INVISIBLE: Icon = Icon::new("invisible", include_bytes!("../data/icons/invisible.png")); pub const ADD: Icon = Icon::new("add", include_bytes!("../data/icons/add.png")); + +pub const ADD_BIG: Icon = Icon::new("add_big", include_bytes!("../data/icons/add_big.png")); pub const REMOVE: Icon = Icon::new("remove", include_bytes!("../data/icons/remove.png")); pub const RESET: Icon = Icon::new("reset", include_bytes!("../data/icons/reset.png")); diff --git a/crates/re_viewer/src/ui/add_space_view_or_container_modal.rs b/crates/re_viewer/src/ui/add_space_view_or_container_modal.rs new file mode 100644 index 000000000000..fe512106f39c --- /dev/null +++ b/crates/re_viewer/src/ui/add_space_view_or_container_modal.rs @@ -0,0 +1,165 @@ +//! Modal for adding a new space view of container to an existing target container. + +use itertools::Itertools; + +use re_log_types::{EntityPath, EntityPathFilter}; +use re_space_view::DataQueryBlueprint; +use re_viewer_context::ViewerContext; +use re_viewport::{icon_for_container_kind, SpaceViewBlueprint, Viewport}; + +#[derive(Default)] +pub struct AddSpaceViewOrContainerModal { + target_container: Option, + modal_handler: re_ui::modal::ModalHandler, +} + +impl AddSpaceViewOrContainerModal { + pub fn open(&mut self, target_container: egui_tiles::TileId) { + self.target_container = Some(target_container); + self.modal_handler.open(); + } + + pub fn ui(&mut self, ui: &mut egui::Ui, ctx: &ViewerContext<'_>, viewport: &Viewport<'_, '_>) { + self.modal_handler.ui( + ctx.re_ui, + ui, + || re_ui::modal::Modal::new("Add Space View or Container"), + |_, ui, _| modal_ui(ui, ctx, viewport, self.target_container), + ); + } +} + +fn modal_ui( + ui: &mut egui::Ui, + ctx: &ViewerContext<'_>, + viewport: &Viewport<'_, '_>, + target_container: Option, +) { + ui.spacing_mut().item_spacing = egui::vec2(14.0, 10.0); + + let container_data = [ + ( + "Tabs", + "Create a new tabbed container.", + egui_tiles::ContainerKind::Tabs, + ), + ( + "Horizontal", + "Create a new horizontal container.", + egui_tiles::ContainerKind::Horizontal, + ), + ( + "Vertical", + "Create a new vertical container.", + egui_tiles::ContainerKind::Vertical, + ), + ( + "Grid", + "Create a new grid container.", + egui_tiles::ContainerKind::Grid, + ), + ]; + + for (title, subtitle, kind) in container_data { + if row_ui(ui, icon_for_container_kind(&kind), title, subtitle).clicked() { + viewport.blueprint.add_container(kind, target_container); + viewport.blueprint.mark_user_interaction(ctx); + } + } + + ui.separator(); + + // space view of any kind + for space_view in ctx + .space_view_class_registry + .iter_registry() + .sorted_by_key(|entry| entry.class.display_name()) + .map(|entry| { + SpaceViewBlueprint::new( + entry.class.identifier(), + &format!("empty {}", entry.class.display_name()), + &EntityPath::root(), + DataQueryBlueprint::new(entry.class.identifier(), EntityPathFilter::default()), + ) + }) + { + let icon = space_view.class(ctx.space_view_class_registry).icon(); + let title = space_view + .class(ctx.space_view_class_registry) + .display_name(); + let subtitle = format!("Create a new Space View to display {title} content."); + + if row_ui(ui, icon, title, &subtitle).clicked() { + viewport + .blueprint + .add_space_views(std::iter::once(space_view), ctx, target_container); + viewport.blueprint.mark_user_interaction(ctx); + } + } +} + +fn row_ui(ui: &mut egui::Ui, icon: &re_ui::Icon, title: &str, subtitle: &str) -> egui::Response { + let top_left_corner = ui.cursor().min; + + ui.horizontal(|ui| { + //TODO(ab): move this to re_ui + //TODO(ab): use design token + let row_height = 42.0; + let icon_size = egui::vec2(18.0, 18.0); + let thumbnail_rounding = 6.0; + + let thumbnail_content = |ui: &mut egui::Ui| { + let (rect, _) = ui.allocate_exact_size(icon_size, egui::Sense::hover()); + icon.as_image() + .tint(ui.visuals().widgets.active.fg_stroke.color) + .paint_at(ui, rect); + }; + + egui::Frame { + inner_margin: egui::Margin::symmetric( + (62. - icon_size.x) / 2.0, + (row_height - icon_size.y) / 2.0, + ), // should be 62x42 when combined with icon size + rounding: egui::Rounding::same(thumbnail_rounding), + fill: egui::Color32::from_gray(50), + ..Default::default() + } + .show(ui, thumbnail_content); + + ui.vertical(|ui| { + ui.strong(title); + ui.add_space(-5.0); + + ui.add(egui::Label::new(subtitle).wrap(false)); + }); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let right_coord = ui.cursor().max.x; + + // interact with the entire row + let interact_rect = egui::Rect::from_min_max( + top_left_corner, + egui::pos2(right_coord, top_left_corner.y + row_height), + ); + + let response = + ui.interact(interact_rect, title.to_owned().into(), egui::Sense::click()); + let tint = if response.hovered() { + ui.visuals().widgets.active.fg_stroke.color + } else { + ui.visuals().widgets.inactive.fg_stroke.color + }; + + ui.add( + re_ui::icons::ADD_BIG + .as_image() + .fit_to_exact_size(egui::vec2(24.0, 24.0)) + .tint(tint), + ); + + response + }) + .inner + }) + .inner +} diff --git a/crates/re_viewer/src/ui/mod.rs b/crates/re_viewer/src/ui/mod.rs index 53eeb8b238fd..e61204db2c3b 100644 --- a/crates/re_viewer/src/ui/mod.rs +++ b/crates/re_viewer/src/ui/mod.rs @@ -6,9 +6,9 @@ mod selection_history_ui; mod top_panel; mod welcome_screen; +pub(crate) mod add_space_view_or_container_modal; pub(crate) mod memory_panel; pub(crate) mod selection_panel; - pub(crate) mod visible_history; pub use blueprint_panel::blueprint_panel_ui; diff --git a/crates/re_viewer/src/ui/rerun_menu.rs b/crates/re_viewer/src/ui/rerun_menu.rs index e489965c6ebe..08b3817c9d1b 100644 --- a/crates/re_viewer/src/ui/rerun_menu.rs +++ b/crates/re_viewer/src/ui/rerun_menu.rs @@ -318,6 +318,19 @@ fn experimental_feature_ui( ) .on_hover_text("Show an entity filter DSL when selecting a space-view."); + re_ui + .checkbox( + ui, + &mut app_options.experimental_additive_workflow, + "Enable the container addition workflow", + ) + .on_hover_text( + "This flag enables the experimental container addition workflow, including:\n\ + - Remove the automatic simplification of the container tree.\n\ + - Add a 'Content' list in the selection panel when a container is selected.\n\ + - Add the 'Add space view/container' modal, accessible from the selection panel.", + ); + re_ui .checkbox( ui, diff --git a/crates/re_viewer/src/ui/selection_panel.rs b/crates/re_viewer/src/ui/selection_panel.rs index 9120991d2234..8641aec9cee5 100644 --- a/crates/re_viewer/src/ui/selection_panel.rs +++ b/crates/re_viewer/src/ui/selection_panel.rs @@ -14,13 +14,16 @@ use re_types::{ use re_ui::list_item::ListItem; use re_ui::ReUi; use re_viewer_context::{ - gpu_bridge::colormap_dropdown_button_ui, Item, SpaceViewClass, SpaceViewClassIdentifier, - SpaceViewId, SystemCommand, SystemCommandSender as _, UiVerbosity, ViewerContext, + gpu_bridge::colormap_dropdown_button_ui, HoverHighlight, Item, SpaceViewClass, + SpaceViewClassIdentifier, SpaceViewId, SystemCommand, SystemCommandSender as _, UiVerbosity, + ViewerContext, }; use re_viewport::{ - external::re_space_view::blueprint::components::QueryExpressions, Viewport, ViewportBlueprint, + external::re_space_view::blueprint::components::QueryExpressions, icon_for_container_kind, + Viewport, ViewportBlueprint, }; +use crate::ui::add_space_view_or_container_modal::AddSpaceViewOrContainerModal; use crate::ui::visible_history::visible_history_ui; use super::selection_history_ui::SelectionHistoryUi; @@ -32,6 +35,9 @@ use super::selection_history_ui::SelectionHistoryUi; #[serde(default)] pub(crate) struct SelectionPanel { selection_state_ui: SelectionHistoryUi, + + #[serde(skip)] + add_space_view_or_container_modal: AddSpaceViewOrContainerModal, } impl SelectionPanel { @@ -126,6 +132,13 @@ impl SelectionPanel { match item { Item::Container(tile_id) => { container_top_level_properties(ui, ctx, viewport, tile_id); + + // the container children and related additive workflow is only available with the new container + // blueprints feature + if ctx.app_options.experimental_additive_workflow { + ui.add_space(12.0); + self.container_children(ui, ctx, viewport, tile_id); + } } Item::SpaceView(space_view_id) => { @@ -155,6 +168,69 @@ impl SelectionPanel { }); } } + + fn container_children( + &mut self, + ui: &mut egui::Ui, + ctx: &ViewerContext<'_>, + viewport: &Viewport<'_, '_>, + tile_id: &egui_tiles::TileId, + ) { + let Some(Tile::Container(container)) = viewport.tree.tiles.get(*tile_id) else { + return; + }; + + ui.horizontal(|ui| { + ui.strong("Contents"); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ctx + .re_ui + .small_icon_button(ui, &re_ui::icons::ADD) + .clicked() + { + self.add_space_view_or_container_modal.open(*tile_id); + } + }); + }); + + self.add_space_view_or_container_modal.ui(ui, ctx, viewport); + + let show_content = |ui: &mut egui::Ui| { + let mut has_child = false; + for &child_tile_id in container.children() { + has_child |= show_list_item_for_container_child(ui, ctx, viewport, child_tile_id); + } + + if !has_child { + ListItem::new(ctx.re_ui, "empty — use the + button to add content") + .weak(true) + .italics(true) + .active(false) + .show(ui); + } + }; + + egui::Frame { + outer_margin: egui::Margin::ZERO, + inner_margin: egui::Margin::ZERO, + stroke: ui.visuals().widgets.noninteractive.bg_stroke, + ..Default::default() + } + .show(ui, |ui| { + let clip_rect = ui.clip_rect(); + ui.set_clip_rect(ui.max_rect()); + ui.spacing_mut().item_spacing.y = 0.0; + + egui::Frame { + inner_margin: egui::Margin::symmetric(4.0, 0.0), + ..Default::default() + } + .show(ui, show_content); + + ui.set_clip_rect(clip_rect); + }); + } } fn has_data_section(item: &Item) -> bool { @@ -405,7 +481,7 @@ fn space_view_top_level_properties( fn container_top_level_properties( ui: &mut egui::Ui, - _ctx: &ViewerContext<'_>, + ctx: &ViewerContext<'_>, viewport: &mut Viewport<'_, '_>, tile_id: &egui_tiles::TileId, ) { @@ -485,10 +561,90 @@ fn container_top_level_properties( ui.end_row(); } + + // this feature is only available with the new container blueprints feature + #[allow(clippy::collapsible_if)] + if ctx.app_options.experimental_additive_workflow { + if ui + .button("Simplify hierarchy") + .on_hover_text("Simplify this container and its children") + .clicked() + { + viewport.blueprint.simplify_container( + *tile_id, + egui_tiles::SimplificationOptions { + prune_empty_tabs: true, + prune_empty_containers: true, + prune_single_child_tabs: false, + prune_single_child_containers: false, + all_panes_must_have_tabs: true, + join_nested_linear_containers: true, + }, + ); + } + } }); } } +// TODO(#4560): this code should be generic and part of re_data_ui +/// Show a list item for a single container child. +/// +/// Return true if successful. +fn show_list_item_for_container_child( + ui: &mut egui::Ui, + ctx: &ViewerContext<'_>, + viewport: &Viewport<'_, '_>, + child_tile_id: egui_tiles::TileId, +) -> bool { + let Some(child_tile) = viewport.tree.tiles.get(child_tile_id) else { + re_log::warn_once!("Could not find child tile with ID {child_tile_id:?}",); + return false; + }; + + let (item, mut list_item) = match child_tile { + Tile::Pane(space_view_id) => { + let Some(space_view) = viewport.blueprint.space_views.get(space_view_id) else { + re_log::warn_once!("Could not find space view with ID {space_view_id:?}",); + return false; + }; + + ( + Item::SpaceView(*space_view_id), + ListItem::new(ctx.re_ui, space_view.display_name.clone()) + .with_icon(space_view.class(ctx.space_view_class_registry).icon()), + ) + } + Tile::Container(container) => { + // TODO(#4285): this hack should be cleaned with "blueprintified" containers + if let (egui_tiles::Container::Tabs(_), Some(child_id)) = + (container, container.only_child()) + { + return show_list_item_for_container_child(ui, ctx, viewport, child_id); + } + + ( + Item::Container(child_tile_id), + ListItem::new(ctx.re_ui, format!("{:?}", container.kind())) + .with_icon(icon_for_container_kind(&container.kind())), + ) + } + }; + + let is_item_hovered = + ctx.selection_state().highlight_for_ui_element(&item) == HoverHighlight::Hovered; + + if is_item_hovered { + list_item = list_item.force_hovered(true); + } + + let response = list_item.show(ui); + + item_ui::select_hovered_on_click(ctx, &response, std::iter::once(item)); + + true +} + fn has_blueprint_section(item: &Item) -> bool { match item { Item::ComponentPath(_) | Item::Container(_) => false, diff --git a/crates/re_viewer_context/src/app_options.rs b/crates/re_viewer_context/src/app_options.rs index b95fe4ce36c2..24f13c3bffca 100644 --- a/crates/re_viewer_context/src/app_options.rs +++ b/crates/re_viewer_context/src/app_options.rs @@ -22,6 +22,9 @@ pub struct AppOptions { pub experimental_entity_filter_editor: bool, + /// Enable the experimental support for the container addition workflow. + pub experimental_additive_workflow: bool, + /// Displays an overlay for debugging picking. pub show_picking_debug_overlay: bool, @@ -49,6 +52,8 @@ impl Default for AppOptions { experimental_entity_filter_editor: false, + experimental_additive_workflow: cfg!(debug_assertions), + show_picking_debug_overlay: false, show_blueprint_in_timeline: false, diff --git a/crates/re_viewport/src/viewport.rs b/crates/re_viewport/src/viewport.rs index edf888a64e76..09bdd7ecb2c0 100644 --- a/crates/re_viewport/src/viewport.rs +++ b/crates/re_viewport/src/viewport.rs @@ -13,8 +13,8 @@ use re_data_ui::item_ui; use re_ui::{Icon, ReUi}; use re_viewer_context::{ - Item, SpaceViewClassIdentifier, SpaceViewClassRegistry, SpaceViewId, SpaceViewState, - SystemExecutionOutput, ViewQuery, ViewerContext, + AppOptions, Item, SpaceViewClassIdentifier, SpaceViewClassRegistry, SpaceViewId, + SpaceViewState, SystemExecutionOutput, ViewQuery, ViewerContext, }; use crate::{ @@ -95,6 +95,27 @@ pub enum TreeAction { SimplifyContainer(egui_tiles::TileId, egui_tiles::SimplificationOptions), } +fn tree_simplification_option_for_app_options( + app_options: &AppOptions, +) -> egui_tiles::SimplificationOptions { + if app_options.experimental_additive_workflow { + // If the user is using the additive workflow, we don't want to aggressively simplify the tree. + egui_tiles::SimplificationOptions { + prune_empty_tabs: false, + all_panes_must_have_tabs: true, + prune_empty_containers: false, + prune_single_child_tabs: false, + prune_single_child_containers: false, + join_nested_linear_containers: true, + } + } else { + egui_tiles::SimplificationOptions { + all_panes_must_have_tabs: true, + ..Default::default() + } + } +} + // ---------------------------------------------------------------------------- /// Defines the layout of the Viewport @@ -415,16 +436,10 @@ impl<'a, 'b> Viewport<'a, 'b> { if ctx.app_options.legacy_container_blueprint { self.blueprint.set_tree(&self.tree, ctx); } else if self.edited { - // TODO(abey79): Decide what simplification to do here. Some of this - // might need to get rolled into the save logic instead. - // Simplify before we save the tree. Normally additional simplification will // happen on the next render loop, but that's too late -- unsimplified // changes will be baked into the tree. - let options = egui_tiles::SimplificationOptions { - all_panes_must_have_tabs: true, - ..Default::default() - }; + let options = tree_simplification_option_for_app_options(ctx.app_options); self.tree.simplify(&options); self.blueprint.save_tree_as_containers(&self.tree, ctx); @@ -682,11 +697,10 @@ impl<'a, 'b> egui_tiles::Behavior for TabViewer<'a, 'b> { } /// What are the rules for simplifying the tree? + /// + /// These options are applied on every frame by `egui_tiles`. fn simplification_options(&self) -> egui_tiles::SimplificationOptions { - egui_tiles::SimplificationOptions { - all_panes_must_have_tabs: true, - ..Default::default() - } + tree_simplification_option_for_app_options(self.ctx.app_options) } // Callbacks: diff --git a/crates/re_viewport/src/viewport_blueprint_ui.rs b/crates/re_viewport/src/viewport_blueprint_ui.rs index 38fc10fa4c81..129790c2d1e0 100644 --- a/crates/re_viewport/src/viewport_blueprint_ui.rs +++ b/crates/re_viewport/src/viewport_blueprint_ui.rs @@ -65,7 +65,10 @@ impl Viewport<'_, '_> { tile_id: egui_tiles::TileId, container: &egui_tiles::Container, ) { - if let Some(child_id) = container.only_child() { + // TODO(#4285): this will disappear once we walk the container blueprint tree instead of `egui_tiles::Tree` + if let (egui_tiles::Container::Tabs(_), Some(child_id)) = + (container, container.only_child()) + { // Maybe a tab container with only one child - collapse it in the tree view to make it more easily understood. // This means we won't be showing the visibility button of the parent container, // so if the child is made invisible, we should do the same for the parent.