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()
+}