diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 6be030512871..8c364bda6cf3 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -88,11 +88,11 @@ Of course, this will only take us so far. In the future we plan on caching queri Here is an overview of the crates included in the project: - - - - - + + + + + @@ -131,6 +131,7 @@ Update instructions: | re_viewer | The Rerun Viewer | | re_viewport | The central viewport panel of the Rerun viewer. | | re_time_panel | The time panel of the Rerun Viewer, allowing to control the displayed timeline & time. | +| re_selection_panel | The UI for the selection panel. | | re_space_view | Types & utilities for defining Space View classes and communicating with the Viewport. | | re_space_view_bar_chart | A Space View that shows a single bar chart. | | re_space_view_dataframe | A Space View that shows the data contained in entities in a table. | diff --git a/Cargo.lock b/Cargo.lock index 8328078fed7e..f009ea39d439 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4668,6 +4668,34 @@ dependencies = [ "thiserror", ] +[[package]] +name = "re_selection_panel" +version = "0.17.0-alpha.2" +dependencies = [ + "egui", + "egui_tiles", + "itertools 0.12.0", + "nohash-hasher", + "once_cell", + "re_context_menu", + "re_data_store", + "re_data_ui", + "re_entity_db", + "re_log", + "re_log_types", + "re_renderer", + "re_space_view_spatial", + "re_space_view_time_series", + "re_tracing", + "re_types", + "re_types_core", + "re_ui", + "re_viewer_context", + "re_viewport_blueprint", + "serde", + "static_assertions", +] + [[package]] name = "re_smart_channel" version = "0.17.0-alpha.2" @@ -5036,18 +5064,15 @@ dependencies = [ "egui", "egui-wgpu", "egui_plot", - "egui_tiles", "ehttp", "image", "itertools 0.12.0", "js-sys", - "once_cell", "poll-promise", "re_analytics", "re_blueprint_tree", "re_build_info", "re_build_tools", - "re_context_menu", "re_data_loader", "re_data_source", "re_data_store", @@ -5062,6 +5087,7 @@ dependencies = [ "re_query", "re_renderer", "re_sdk_comms", + "re_selection_panel", "re_smart_channel", "re_space_view_bar_chart", "re_space_view_dataframe", @@ -5084,7 +5110,6 @@ dependencies = [ "ron", "serde", "serde_json", - "static_assertions", "thiserror", "time", "wasm-bindgen", @@ -5146,10 +5171,8 @@ dependencies = [ "image", "itertools 0.12.0", "nohash-hasher", - "once_cell", "rayon", "re_context_menu", - "re_data_ui", "re_entity_db", "re_log", "re_log_types", diff --git a/Cargo.toml b/Cargo.toml index 7c1cbdd8641d..400ec03eac0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ re_log_types = { path = "crates/re_log_types", version = "=0.17.0-alpha.2", defa re_memory = { path = "crates/re_memory", version = "=0.17.0-alpha.2", default-features = false } re_query = { path = "crates/re_query", version = "=0.17.0-alpha.2", default-features = false } re_renderer = { path = "crates/re_renderer", version = "=0.17.0-alpha.2", default-features = false } +re_selection_panel = { path = "crates/re_selection_panel", version = "=0.17.0-alpha.2", default-features = false } re_sdk = { path = "crates/re_sdk", version = "=0.17.0-alpha.2", default-features = false } re_sdk_comms = { path = "crates/re_sdk_comms", version = "=0.17.0-alpha.2", default-features = false } re_smart_channel = { path = "crates/re_smart_channel", version = "=0.17.0-alpha.2", default-features = false } diff --git a/crates/re_selection_panel/Cargo.toml b/crates/re_selection_panel/Cargo.toml new file mode 100644 index 000000000000..203a1717fa06 --- /dev/null +++ b/crates/re_selection_panel/Cargo.toml @@ -0,0 +1,44 @@ +[package] +authors.workspace = true +description = "The UI for the selection panel." +edition.workspace = true +homepage.workspace = true +license.workspace = true +name = "re_selection_panel" +publish = true +readme = "README.md" +repository.workspace = true +rust-version.workspace = true +version.workspace = true +include = ["../../LICENSE-APACHE", "../../LICENSE-MIT", "**/*.rs", "Cargo.toml"] + +[lints] +workspace = true + +[package.metadata.docs.rs] +all-features = true + +[dependencies] +re_context_menu.workspace = true +re_data_store.workspace = true +re_data_ui.workspace = true +re_entity_db = { workspace = true, features = ["serde"] } +re_log.workspace = true +re_log_types.workspace = true +re_renderer.workspace = true +re_space_view_time_series.workspace = true +re_space_view_spatial.workspace = true +re_tracing.workspace = true +re_types_core.workspace = true +re_types.workspace = true +re_ui.workspace = true +re_viewer_context.workspace = true +re_viewport_blueprint.workspace = true + +egui_tiles.workspace = true +egui.workspace = true +itertools.workspace = true +once_cell.workspace = true +nohash-hasher.workspace = true +serde = { workspace = true, features = ["derive"] } +static_assertions.workspace = true diff --git a/crates/re_selection_panel/README.md b/crates/re_selection_panel/README.md new file mode 100644 index 000000000000..8ea22245eeea --- /dev/null +++ b/crates/re_selection_panel/README.md @@ -0,0 +1,10 @@ +# re_selection_panel + +Part of the [`rerun`](https://github.com/rerun-io/rerun) family of crates. + +[![Latest version](https://img.shields.io/crates/v/re_selection_panel.svg)](https://crates.io/crates/re_selection_panel?speculative-link) +[![Documentation](https://docs.rs/re_selection_panel/badge.svg)](https://docs.rs/re_selection_panel?speculative-link) +![MIT](https://img.shields.io/badge/license-MIT-blue.svg) +![Apache](https://img.shields.io/badge/license-Apache-blue.svg) + +The UI for the selection panel. diff --git a/crates/re_selection_panel/src/lib.rs b/crates/re_selection_panel/src/lib.rs new file mode 100644 index 000000000000..9e38c867b3cd --- /dev/null +++ b/crates/re_selection_panel/src/lib.rs @@ -0,0 +1,8 @@ +mod override_ui; +mod query_range_ui; +mod selection_history_ui; +mod selection_panel; +mod space_view_entity_picker; +mod space_view_space_origin_ui; + +pub use selection_panel::SelectionPanel; diff --git a/crates/re_viewer/src/ui/override_ui.rs b/crates/re_selection_panel/src/override_ui.rs similarity index 100% rename from crates/re_viewer/src/ui/override_ui.rs rename to crates/re_selection_panel/src/override_ui.rs diff --git a/crates/re_viewer/src/ui/query_range_ui.rs b/crates/re_selection_panel/src/query_range_ui.rs similarity index 100% rename from crates/re_viewer/src/ui/query_range_ui.rs rename to crates/re_selection_panel/src/query_range_ui.rs diff --git a/crates/re_viewer/src/ui/selection_history_ui.rs b/crates/re_selection_panel/src/selection_history_ui.rs similarity index 98% rename from crates/re_viewer/src/ui/selection_history_ui.rs rename to crates/re_selection_panel/src/selection_history_ui.rs index f4264f3505d4..14c35eae0c99 100644 --- a/crates/re_viewer/src/ui/selection_history_ui.rs +++ b/crates/re_selection_panel/src/selection_history_ui.rs @@ -1,4 +1,5 @@ use egui::RichText; + use re_ui::UICommand; use re_viewer_context::{Item, ItemCollection, SelectionHistory}; use re_viewport_blueprint::ViewportBlueprint; @@ -143,7 +144,9 @@ impl SelectionHistoryUi { } } if sel.iter_items().count() == 1 { - item_kind_ui(ui, sel.iter_items().next().unwrap()); + if let Some(item) = sel.iter_items().next() { + item_kind_ui(ui, item); + } } }); } diff --git a/crates/re_viewer/src/ui/selection_panel.rs b/crates/re_selection_panel/src/selection_panel.rs similarity index 79% rename from crates/re_viewer/src/ui/selection_panel.rs rename to crates/re_selection_panel/src/selection_panel.rs index 5327477da7a5..602991000a6b 100644 --- a/crates/re_viewer/src/ui/selection_panel.rs +++ b/crates/re_selection_panel/src/selection_panel.rs @@ -20,34 +20,40 @@ use re_ui::{icons, list_item, ReUi, SyntaxHighlighting as _}; use re_viewer_context::{ contents_name_style, gpu_bridge::colormap_dropdown_button_ui, icon_for_container_kind, ContainerId, Contents, DataQueryResult, HoverHighlight, Item, SpaceViewClass, SpaceViewId, - UiLayout, ViewerContext, + UiLayout, ViewStates, ViewerContext, }; -use re_viewport::Viewport; use re_viewport_blueprint::{ui::show_add_space_view_or_container_modal, ViewportBlueprint}; -use crate::ui::override_ui::override_visualizer_ui; -use crate::{app_state::default_selection_panel_width, ui::override_ui::override_ui}; - -use super::{ +use crate::override_ui::{override_ui, override_visualizer_ui}; +use crate::space_view_entity_picker::SpaceViewEntityPicker; +use crate::{ query_range_ui::query_range_ui_data_result, query_range_ui::query_range_ui_space_view, selection_history_ui::SelectionHistoryUi, }; // --- +fn default_selection_panel_width(screen_width: f32) -> f32 { + (0.45 * screen_width).min(300.0).round() +} /// The "Selection view" sidebar. #[derive(Default, serde::Deserialize, serde::Serialize)] #[serde(default)] -pub(crate) struct SelectionPanel { +pub struct SelectionPanel { selection_state_ui: SelectionHistoryUi, + + #[serde(skip)] + /// State for the "Add entity" modal. + space_view_entity_modal: SpaceViewEntityPicker, } impl SelectionPanel { pub fn show_panel( &mut self, ctx: &ViewerContext<'_>, + blueprint: &ViewportBlueprint, + view_states: &mut ViewStates, ui: &mut egui::Ui, - viewport: &mut Viewport<'_, '_>, expanded: bool, ) { let screen_width = ui.ctx().screen_rect().width(); @@ -76,7 +82,7 @@ impl SelectionPanel { if let Some(selection) = self.selection_state_ui.selection_ui( ctx.re_ui, ui, - viewport.blueprint, + blueprint, &mut history, ) { ctx.selection_state().set_selection(selection); @@ -93,19 +99,23 @@ impl SelectionPanel { .show(ui, |ui| { ui.add_space(ui.spacing().item_spacing.y); ctx.re_ui.panel_content(ui, |_, ui| { - self.contents(ctx, ui, viewport); + self.contents(ctx, blueprint, view_states, ui); }); }); }); }); + + // run modals (these are noop if the modals are not active) + self.space_view_entity_modal.ui(ui.ctx(), ctx, blueprint); } #[allow(clippy::unused_self)] fn contents( &mut self, ctx: &ViewerContext<'_>, + blueprint: &ViewportBlueprint, + view_states: &mut ViewStates, ui: &mut egui::Ui, - viewport: &mut Viewport<'_, '_>, ) { re_tracing::profile_function!(); @@ -124,17 +134,17 @@ impl SelectionPanel { }; for (i, item) in selection.iter_items().enumerate() { ui.push_id(item, |ui| { - what_is_selected_ui(ui, ctx, viewport.blueprint, item); + what_is_selected_ui(ctx, blueprint, ui, item); match item { Item::Container(container_id) => { - container_top_level_properties(ui, ctx, viewport, container_id); + container_top_level_properties(ctx, blueprint, ui, container_id); ui.add_space(12.0); - container_children(ui, ctx, viewport, container_id); + container_children(ctx, blueprint, ui, container_id); } Item::SpaceView(space_view_id) => { - space_view_top_level_properties(ui, ctx, viewport.blueprint, space_view_id); + space_view_top_level_properties(ctx, blueprint, ui, space_view_id); } _ => {} @@ -153,7 +163,7 @@ impl SelectionPanel { // Special override section for space-view-entities if let Item::DataResult(space_view_id, instance_path) = item { - if let Some(space_view) = viewport.blueprint.space_views.get(space_view_id) { + if let Some(space_view) = blueprint.space_views.get(space_view_id) { // TODO(jleibs): Overrides still require special handling inside the visualizers. // For now, only show the override section for TimeSeries until support is implemented // generically. @@ -179,7 +189,7 @@ impl SelectionPanel { if has_blueprint_section(item) { ctx.re_ui .large_collapsing_header(ui, "Blueprint", true, |ui| { - blueprint_ui(ui, ctx, viewport, item); + self.blueprint_ui(ctx, blueprint, view_states, ui, item); }); } @@ -190,15 +200,287 @@ impl SelectionPanel { }); } } + + /// What is the blueprint stuff for this item? + fn blueprint_ui( + &mut self, + ctx: &ViewerContext<'_>, + blueprint: &ViewportBlueprint, + view_states: &mut ViewStates, + ui: &mut egui::Ui, + item: &Item, + ) { + match item { + Item::AppId(_) + | Item::DataSource(_) + | Item::StoreId(_) + | Item::ComponentPath(_) + | Item::Container(_) + | Item::InstancePath(_) => {} + + Item::SpaceView(space_view_id) => { + self.blueprint_ui_for_space_view(ctx, blueprint, view_states, ui, *space_view_id); + } + + Item::DataResult(space_view_id, instance_path) => { + blueprint_ui_for_data_result(ctx, blueprint, ui, *space_view_id, instance_path); + } + } + } + + fn blueprint_ui_for_space_view( + &mut self, + ctx: &ViewerContext<'_>, + blueprint: &ViewportBlueprint, + view_states: &mut ViewStates, + ui: &mut Ui, + space_view_id: SpaceViewId, + ) { + if let Some(space_view) = blueprint.space_view(&space_view_id) { + if let Some(new_entity_path_filter) = self.entity_path_filter_ui( + ctx, + ui, + space_view_id, + &space_view.contents.entity_path_filter, + &space_view.space_origin, + ) { + space_view + .contents + .set_entity_path_filter(ctx, &new_entity_path_filter); + } + + ui.add_space(ui.spacing().item_spacing.y); + } + + if ui + .button("Clone space view") + .on_hover_text( + "Create an exact duplicate of this space view including all blueprint settings", + ) + .clicked() + { + if let Some(new_space_view_id) = blueprint.duplicate_space_view(&space_view_id, ctx) { + ctx.selection_state() + .set_selection(Item::SpaceView(new_space_view_id)); + blueprint.mark_user_interaction(ctx); + } + } + + ui.add_space(ui.spacing().item_spacing.y / 2.0); + ReUi::full_span_separator(ui); + ui.add_space(ui.spacing().item_spacing.y / 2.0); + + if let Some(space_view) = blueprint.space_view(&space_view_id) { + let class_identifier = *space_view.class_identifier(); + + let space_view_state = view_states.view_state_mut( + ctx.space_view_class_registry, + space_view.id, + &class_identifier, + ); + + query_range_ui_space_view(ctx, ui, space_view); + + // Space View don't inherit (legacy) properties. + let mut props = + space_view.legacy_properties(ctx.store_context.blueprint, ctx.blueprint_query); + let props_before = props.clone(); + + let space_view_class = space_view.class(ctx.space_view_class_registry); + if let Err(err) = space_view_class.selection_ui( + ctx, + ui, + space_view_state.view_state.as_mut(), + &space_view.space_origin, + space_view.id, + &mut props, + ) { + re_log::error!( + "Error in space view selection UI (class: {}, display name: {}): {err}", + space_view.class_identifier(), + space_view_class.display_name(), + ); + } + + if props_before != props { + space_view.save_legacy_properties(ctx, props); + } + } + } + + /// Returns a new filter when the editing is done, and there has been a change. + fn entity_path_filter_ui( + &mut self, + ctx: &ViewerContext<'_>, + ui: &mut egui::Ui, + space_view_id: SpaceViewId, + filter: &EntityPathFilter, + origin: &EntityPath, + ) -> Option { + fn entity_path_filter_help_ui(ui: &mut egui::Ui) { + let markdown = r#" +# Entity path query syntax + +Entity path queries are described as a list of include/exclude rules that act on paths: + +```diff ++ /world/** # add everything… +- /world/roads/** # …but remove all roads… ++ /world/roads/main # …but show main road +``` + +If there are multiple matching rules, the most specific rule wins. +If there are multiple rules of the same specificity, the last one wins. +If no rules match, the path is excluded. + +The `/**` suffix matches the whole subtree, i.e. self and any child, recursively +(`/world/**` matches both `/world` and `/world/car/driver`). +Other uses of `*` are not (yet) supported. + +`EntityPathFilter` sorts the rule by entity path, with recursive coming before non-recursive. +This means the last matching rule is also the most specific one. +For instance: + +```diff ++ /world/** +- /world +- /world/car/** ++ /world/car/driver +``` + +The last rule matching `/world/car/driver` is `+ /world/car/driver`, so it is included. +The last rule matching `/world/car/hood` is `- /world/car/**`, so it is excluded. +The last rule matching `/world` is `- /world`, so it is excluded. +The last rule matching `/world/house` is `+ /world/**`, so it is included. + "# + .trim(); + + re_ui::markdown_ui(ui, egui::Id::new("entity_path_filter_help_ui"), markdown); + } + + fn syntax_highlight_entity_path_filter( + style: &egui::Style, + mut string: &str, + ) -> egui::text::LayoutJob { + let font_id = egui::TextStyle::Body.resolve(style); + + let mut job = egui::text::LayoutJob::default(); + + while !string.is_empty() { + let newline = string.find('\n').unwrap_or(string.len() - 1); + let line = &string[..=newline]; + string = &string[newline + 1..]; + let is_exclusion = line.trim_start().starts_with('-'); + + let color = if is_exclusion { + egui::Color32::LIGHT_RED + } else { + egui::Color32::LIGHT_GREEN + }; + + let text_format = egui::TextFormat { + font_id: font_id.clone(), + color, + ..Default::default() + }; + + job.append(line, 0.0, text_format); + } + + job + } + + fn text_layouter( + ui: &egui::Ui, + string: &str, + wrap_width: f32, + ) -> std::sync::Arc { + let mut layout_job = syntax_highlight_entity_path_filter(ui.style(), string); + layout_job.wrap.max_width = wrap_width; + ui.fonts(|f| f.layout_job(layout_job)) + } + + // We store the string we are temporarily editing in the `Ui`'s temporary data storage. + // This is so it can contain invalid rules while the user edits it, and it's only normalized + // when they press enter, or stops editing. + let filter_text_id = ui.id().with("filter_text"); + + let mut filter_string = ui.data_mut(|data| { + data.get_temp_mut_or_insert_with::(filter_text_id, || filter.formatted()) + .clone() + }); + + let rightmost_x = ui.cursor().min.x; + ui.horizontal(|ui| { + ui.label("Entity path query").on_hover_text( + "The entity path query consists of a list of include/exclude rules \ + that determines what entities are part of this space view", + ); + + let current_x = ui.cursor().min.x; + // Compute a width that results in these things to be right-aligned with the following text edit. + let desired_width = (ui.available_width() - ui.spacing().item_spacing.x) + .at_most(ui.spacing().text_edit_width - (current_x - rightmost_x)); + + ui.allocate_ui_with_layout( + egui::vec2(desired_width, ui.available_height()), + egui::Layout::right_to_left(egui::Align::Center), + |ui| { + re_ui::help_hover_button(ui).on_hover_ui(entity_path_filter_help_ui); + if ui + .button("Edit") + .on_hover_text("Modify the entity query using the editor") + .clicked() + { + self.space_view_entity_modal.open(space_view_id); + } + }, + ); + }); + + let response = + ui.add(egui::TextEdit::multiline(&mut filter_string).layouter(&mut text_layouter)); + + if response.has_focus() { + ui.data_mut(|data| data.insert_temp::(filter_text_id, filter_string.clone())); + } else { + // Reconstruct it from the filter next frame + ui.data_mut(|data| data.remove::(filter_text_id)); + } + + // Show some statistics about the query, print a warning text if something seems off. + let query = ctx.lookup_query_result(space_view_id); + if query.num_matching_entities == 0 { + ui.label(ctx.re_ui.warning_text("Does not match any entity")); + } else if query.num_matching_entities == 1 { + ui.label("Matches 1 entity"); + } else { + ui.label(format!("Matches {} entities", query.num_matching_entities)); + } + if query.num_matching_entities != 0 && query.num_visualized_entities == 0 { + // TODO(andreas): Talk about this root bit only if it's a spatial view. + ui.label(ctx.re_ui.warning_text( + format!("This space view is not able to visualize any of the matched entities using the current root \"{origin:?}\"."), + )); + } + + // Apply the edit. + let new_filter = EntityPathFilter::parse_forgiving(&filter_string, &Default::default()); + if &new_filter == filter { + None // no change + } else { + Some(new_filter) + } + } } fn container_children( - ui: &mut egui::Ui, ctx: &ViewerContext<'_>, - viewport: &Viewport<'_, '_>, + blueprint: &ViewportBlueprint, + ui: &mut egui::Ui, container_id: &ContainerId, ) { - let Some(container) = viewport.blueprint.container(container_id) else { + let Some(container) = blueprint.container(container_id) else { return; }; @@ -215,7 +497,7 @@ fn container_children( let show_content = |ui: &mut egui::Ui| { let mut has_child = false; for child_contents in &container.contents { - has_child |= show_list_item_for_container_child(ui, ctx, viewport, child_contents); + has_child |= show_list_item_for_container_child(ctx, blueprint, ui, child_contents); } if !has_child { @@ -291,9 +573,9 @@ fn space_view_button( /// /// This includes a title bar and contextual information about there this item is located. fn what_is_selected_ui( - ui: &mut egui::Ui, ctx: &ViewerContext<'_>, - viewport: &ViewportBlueprint, + blueprint: &ViewportBlueprint, + ui: &mut egui::Ui, item: &Item, ) { match item { @@ -347,7 +629,7 @@ fn what_is_selected_ui( } Item::Container(container_id) => { - if let Some(container_blueprint) = viewport.container(container_id) { + if let Some(container_blueprint) = blueprint.container(container_id) { let hover_text = if let Some(display_name) = container_blueprint.display_name.as_ref() { format!( @@ -405,11 +687,11 @@ fn what_is_selected_ui( item_ui::entity_path_button(ctx, &query, db, ui, None, entity_path); }); - list_existing_data_blueprints(ui, ctx, &entity_path.clone().into(), viewport); + list_existing_data_blueprints(ctx, blueprint, ui, &entity_path.clone().into()); } Item::SpaceView(space_view_id) => { - if let Some(space_view) = viewport.space_view(space_view_id) { + if let Some(space_view) = blueprint.space_view(space_view_id) { let space_view_class = space_view.class(ctx.space_view_class_registry); let hover_text = if let Some(display_name) = space_view.display_name.as_ref() { @@ -467,13 +749,13 @@ fn what_is_selected_ui( } } - list_existing_data_blueprints(ui, ctx, instance_path, viewport); + list_existing_data_blueprints(ctx, blueprint, ui, instance_path); } Item::DataResult(space_view_id, instance_path) => { let name = instance_path.syntax_highlighted(ui.style()); - if let Some(space_view) = viewport.space_view(space_view_id) { + if let Some(space_view) = blueprint.space_view(space_view_id) { let typ = item.kind(); item_title_ui( ctx.re_ui, @@ -553,10 +835,10 @@ fn item_title_ui( /// Display a list of all the space views an entity appears in. fn list_existing_data_blueprints( - ui: &mut egui::Ui, ctx: &ViewerContext<'_>, - instance_path: &InstancePath, blueprint: &ViewportBlueprint, + ui: &mut egui::Ui, + instance_path: &InstancePath, ) { let space_views_with_path = blueprint.space_views_containing_entity_path(ctx, &instance_path.entity_path); @@ -592,9 +874,9 @@ fn list_existing_data_blueprints( /// out as needing to be edited in most case when creating a new space view, which is why they are /// shown at the very top. fn space_view_top_level_properties( - ui: &mut egui::Ui, ctx: &ViewerContext<'_>, viewport: &ViewportBlueprint, + ui: &mut egui::Ui, space_view_id: &SpaceViewId, ) { if let Some(space_view) = viewport.space_view(space_view_id) { @@ -637,12 +919,12 @@ fn space_view_top_level_properties( } fn container_top_level_properties( - ui: &mut egui::Ui, ctx: &ViewerContext<'_>, - viewport: &Viewport<'_, '_>, + blueprint: &ViewportBlueprint, + ui: &mut egui::Ui, container_id: &ContainerId, ) { - let Some(container) = viewport.blueprint.container(container_id) else { + let Some(container) = blueprint.container(container_id) else { return; }; @@ -663,9 +945,7 @@ fn container_top_level_properties( let mut container_kind = container.container_kind; container_kind_selection_ui(ctx, ui, &mut container_kind); - viewport - .blueprint - .set_container_kind(*container_id, container_kind); + blueprint.set_container_kind(*container_id, container_kind); ui.end_row(); @@ -710,7 +990,7 @@ fn container_top_level_properties( .on_hover_text("Simplify this container and its children") .clicked() { - viewport.blueprint.simplify_container( + blueprint.simplify_container( container_id, egui_tiles::SimplificationOptions { prune_empty_tabs: true, @@ -748,7 +1028,7 @@ fn container_top_level_properties( .on_hover_text("Make all children the same size") .clicked() { - viewport.blueprint.make_all_children_same_size(container_id); + blueprint.make_all_children_same_size(container_id); } ui.end_row(); }); @@ -793,15 +1073,15 @@ fn container_kind_selection_ui( /// /// Return true if successful. fn show_list_item_for_container_child( - ui: &mut egui::Ui, ctx: &ViewerContext<'_>, - viewport: &Viewport<'_, '_>, + blueprint: &ViewportBlueprint, + ui: &mut egui::Ui, child_contents: &Contents, ) -> bool { let mut remove_contents = false; let (item, list_item_content) = match child_contents { Contents::SpaceView(space_view_id) => { - let Some(space_view) = viewport.blueprint.space_view(space_view_id) else { + let Some(space_view) = blueprint.space_view(space_view_id) else { re_log::warn_once!("Could not find space view with ID {space_view_id:?}",); return false; }; @@ -826,7 +1106,7 @@ fn show_list_item_for_container_child( ) } Contents::Container(container_id) => { - let Some(container) = viewport.blueprint.container(container_id) else { + let Some(container) = blueprint.container(container_id) else { re_log::warn_once!("Could not find container with ID {container_id:?}",); return false; }; @@ -862,7 +1142,7 @@ fn show_list_item_for_container_child( context_menu_ui_for_item( ctx, - viewport.blueprint, + blueprint, &item, &response, SelectionUpdateBehavior::Ignore, @@ -870,8 +1150,8 @@ fn show_list_item_for_container_child( ctx.select_hovered_on_click(&response, item); if remove_contents { - viewport.blueprint.mark_user_interaction(ctx); - viewport.blueprint.remove_contents(*child_contents); + blueprint.mark_user_interaction(ctx); + blueprint.remove_contents(*child_contents); } true @@ -890,120 +1170,14 @@ fn has_blueprint_section(item: &Item) -> bool { } } -/// What is the blueprint stuff for this item? -fn blueprint_ui( - ui: &mut egui::Ui, - ctx: &ViewerContext<'_>, - viewport: &mut Viewport<'_, '_>, - item: &Item, -) { - match item { - Item::AppId(_) - | Item::DataSource(_) - | Item::StoreId(_) - | Item::ComponentPath(_) - | Item::Container(_) - | Item::InstancePath(_) => {} - - Item::SpaceView(space_view_id) => { - blueprint_ui_for_space_view(ui, ctx, viewport, *space_view_id); - } - - Item::DataResult(space_view_id, instance_path) => { - blueprint_ui_for_data_result(ui, ctx, viewport, *space_view_id, instance_path); - } - } -} - -fn blueprint_ui_for_space_view( - ui: &mut Ui, - ctx: &ViewerContext<'_>, - viewport: &mut Viewport<'_, '_>, - space_view_id: SpaceViewId, -) { - if let Some(space_view) = viewport.blueprint.space_view(&space_view_id) { - if let Some(new_entity_path_filter) = entity_path_filter_ui( - ui, - ctx, - viewport, - space_view_id, - &space_view.contents.entity_path_filter, - &space_view.space_origin, - ) { - space_view - .contents - .set_entity_path_filter(ctx, &new_entity_path_filter); - } - - ui.add_space(ui.spacing().item_spacing.y); - } - - if ui - .button("Clone space view") - .on_hover_text( - "Create an exact duplicate of this space view including all blueprint settings", - ) - .clicked() - { - if let Some(new_space_view_id) = - viewport.blueprint.duplicate_space_view(&space_view_id, ctx) - { - ctx.selection_state() - .set_selection(Item::SpaceView(new_space_view_id)); - viewport.blueprint.mark_user_interaction(ctx); - } - } - - ui.add_space(ui.spacing().item_spacing.y / 2.0); - ReUi::full_span_separator(ui); - ui.add_space(ui.spacing().item_spacing.y / 2.0); - - if let Some(space_view) = viewport.blueprint.space_view(&space_view_id) { - let class_identifier = *space_view.class_identifier(); - - let space_view_state = viewport.state.space_view_state_mut( - ctx.space_view_class_registry, - space_view.id, - &class_identifier, - ); - - query_range_ui_space_view(ctx, ui, space_view); - - // Space View don't inherit (legacy) properties. - let mut props = - space_view.legacy_properties(ctx.store_context.blueprint, ctx.blueprint_query); - let props_before = props.clone(); - - let space_view_class = space_view.class(ctx.space_view_class_registry); - if let Err(err) = space_view_class.selection_ui( - ctx, - ui, - space_view_state.space_view_state.as_mut(), - &space_view.space_origin, - space_view.id, - &mut props, - ) { - re_log::error!( - "Error in space view selection UI (class: {}, display name: {}): {err}", - space_view.class_identifier(), - space_view_class.display_name(), - ); - } - - if props_before != props { - space_view.save_legacy_properties(ctx, props); - } - } -} - fn blueprint_ui_for_data_result( - ui: &mut Ui, ctx: &ViewerContext<'_>, - viewport: &Viewport<'_, '_>, + blueprint: &ViewportBlueprint, + ui: &mut Ui, space_view_id: SpaceViewId, instance_path: &InstancePath, ) { - if let Some(space_view) = viewport.blueprint.space_view(&space_view_id) { + if let Some(space_view) = blueprint.space_view(&space_view_id) { if instance_path.instance.is_all() { // the whole entity let entity_path = &instance_path.entity_path; @@ -1033,167 +1207,6 @@ fn blueprint_ui_for_data_result( } } -/// Returns a new filter when the editing is done, and there has been a change. -fn entity_path_filter_ui( - ui: &mut egui::Ui, - ctx: &ViewerContext<'_>, - viewport: &mut Viewport<'_, '_>, - space_view_id: SpaceViewId, - filter: &EntityPathFilter, - origin: &EntityPath, -) -> Option { - fn entity_path_filter_help_ui(ui: &mut egui::Ui) { - let markdown = r#" -# Entity path query syntax - -Entity path queries are described as a list of include/exclude rules that act on paths: - -```diff -+ /world/** # add everything… -- /world/roads/** # …but remove all roads… -+ /world/roads/main # …but show main road -``` - -If there are multiple matching rules, the most specific rule wins. -If there are multiple rules of the same specificity, the last one wins. -If no rules match, the path is excluded. - -The `/**` suffix matches the whole subtree, i.e. self and any child, recursively -(`/world/**` matches both `/world` and `/world/car/driver`). -Other uses of `*` are not (yet) supported. - -`EntityPathFilter` sorts the rule by entity path, with recursive coming before non-recursive. -This means the last matching rule is also the most specific one. -For instance: - -```diff -+ /world/** -- /world -- /world/car/** -+ /world/car/driver -``` - -The last rule matching `/world/car/driver` is `+ /world/car/driver`, so it is included. -The last rule matching `/world/car/hood` is `- /world/car/**`, so it is excluded. -The last rule matching `/world` is `- /world`, so it is excluded. -The last rule matching `/world/house` is `+ /world/**`, so it is included. - "# - .trim(); - - re_ui::markdown_ui(ui, egui::Id::new("entity_path_filter_help_ui"), markdown); - } - - fn syntax_highlight_entity_path_filter( - style: &egui::Style, - mut string: &str, - ) -> egui::text::LayoutJob { - let font_id = egui::TextStyle::Body.resolve(style); - - let mut job = egui::text::LayoutJob::default(); - - while !string.is_empty() { - let newline = string.find('\n').unwrap_or(string.len() - 1); - let line = &string[..=newline]; - string = &string[newline + 1..]; - let is_exclusion = line.trim_start().starts_with('-'); - - let color = if is_exclusion { - egui::Color32::LIGHT_RED - } else { - egui::Color32::LIGHT_GREEN - }; - - let text_format = egui::TextFormat { - font_id: font_id.clone(), - color, - ..Default::default() - }; - - job.append(line, 0.0, text_format); - } - - job - } - - fn text_layouter(ui: &egui::Ui, string: &str, wrap_width: f32) -> std::sync::Arc { - let mut layout_job = syntax_highlight_entity_path_filter(ui.style(), string); - layout_job.wrap.max_width = wrap_width; - ui.fonts(|f| f.layout_job(layout_job)) - } - - // We store the string we are temporarily editing in the `Ui`'s temporary data storage. - // This is so it can contain invalid rules while the user edits it, and it's only normalized - // when they press enter, or stops editing. - let filter_text_id = ui.id().with("filter_text"); - - let mut filter_string = ui.data_mut(|data| { - data.get_temp_mut_or_insert_with::(filter_text_id, || filter.formatted()) - .clone() - }); - - let rightmost_x = ui.cursor().min.x; - ui.horizontal(|ui| { - ui.label("Entity path query").on_hover_text( - "The entity path query consists of a list of include/exclude rules \ - that determines what entities are part of this space view", - ); - - let current_x = ui.cursor().min.x; - // Compute a width that results in these things to be right-aligned with the following text edit. - let desired_width = (ui.available_width() - ui.spacing().item_spacing.x) - .at_most(ui.spacing().text_edit_width - (current_x - rightmost_x)); - - ui.allocate_ui_with_layout( - egui::vec2(desired_width, ui.available_height()), - egui::Layout::right_to_left(egui::Align::Center), - |ui| { - re_ui::help_hover_button(ui).on_hover_ui(entity_path_filter_help_ui); - if ui - .button("Edit") - .on_hover_text("Modify the entity query using the editor") - .clicked() - { - viewport.show_add_remove_entities_modal(space_view_id); - } - }, - ); - }); - - let response = - ui.add(egui::TextEdit::multiline(&mut filter_string).layouter(&mut text_layouter)); - - if response.has_focus() { - ui.data_mut(|data| data.insert_temp::(filter_text_id, filter_string.clone())); - } else { - // Reconstruct it from the filter next frame - ui.data_mut(|data| data.remove::(filter_text_id)); - } - - // Show some statistics about the query, print a warning text if something seems off. - let query = ctx.lookup_query_result(space_view_id); - if query.num_matching_entities == 0 { - ui.label(ctx.re_ui.warning_text("Does not match any entity")); - } else if query.num_matching_entities == 1 { - ui.label("Matches 1 entity"); - } else { - ui.label(format!("Matches {} entities", query.num_matching_entities)); - } - if query.num_matching_entities != 0 && query.num_visualized_entities == 0 { - // TODO(andreas): Talk about this root bit only if it's a spatial view. - ui.label(ctx.re_ui.warning_text( - format!("This space view is not able to visualize any of the matched entities using the current root \"{origin:?}\"."), - )); - } - - // Apply the edit. - let new_filter = EntityPathFilter::parse_forgiving(&filter_string, &Default::default()); - if &new_filter == filter { - None // no change - } else { - Some(new_filter) - } -} - fn entity_props_ui( ctx: &ViewerContext<'_>, ui: &mut egui::Ui, diff --git a/crates/re_viewport/src/space_view_entity_picker.rs b/crates/re_selection_panel/src/space_view_entity_picker.rs similarity index 99% rename from crates/re_viewport/src/space_view_entity_picker.rs rename to crates/re_selection_panel/src/space_view_entity_picker.rs index 2d86da761a84..dc1183b86e75 100644 --- a/crates/re_viewport/src/space_view_entity_picker.rs +++ b/crates/re_selection_panel/src/space_view_entity_picker.rs @@ -11,7 +11,7 @@ use re_viewport_blueprint::{SpaceViewBlueprint, ViewportBlueprint}; /// /// Delegates to [`re_ui::modal::ModalHandler`] #[derive(Default)] -pub struct SpaceViewEntityPicker { +pub(crate) struct SpaceViewEntityPicker { space_view_id: Option, modal_handler: re_ui::modal::ModalHandler, } @@ -156,6 +156,7 @@ fn add_entities_line_ui( ui.horizontal(|ui| { let entity_path = &entity_tree.path; + #[allow(clippy::unwrap_used)] let add_info = entities_add_info.get(entity_path).unwrap(); let is_explicitly_excluded = entity_path_filter.is_explicitly_excluded(entity_path); diff --git a/crates/re_viewer/src/ui/space_view_space_origin_ui.rs b/crates/re_selection_panel/src/space_view_space_origin_ui.rs similarity index 94% rename from crates/re_viewer/src/ui/space_view_space_origin_ui.rs rename to crates/re_selection_panel/src/space_view_space_origin_ui.rs index 6830c3e83199..27d19c46b0ed 100644 --- a/crates/re_viewer/src/ui/space_view_space_origin_ui.rs +++ b/crates/re_selection_panel/src/space_view_space_origin_ui.rs @@ -1,12 +1,11 @@ use std::ops::ControlFlow; -use eframe::emath::NumExt; -use egui::{Key, Ui}; +use egui::{Key, NumExt as _, Ui}; use re_log_types::EntityPath; use re_ui::{list_item, ReUi, SyntaxHighlighting}; use re_viewer_context::ViewerContext; -use re_viewport_blueprint::SpaceViewBlueprint; +use re_viewport_blueprint::{default_created_space_views, SpaceViewBlueprint}; /// State of the space origin widget. #[derive(Default, Clone)] @@ -92,13 +91,12 @@ fn space_view_space_origin_widget_editing_ui( // All suggestions for this class of space views. // TODO(#4895): we should have/use a much simpler heuristic API to get a list of compatible entity sub-tree - let space_view_suggestions = - re_viewport::space_view_heuristics::default_created_space_views(ctx) - .into_iter() - .filter(|this_space_view| { - this_space_view.class_identifier() == space_view.class_identifier() - }) - .collect::>(); + let space_view_suggestions = default_created_space_views(ctx) + .into_iter() + .filter(|this_space_view| { + this_space_view.class_identifier() == space_view.class_identifier() + }) + .collect::>(); // Filtered suggestions based on the current text edit content. let filtered_space_view_suggestions = space_view_suggestions diff --git a/crates/re_viewer/Cargo.toml b/crates/re_viewer/Cargo.toml index 56b341432d91..d9c82e49ead5 100644 --- a/crates/re_viewer/Cargo.toml +++ b/crates/re_viewer/Cargo.toml @@ -38,7 +38,6 @@ analytics = ["dep:re_analytics"] [dependencies] # Internal: -re_context_menu.workspace = true re_build_info.workspace = true re_blueprint_tree.workspace = true re_data_loader.workspace = true @@ -58,6 +57,7 @@ re_log_types.workspace = true re_memory.workspace = true re_query.workspace = true re_renderer = { workspace = true, default-features = false } +re_selection_panel.workspace = true re_sdk_comms.workspace = true re_smart_channel.workspace = true re_space_view_bar_chart.workspace = true @@ -96,17 +96,14 @@ eframe = { workspace = true, default-features = false, features = [ egui_plot.workspace = true egui-wgpu.workspace = true egui.workspace = true -egui_tiles.workspace = true ehttp.workspace = true image = { workspace = true, default-features = false, features = ["png"] } itertools = { workspace = true } -once_cell = { workspace = true } poll-promise = { workspace = true, features = ["web"] } rfd.workspace = true ron.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true -static_assertions.workspace = true thiserror.workspace = true time = { workspace = true, features = ["formatting"] } web-time.workspace = true diff --git a/crates/re_viewer/src/app_state.rs b/crates/re_viewer/src/app_state.rs index 6775f4a05deb..106f7df3e23d 100644 --- a/crates/re_viewer/src/app_state.rs +++ b/crates/re_viewer/src/app_state.rs @@ -8,9 +8,10 @@ use re_types::blueprint::components::PanelState; use re_viewer_context::{ blueprint_timeline, AppOptions, ApplicationSelectionState, Caches, CommandSender, ComponentUiRegistry, PlayState, RecordingConfig, SpaceViewClassExt as _, - SpaceViewClassRegistry, StoreContext, StoreHub, SystemCommandSender as _, ViewerContext, + SpaceViewClassRegistry, StoreContext, StoreHub, SystemCommandSender as _, ViewStates, + ViewerContext, }; -use re_viewport::{Viewport, ViewportState}; +use re_viewport::Viewport; use re_viewport_blueprint::ui::add_space_view_or_container_modal_ui; use re_viewport_blueprint::ViewportBlueprint; @@ -33,7 +34,7 @@ pub struct AppState { recording_configs: HashMap, blueprint_cfg: RecordingConfig, - selection_panel: crate::selection_panel::SelectionPanel, + selection_panel: re_selection_panel::SelectionPanel, time_panel: re_time_panel::TimePanel, blueprint_panel: re_time_panel::TimePanel, #[serde(skip)] @@ -42,10 +43,12 @@ pub struct AppState { #[serde(skip)] welcome_screen: crate::ui::WelcomeScreen, - // TODO(jleibs): This is sort of a weird place to put this but makes more - // sense than the blueprint + /// Storage for the state of each `SpaceView` + /// + /// This is stored here for simplicity. An exclusive reference for that is passed to the users, + /// such as [`Viewport`] and [`SelectionPanel`]. #[serde(skip)] - viewport_state: ViewportState, + view_states: ViewStates, /// Selection & hovering state. pub selection_state: ApplicationSelectionState, @@ -70,7 +73,7 @@ impl Default for AppState { blueprint_panel: re_time_panel::TimePanel::new_blueprint_panel(), blueprint_tree: Default::default(), welcome_screen: Default::default(), - viewport_state: Default::default(), + view_states: Default::default(), selection_state: Default::default(), focused_item: Default::default(), } @@ -143,7 +146,7 @@ impl AppState { blueprint_panel, blueprint_tree, welcome_screen, - viewport_state, + view_states, selection_state, focused_item, } = self; @@ -160,7 +163,6 @@ impl AppState { ); let mut viewport = Viewport::new( &viewport_blueprint, - viewport_state, space_view_class_registry, receiver, sender, @@ -257,7 +259,7 @@ impl AppState { // First update the viewport and thus all active space views. // This may update their heuristics, so that all panels that are shown in this frame, // have the latest information. - viewport.on_frame_start(&ctx); + viewport.on_frame_start(&ctx, view_states); { re_tracing::profile_scope!("updated_query_results"); @@ -289,7 +291,7 @@ impl AppState { &blueprint_query, rec_cfg.time_ctrl.read().timeline(), space_view_class_registry, - viewport.state.legacy_auto_properties(space_view.id), + view_states.legacy_auto_properties(space_view.id), query_result, ); } @@ -351,8 +353,9 @@ impl AppState { selection_panel.show_panel( &ctx, + &viewport_blueprint, + view_states, ui, - &mut viewport, app_blueprint.selection_panel_state.is_expanded(), ); @@ -429,7 +432,7 @@ impl AppState { if show_welcome { welcome_screen.ui(ui, re_ui, command_sender, welcome_screen_state); } else { - viewport.viewport_ui(ui, &ctx); + viewport.viewport_ui(ui, &ctx, view_states); } }); @@ -592,7 +595,3 @@ fn check_for_clicked_hyperlinks( pub fn default_blueprint_panel_width(screen_width: f32) -> f32 { (0.35 * screen_width).min(200.0).round() } - -pub fn default_selection_panel_width(screen_width: f32) -> f32 { - (0.45 * screen_width).min(300.0).round() -} diff --git a/crates/re_viewer/src/lib.rs b/crates/re_viewer/src/lib.rs index f5519a34176c..3017d5489ccb 100644 --- a/crates/re_viewer/src/lib.rs +++ b/crates/re_viewer/src/lib.rs @@ -25,10 +25,7 @@ mod viewer_analytics; /// Unstable. Used for the ongoing blueprint experimentations. pub mod blueprint; -pub(crate) use { - app_state::AppState, - ui::{memory_panel, selection_panel}, -}; +pub(crate) use {app_state::AppState, ui::memory_panel}; pub use app::{App, StartupOptions}; diff --git a/crates/re_viewer/src/ui/mod.rs b/crates/re_viewer/src/ui/mod.rs index 802406d84163..8a4f36bde88c 100644 --- a/crates/re_viewer/src/ui/mod.rs +++ b/crates/re_viewer/src/ui/mod.rs @@ -1,15 +1,10 @@ mod mobile_warning_ui; -mod override_ui; mod recordings_panel; mod rerun_menu; -mod selection_history_ui; mod top_panel; mod welcome_screen; pub(crate) mod memory_panel; -pub(crate) mod query_range_ui; -pub(crate) mod selection_panel; -pub(crate) mod space_view_space_origin_ui; pub use recordings_panel::recordings_panel_ui; // ---- diff --git a/crates/re_viewer_context/src/lib.rs b/crates/re_viewer_context/src/lib.rs index 67dca7747eee..4dad50df7018 100644 --- a/crates/re_viewer_context/src/lib.rs +++ b/crates/re_viewer_context/src/lib.rs @@ -51,13 +51,14 @@ pub use selection_state::{ }; pub use space_view::{ DataResult, IdentifiedViewSystem, OverridePath, PerSystemDataResults, PerSystemEntities, - PropertyOverrides, RecommendedSpaceView, SmallVisualizerSet, SpaceViewClass, SpaceViewClassExt, - SpaceViewClassLayoutPriority, SpaceViewClassRegistry, SpaceViewClassRegistryError, - SpaceViewEntityHighlight, SpaceViewHighlights, SpaceViewOutlineMasks, SpaceViewSpawnHeuristics, - SpaceViewState, SpaceViewStateExt, SpaceViewSystemExecutionError, SpaceViewSystemRegistrator, - SystemExecutionOutput, ViewContextCollection, ViewContextSystem, ViewQuery, - ViewSystemIdentifier, VisualizableFilterContext, VisualizerAdditionalApplicabilityFilter, - VisualizerCollection, VisualizerQueryInfo, VisualizerSystem, + PerViewState, PropertyOverrides, RecommendedSpaceView, SmallVisualizerSet, SpaceViewClass, + SpaceViewClassExt, SpaceViewClassLayoutPriority, SpaceViewClassRegistry, + SpaceViewClassRegistryError, SpaceViewEntityHighlight, SpaceViewHighlights, + SpaceViewOutlineMasks, SpaceViewSpawnHeuristics, SpaceViewState, SpaceViewStateExt, + SpaceViewSystemExecutionError, SpaceViewSystemRegistrator, SystemExecutionOutput, + ViewContextCollection, ViewContextSystem, ViewQuery, ViewStates, ViewSystemIdentifier, + VisualizableFilterContext, VisualizerAdditionalApplicabilityFilter, VisualizerCollection, + VisualizerQueryInfo, VisualizerSystem, }; pub use store_context::StoreContext; pub use store_hub::StoreHub; diff --git a/crates/re_viewer_context/src/space_view/mod.rs b/crates/re_viewer_context/src/space_view/mod.rs index df04e0e9522a..31003ddf662e 100644 --- a/crates/re_viewer_context/src/space_view/mod.rs +++ b/crates/re_viewer_context/src/space_view/mod.rs @@ -13,6 +13,7 @@ mod spawn_heuristics; mod system_execution_output; mod view_context_system; mod view_query; +mod view_states; mod visualizer_entity_subscriber; mod visualizer_system; @@ -32,6 +33,7 @@ pub use view_query::{ DataResult, OverridePath, PerSystemDataResults, PropertyOverrides, SmallVisualizerSet, ViewQuery, }; +pub use view_states::{PerViewState, ViewStates}; pub use visualizer_entity_subscriber::VisualizerAdditionalApplicabilityFilter; pub use visualizer_system::{VisualizerCollection, VisualizerQueryInfo, VisualizerSystem}; diff --git a/crates/re_viewer_context/src/space_view/view_states.rs b/crates/re_viewer_context/src/space_view/view_states.rs new file mode 100644 index 000000000000..c921afa8ec02 --- /dev/null +++ b/crates/re_viewer_context/src/space_view/view_states.rs @@ -0,0 +1,53 @@ +//! Storage for the state of each `SpaceView`. +//! +//! The `Viewer` has ownership of this state and pass it around to users (mainly viewport and +//! selection panel). + +use ahash::HashMap; +use once_cell::sync::Lazy; + +use re_entity_db::EntityPropertyMap; +use re_log_types::external::re_types_core::SpaceViewClassIdentifier; + +use crate::{SpaceViewClassRegistry, SpaceViewId, SpaceViewState}; + +// State for each `SpaceView` including both the auto properties and +// the internal state of the space view itself. +pub struct PerViewState { + pub auto_properties: EntityPropertyMap, + pub view_state: Box, +} + +// ---------------------------------------------------------------------------- +/// State for the [`SpaceViews`] that persists across frames but otherwise +/// is not saved. +#[derive(Default)] +pub struct ViewStates { + states: HashMap, +} + +static DEFAULT_PROPS: Lazy = Lazy::::new(Default::default); + +impl ViewStates { + pub fn view_state_mut( + &mut self, + space_view_class_registry: &SpaceViewClassRegistry, + space_view_id: SpaceViewId, + space_view_class: &SpaceViewClassIdentifier, + ) -> &mut PerViewState { + self.states + .entry(space_view_id) + .or_insert_with(|| PerViewState { + auto_properties: Default::default(), + view_state: space_view_class_registry + .get_class_or_log_error(space_view_class) + .new_state(), + }) + } + + pub fn legacy_auto_properties(&self, space_view_id: SpaceViewId) -> &EntityPropertyMap { + self.states + .get(&space_view_id) + .map_or(&DEFAULT_PROPS, |state| &state.auto_properties) + } +} diff --git a/crates/re_viewport/Cargo.toml b/crates/re_viewport/Cargo.toml index d862760ebe52..5c1cc575ef14 100644 --- a/crates/re_viewport/Cargo.toml +++ b/crates/re_viewport/Cargo.toml @@ -20,7 +20,6 @@ all-features = true [dependencies] re_context_menu.workspace = true -re_data_ui.workspace = true re_entity_db = { workspace = true, features = ["serde"] } re_log_types.workspace = true re_log.workspace = true @@ -45,5 +44,4 @@ glam.workspace = true image = { workspace = true, default-features = false, features = ["png"] } itertools.workspace = true nohash-hasher.workspace = true -once_cell.workspace = true rayon.workspace = true diff --git a/crates/re_viewport/src/lib.rs b/crates/re_viewport/src/lib.rs index 1ef82670ae1a..2f96ed237402 100644 --- a/crates/re_viewport/src/lib.rs +++ b/crates/re_viewport/src/lib.rs @@ -7,13 +7,11 @@ mod auto_layout; mod screenshot; -mod space_view_entity_picker; -pub mod space_view_heuristics; mod space_view_highlights; mod system_execution; mod viewport; -pub use self::viewport::{Viewport, ViewportState}; +pub use self::viewport::Viewport; pub mod external { pub use re_space_view; diff --git a/crates/re_viewport/src/space_view_heuristics.rs b/crates/re_viewport/src/space_view_heuristics.rs deleted file mode 100644 index 5185143e0ce9..000000000000 --- a/crates/re_viewport/src/space_view_heuristics.rs +++ /dev/null @@ -1,22 +0,0 @@ -use re_viewer_context::ViewerContext; - -use re_viewport_blueprint::SpaceViewBlueprint; - -/// List out all space views we generate by default for the available data. -/// -/// TODO(andreas): This is transitional. We want to pass on the space view spawn heuristics -/// directly and make more high level decisions with it. -pub fn default_created_space_views(ctx: &ViewerContext<'_>) -> Vec { - re_tracing::profile_function!(); - - ctx.space_view_class_registry - .iter_registry() - .flat_map(|entry| { - let spawn_heuristics = entry.class.spawn_heuristics(ctx); - spawn_heuristics - .into_vec() - .into_iter() - .map(|recommendation| SpaceViewBlueprint::new(entry.identifier, recommendation)) - }) - .collect() -} diff --git a/crates/re_viewport/src/viewport.rs b/crates/re_viewport/src/viewport.rs index 27c2f256a316..ae760e465caa 100644 --- a/crates/re_viewport/src/viewport.rs +++ b/crates/re_viewport/src/viewport.rs @@ -4,70 +4,22 @@ use ahash::HashMap; use egui_tiles::{Behavior as _, EditAction}; -use once_cell::sync::Lazy; use re_context_menu::{context_menu_ui_for_item, SelectionUpdateBehavior}; -use re_entity_db::EntityPropertyMap; use re_renderer::ScreenshotProcessor; -use re_types::SpaceViewClassIdentifier; use re_ui::{Icon, ReUi}; use re_viewer_context::{ - blueprint_id_to_tile_id, icon_for_container_kind, ContainerId, Contents, Item, - SpaceViewClassRegistry, SpaceViewId, SpaceViewState, SystemExecutionOutput, ViewQuery, + blueprint_id_to_tile_id, icon_for_container_kind, ContainerId, Contents, Item, PerViewState, + SpaceViewClassRegistry, SpaceViewId, SystemExecutionOutput, ViewQuery, ViewStates, ViewerContext, }; use re_viewport_blueprint::{TreeAction, ViewportBlueprint}; use crate::{ screenshot::handle_pending_space_view_screenshots, - space_view_entity_picker::SpaceViewEntityPicker, system_execution::execute_systems_for_all_space_views, }; -// State for each `SpaceView` including both the auto properties and -// the internal state of the space view itself. -pub struct PerSpaceViewState { - pub auto_properties: EntityPropertyMap, - pub space_view_state: Box, -} - -// ---------------------------------------------------------------------------- -/// State for the [`Viewport`] that persists across frames but otherwise -/// is not saved. -#[derive(Default)] -pub struct ViewportState { - /// State for the "Add entity" modal. - space_view_entity_modal: SpaceViewEntityPicker, - - space_view_states: HashMap, -} - -static DEFAULT_PROPS: Lazy = Lazy::::new(Default::default); - -impl ViewportState { - pub fn space_view_state_mut( - &mut self, - space_view_class_registry: &SpaceViewClassRegistry, - space_view_id: SpaceViewId, - space_view_class: &SpaceViewClassIdentifier, - ) -> &mut PerSpaceViewState { - self.space_view_states - .entry(space_view_id) - .or_insert_with(|| PerSpaceViewState { - auto_properties: Default::default(), - space_view_state: space_view_class_registry - .get_class_or_log_error(space_view_class) - .new_state(), - }) - } - - pub fn legacy_auto_properties(&self, space_view_id: SpaceViewId) -> &EntityPropertyMap { - self.space_view_states - .get(&space_view_id) - .map_or(&DEFAULT_PROPS, |state| &state.auto_properties) - } -} - fn tree_simplification_options() -> egui_tiles::SimplificationOptions { egui_tiles::SimplificationOptions { prune_empty_tabs: false, @@ -82,15 +34,11 @@ fn tree_simplification_options() -> egui_tiles::SimplificationOptions { // ---------------------------------------------------------------------------- /// Defines the layout of the Viewport -pub struct Viewport<'a, 'b> { +pub struct Viewport<'a> { /// The blueprint that drives this viewport. This is the source of truth from the store /// for this frame. pub blueprint: &'a ViewportBlueprint, - /// The persistent state of the viewport that is not saved to the store but otherwise - /// persis frame-to-frame. - pub state: &'b mut ViewportState, - /// The [`egui_tiles::Tree`] tree that actually manages blueprint layout. This tree needs /// to be mutable for things like drag-and-drop and is ultimately saved back to the store. /// at the end of the frame if edited. @@ -112,10 +60,9 @@ pub struct Viewport<'a, 'b> { tree_action_sender: std::sync::mpsc::Sender, } -impl<'a, 'b> Viewport<'a, 'b> { +impl<'a> Viewport<'a> { pub fn new( blueprint: &'a ViewportBlueprint, - state: &'b mut ViewportState, space_view_class_registry: &SpaceViewClassRegistry, tree_action_receiver: std::sync::mpsc::Receiver, tree_action_sender: std::sync::mpsc::Sender, @@ -137,7 +84,6 @@ impl<'a, 'b> Viewport<'a, 'b> { Self { blueprint, - state, tree, tree_edited: edited, tree_action_receiver, @@ -145,19 +91,13 @@ impl<'a, 'b> Viewport<'a, 'b> { } } - pub fn show_add_remove_entities_modal(&mut self, space_view_id: SpaceViewId) { - self.state.space_view_entity_modal.open(space_view_id); - } - - pub fn viewport_ui(&mut self, ui: &mut egui::Ui, ctx: &'a ViewerContext<'_>) { - // run modals (these are noop if the modals are not active) - self.state - .space_view_entity_modal - .ui(ui.ctx(), ctx, self.blueprint); - - let Viewport { - blueprint, state, .. - } = self; + pub fn viewport_ui( + &mut self, + ui: &mut egui::Ui, + ctx: &'a ViewerContext<'_>, + view_states: &mut ViewStates, + ) { + let Viewport { blueprint, .. } = self; let is_zero_sized_viewport = ui.available_size().min_elem() <= 0.0; if is_zero_sized_viewport { @@ -201,7 +141,7 @@ impl<'a, 'b> Viewport<'a, 'b> { re_tracing::profile_scope!("tree.ui"); let mut tab_viewer = TabViewer { - viewport_state: state, + view_states, ctx, viewport_blueprint: blueprint, maximized: &mut maximized, @@ -234,14 +174,14 @@ impl<'a, 'b> Viewport<'a, 'b> { self.blueprint.set_maximized(maximized, ctx); } - pub fn on_frame_start(&mut self, ctx: &ViewerContext<'_>) { + pub fn on_frame_start(&mut self, ctx: &ViewerContext<'_>, view_states: &mut ViewStates) { re_tracing::profile_function!(); for space_view in self.blueprint.space_views.values() { - let PerSpaceViewState { + let PerViewState { auto_properties, - space_view_state, - } = self.state.space_view_state_mut( + view_state: space_view_state, + } = view_states.view_state_mut( ctx.space_view_class_registry, space_view.id, space_view.class_identifier(), @@ -495,7 +435,7 @@ impl<'a, 'b> Viewport<'a, 'b> { /// In our case, each pane is a space view, /// while containers are just groups of things. struct TabViewer<'a, 'b> { - viewport_state: &'a mut ViewportState, + view_states: &'a mut ViewStates, ctx: &'a ViewerContext<'b>, viewport_blueprint: &'a ViewportBlueprint, maximized: &'a mut Option, @@ -563,10 +503,10 @@ impl<'a, 'b> egui_tiles::Behavior for TabViewer<'a, 'b> { ) }); - let PerSpaceViewState { + let PerViewState { auto_properties: _, - space_view_state, - } = self.viewport_state.space_view_state_mut( + view_state: space_view_state, + } = self.view_states.view_state_mut( self.ctx.space_view_class_registry, space_view_blueprint.id, space_view_blueprint.class_identifier(), diff --git a/crates/re_viewport_blueprint/src/lib.rs b/crates/re_viewport_blueprint/src/lib.rs index 28f1e699c469..e29a9d0d9f01 100644 --- a/crates/re_viewport_blueprint/src/lib.rs +++ b/crates/re_viewport_blueprint/src/lib.rs @@ -11,6 +11,7 @@ mod view_properties; mod viewport_blueprint; pub use container::ContainerBlueprint; +use re_viewer_context::ViewerContext; pub use space_view::SpaceViewBlueprint; pub use space_view_contents::SpaceViewContents; pub use tree_actions::TreeAction; @@ -55,3 +56,22 @@ pub fn container_kind_from_egui( egui_tiles::ContainerKind::Grid => ContainerKind::Grid, } } + +/// List out all space views we generate by default for the available data. +/// +/// TODO(andreas): This is transitional. We want to pass on the space view spawn heuristics +/// directly and make more high level decisions with it. +pub fn default_created_space_views(ctx: &ViewerContext<'_>) -> Vec { + re_tracing::profile_function!(); + + ctx.space_view_class_registry + .iter_registry() + .flat_map(|entry| { + let spawn_heuristics = entry.class.spawn_heuristics(ctx); + spawn_heuristics + .into_vec() + .into_iter() + .map(|recommendation| SpaceViewBlueprint::new(entry.identifier, recommendation)) + }) + .collect() +}