diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 8c364bda6cf3b..fdedf20f219ac 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:
-
-
-
-
-
+
+
+
+
+
@@ -125,21 +125,22 @@ Update instructions:
##### UI crates
-| Crate | Description |
-|-----------------------------|----------------------------------------------------------------------------------------|
-| re_blueprint_tree | The UI for the blueprint tree in the left panel. |
-| 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. |
-| re_space_view_spatial | Space Views that show entities in a 2D or 3D spatial relationship. |
-| re_space_view_tensor | A Space View dedicated to visualizing tensors with arbitrary dimensionality. |
-| re_space_view_text_document | A simple Space View that shows a single text box. |
-| re_space_view_text_log | A Space View that shows text entries in a table and scrolls with the active time. |
-| re_space_view_time_series | A Space View that shows plots over Rerun timelines. |
+| Crate | Description |
+|-----------------------------|-------------------------------------------------------------------------------------------------------------|
+| re_blueprint_tree | The UI for the blueprint tree in the left panel. |
+| re_edit_ui | Provides ui editors for Rerun component data for registration with the Rerun Viewer component ui registry. |
+| 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. |
+| re_space_view_spatial | Space Views that show entities in a 2D or 3D spatial relationship. |
+| re_space_view_tensor | A Space View dedicated to visualizing tensors with arbitrary dimensionality. |
+| re_space_view_text_document | A simple Space View that shows a single text box. |
+| re_space_view_text_log | A Space View that shows text entries in a table and scrolls with the active time. |
+| re_space_view_time_series | A Space View that shows plots over Rerun timelines. |
+| re_time_panel | The time panel of the Rerun Viewer, allowing to control the displayed timeline & time. |
+| re_viewer | The Rerun Viewer |
+| re_viewport | The central viewport panel of the Rerun viewer. |
##### UI support crates
diff --git a/Cargo.lock b/Cargo.lock
index c0c559cf73ef1..67b63fe47a50e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4347,6 +4347,21 @@ dependencies = [
"wasm-bindgen-cli-support",
]
+[[package]]
+name = "re_edit_ui"
+version = "0.17.0-alpha.3"
+dependencies = [
+ "egui",
+ "egui_plot",
+ "re_data_store",
+ "re_entity_db",
+ "re_log_types",
+ "re_types",
+ "re_types_blueprint",
+ "re_ui",
+ "re_viewer_context",
+]
+
[[package]]
name = "re_entity_db"
version = "0.17.0-alpha.3"
@@ -5077,6 +5092,7 @@ dependencies = [
"re_data_source",
"re_data_store",
"re_data_ui",
+ "re_edit_ui",
"re_entity_db",
"re_error",
"re_format",
diff --git a/Cargo.toml b/Cargo.toml
index 42f19c710c0c0..1f6f7b8112b1f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -32,13 +32,14 @@ re_analytics = { path = "crates/re_analytics", version = "=0.17.0-alpha.3", defa
re_blueprint_tree = { path = "crates/re_blueprint_tree", version = "=0.17.0-alpha.3", default-features = false }
re_build_info = { path = "crates/re_build_info", version = "=0.17.0-alpha.3", default-features = false }
re_build_tools = { path = "crates/re_build_tools", version = "=0.17.0-alpha.3", default-features = false }
-re_crash_handler = { path = "crates/re_crash_handler", version = "=0.17.0-alpha.3", default-features = false }
re_context_menu = { path = "crates/re_context_menu", version = "=0.17.0-alpha.3", default-features = false }
+re_crash_handler = { path = "crates/re_crash_handler", version = "=0.17.0-alpha.3", default-features = false }
re_data_loader = { path = "crates/re_data_loader", version = "=0.17.0-alpha.3", default-features = false }
re_data_source = { path = "crates/re_data_source", version = "=0.17.0-alpha.3", default-features = false }
re_data_store = { path = "crates/re_data_store", version = "=0.17.0-alpha.3", default-features = false }
re_data_ui = { path = "crates/re_data_ui", version = "=0.17.0-alpha.3", default-features = false }
re_dev_tools = { path = "crates/re_dev_tools", version = "=0.17.0-alpha.3", default-features = false }
+re_edit_ui = { path = "crates/re_edit_ui", version = "=0.17.0-alpha.3", default-features = false }
re_entity_db = { path = "crates/re_entity_db", version = "=0.17.0-alpha.3", default-features = false }
re_error = { path = "crates/re_error", version = "=0.17.0-alpha.3", default-features = false }
re_format = { path = "crates/re_format", version = "=0.17.0-alpha.3", default-features = false }
@@ -50,9 +51,9 @@ re_log_types = { path = "crates/re_log_types", version = "=0.17.0-alpha.3", defa
re_memory = { path = "crates/re_memory", version = "=0.17.0-alpha.3", default-features = false }
re_query = { path = "crates/re_query", version = "=0.17.0-alpha.3", default-features = false }
re_renderer = { path = "crates/re_renderer", version = "=0.17.0-alpha.3", default-features = false }
-re_selection_panel = { path = "crates/re_selection_panel", version = "=0.17.0-alpha.3", default-features = false }
re_sdk = { path = "crates/re_sdk", version = "=0.17.0-alpha.3", default-features = false }
re_sdk_comms = { path = "crates/re_sdk_comms", version = "=0.17.0-alpha.3", default-features = false }
+re_selection_panel = { path = "crates/re_selection_panel", version = "=0.17.0-alpha.3", default-features = false }
re_smart_channel = { path = "crates/re_smart_channel", version = "=0.17.0-alpha.3", default-features = false }
re_space_view = { path = "crates/re_space_view", version = "=0.17.0-alpha.3", default-features = false }
re_space_view_bar_chart = { path = "crates/re_space_view_bar_chart", version = "=0.17.0-alpha.3", default-features = false }
diff --git a/crates/re_data_ui/src/component_ui_registry.rs b/crates/re_data_ui/src/component_ui_registry.rs
index 5154ec45337e4..1a28068bcbe3f 100644
--- a/crates/re_data_ui/src/component_ui_registry.rs
+++ b/crates/re_data_ui/src/component_ui_registry.rs
@@ -4,8 +4,6 @@ use re_log_types::{external::arrow2, EntityPath, Instance};
use re_types::external::arrow2::array::Utf8Array;
use re_viewer_context::{ComponentUiRegistry, UiLayout, ViewerContext};
-use crate::editors::register_editors;
-
use super::{data_label_for_ui_layout, EntityDataUi};
pub fn create_component_ui_registry() -> ComponentUiRegistry {
@@ -33,8 +31,6 @@ pub fn create_component_ui_registry() -> ComponentUiRegistry {
add_to_registry::(&mut registry);
add_to_registry::(&mut registry);
- register_editors(&mut registry);
-
registry
}
diff --git a/crates/re_data_ui/src/lib.rs b/crates/re_data_ui/src/lib.rs
index 7e628df3c4e2c..fa9dd93e2c37f 100644
--- a/crates/re_data_ui/src/lib.rs
+++ b/crates/re_data_ui/src/lib.rs
@@ -17,7 +17,6 @@ mod component_path;
mod component_ui_registry;
mod data;
mod data_source;
-mod editors;
mod entity_db;
mod entity_path;
mod image;
diff --git a/crates/re_edit_ui/Cargo.toml b/crates/re_edit_ui/Cargo.toml
new file mode 100644
index 0000000000000..b6f6bb1f25497
--- /dev/null
+++ b/crates/re_edit_ui/Cargo.toml
@@ -0,0 +1,33 @@
+[package]
+description = "Provides ui editors for Rerun component data for registration with the Rerun Viewer component ui registry."
+authors.workspace = true
+edition.workspace = true
+homepage.workspace = true
+license.workspace = true
+name = "re_edit_ui"
+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_data_store.workspace = true
+re_entity_db.workspace = true
+re_log_types.workspace = true
+re_types = { workspace = true, features = [
+ "egui_plot", # Needed to draw marker shapes.
+] }
+re_types_blueprint.workspace = true
+re_ui.workspace = true
+re_viewer_context.workspace = true
+
+egui_plot.workspace = true
+egui.workspace = true
diff --git a/crates/re_edit_ui/README.md b/crates/re_edit_ui/README.md
new file mode 100644
index 0000000000000..2db50e9584a93
--- /dev/null
+++ b/crates/re_edit_ui/README.md
@@ -0,0 +1,10 @@
+# re_data_ui
+
+Part of the [`rerun`](https://github.com/rerun-io/rerun) family of crates.
+
+[](https://crates.io/crates/re_data_ui)
+[](https://docs.rs/re_data_ui)
+
+
+
+Provides ui editors for Rerun component data for registration with the Rerun Viewer component ui registry.
diff --git a/crates/re_data_ui/src/editors/corner2d.rs b/crates/re_edit_ui/src/corner2d.rs
similarity index 100%
rename from crates/re_data_ui/src/editors/corner2d.rs
rename to crates/re_edit_ui/src/corner2d.rs
diff --git a/crates/re_data_ui/src/editors/mod.rs b/crates/re_edit_ui/src/lib.rs
similarity index 98%
rename from crates/re_data_ui/src/editors/mod.rs
rename to crates/re_edit_ui/src/lib.rs
index 4c86de8bd8f0a..f6067c2cda82d 100644
--- a/crates/re_data_ui/src/editors/mod.rs
+++ b/crates/re_edit_ui/src/lib.rs
@@ -464,6 +464,10 @@ fn register_editor<'a, C>(
);
}
+/// Registers all editors of this crate in the component UI registry.
+///
+/// ⚠️ This is supposed to be the only export of this crate.
+/// This crate is meant to be a leaf crate in the viewer ecosystem and should only be used by [`re_viewer`] itself.
pub fn register_editors(registry: &mut re_viewer_context::ComponentUiRegistry) {
register_editor::(registry, default_color, edit_color_ui);
register_editor::(
diff --git a/crates/re_data_ui/src/editors/visible.rs b/crates/re_edit_ui/src/visible.rs
similarity index 100%
rename from crates/re_data_ui/src/editors/visible.rs
rename to crates/re_edit_ui/src/visible.rs
diff --git a/crates/re_viewer/Cargo.toml b/crates/re_viewer/Cargo.toml
index d393e84ea6b3b..7a189509afe8b 100644
--- a/crates/re_viewer/Cargo.toml
+++ b/crates/re_viewer/Cargo.toml
@@ -44,6 +44,7 @@ re_data_loader.workspace = true
re_data_source.workspace = true
re_data_store.workspace = true
re_data_ui.workspace = true
+re_edit_ui.workspace = true
re_entity_db = { workspace = true, features = ["serde"] }
re_error.workspace = true
re_format.workspace = true
diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs
index 699eb5c514a93..1ff03c4dcf76e 100644
--- a/crates/re_viewer/src/app.rs
+++ b/crates/re_viewer/src/app.rs
@@ -229,7 +229,8 @@ impl App {
let (command_sender, command_receiver) = command_channel();
- let component_ui_registry = re_data_ui::create_component_ui_registry();
+ let mut component_ui_registry = re_data_ui::create_component_ui_registry();
+ re_edit_ui::register_editors(&mut component_ui_registry);
// TODO(emilk): `Instant::MIN` when we have our own `Instant` that supports it.;
let long_time_ago = web_time::Instant::now()
diff --git a/crates/re_viewer_context/src/item_ui.rs b/crates/re_viewer_context/src/item_ui.rs
new file mode 100644
index 0000000000000..27130ea5ad942
--- /dev/null
+++ b/crates/re_viewer_context/src/item_ui.rs
@@ -0,0 +1,754 @@
+//! Basic ui elements & interaction for most `Item`.
+
+use crate::{HoverHighlight, Item, SpaceViewId, UiLayout, ViewerContext};
+use re_entity_db::{EntityTree, InstancePath};
+use re_log_types::{ApplicationId, ComponentPath, EntityPath, TimeInt, Timeline};
+use re_ui::{icons, list_item, SyntaxHighlighting};
+
+// TODO(andreas): This is where we want to go, but we need to figure out how get the [`SpaceViewClass`] from the `SpaceViewId`.
+// Simply pass in optional icons?
+//
+// Show a button to an [`Item`] with a given text.
+// pub fn item_button_to(
+// ctx: &ViewerContext<'_>,
+// ui: &mut egui::Ui,
+// item: &Item,
+// text: impl Into,
+// ) -> egui::Response {
+// match item {
+// Item::ComponentPath(component_path) => {
+// component_path_button_to(ctx, ui, text, component_path)
+// }
+// Item::SpaceView(space_view_id) => {
+// space_view_button_to(ctx, ui, text, *space_view_id, space_view_category)
+// }
+// Item::InstancePath(space_view_id, instance_path) => {
+// instance_path_button_to(ctx, ui, *space_view_id, instance_path, text)
+// }
+// }
+// }
+
+/// Show an entity path and make it selectable.
+pub fn entity_path_button(
+ ctx: &ViewerContext<'_>,
+ query: &re_data_store::LatestAtQuery,
+ db: &re_entity_db::EntityDb,
+ ui: &mut egui::Ui,
+ space_view_id: Option,
+ entity_path: &EntityPath,
+) -> egui::Response {
+ instance_path_button_to(
+ ctx,
+ query,
+ db,
+ ui,
+ space_view_id,
+ &InstancePath::entity_all(entity_path.clone()),
+ entity_path.syntax_highlighted(ui.style()),
+ )
+}
+
+/// Show the different parts of an entity path and make them selectable.
+pub fn entity_path_parts_buttons(
+ ctx: &ViewerContext<'_>,
+ query: &re_data_store::LatestAtQuery,
+ db: &re_entity_db::EntityDb,
+ ui: &mut egui::Ui,
+ space_view_id: Option,
+ entity_path: &EntityPath,
+) -> egui::Response {
+ let with_icon = false; // too much noise with icons in a path
+
+ ui.horizontal(|ui| {
+ ui.spacing_mut().item_spacing.x = 2.0;
+
+ // Show one single icon up-front instead:
+ let instance_path = InstancePath::entity_all(entity_path.clone());
+ ui.add(instance_path_icon(&query.timeline(), db, &instance_path).as_image());
+
+ let mut accumulated = Vec::new();
+ for part in entity_path.iter() {
+ accumulated.push(part.clone());
+
+ ui.strong("/");
+ instance_path_button_to_ex(
+ ctx,
+ query,
+ db,
+ ui,
+ space_view_id,
+ &InstancePath::entity_all(accumulated.clone().into()),
+ part.syntax_highlighted(ui.style()),
+ with_icon,
+ );
+ }
+ })
+ .response
+}
+
+/// Show an entity path and make it selectable.
+pub fn entity_path_button_to(
+ ctx: &ViewerContext<'_>,
+ query: &re_data_store::LatestAtQuery,
+ db: &re_entity_db::EntityDb,
+ ui: &mut egui::Ui,
+ space_view_id: Option,
+ entity_path: &EntityPath,
+ text: impl Into,
+) -> egui::Response {
+ instance_path_button_to(
+ ctx,
+ query,
+ db,
+ ui,
+ space_view_id,
+ &InstancePath::entity_all(entity_path.clone()),
+ text,
+ )
+}
+
+/// Show an instance id and make it selectable.
+pub fn instance_path_button(
+ ctx: &ViewerContext<'_>,
+ query: &re_data_store::LatestAtQuery,
+ db: &re_entity_db::EntityDb,
+ ui: &mut egui::Ui,
+ space_view_id: Option,
+ instance_path: &InstancePath,
+) -> egui::Response {
+ instance_path_button_to(
+ ctx,
+ query,
+ db,
+ ui,
+ space_view_id,
+ instance_path,
+ instance_path.syntax_highlighted(ui.style()),
+ )
+}
+
+/// Return the instance path icon.
+///
+/// The choice of icon is based on whether the instance is "empty" as in hasn't any logged component
+/// _on the current timeline_.
+pub fn instance_path_icon(
+ timeline: &re_data_store::Timeline,
+ db: &re_entity_db::EntityDb,
+ instance_path: &InstancePath,
+) -> &'static icons::Icon {
+ if instance_path.is_all() {
+ // It is an entity path
+ if db
+ .store()
+ .all_components(timeline, &instance_path.entity_path)
+ .is_some()
+ {
+ &icons::ENTITY
+ } else {
+ &icons::ENTITY_EMPTY
+ }
+ } else {
+ // An instance path
+ &icons::ENTITY
+ }
+}
+
+/// The current time query, based on the current time control and an `entity_path`
+///
+/// If the user is inspecting the blueprint, and the `entity_path` is on the blueprint
+/// timeline, then use the blueprint. Otherwise, use the recording.
+// TODO(jleibs): Ideally this wouldn't be necessary and we could make the assessment
+// directly from the entity_path.
+pub fn guess_query_and_db_for_selected_entity<'a>(
+ ctx: &'a ViewerContext<'_>,
+ entity_path: &EntityPath,
+) -> (re_data_store::LatestAtQuery, &'a re_entity_db::EntityDb) {
+ if ctx.app_options.inspect_blueprint_timeline
+ && ctx.store_context.blueprint.is_logged_entity(entity_path)
+ {
+ (
+ ctx.blueprint_cfg.time_ctrl.read().current_query(),
+ ctx.store_context.blueprint,
+ )
+ } else {
+ (
+ ctx.rec_cfg.time_ctrl.read().current_query(),
+ ctx.recording(),
+ )
+ }
+}
+
+pub fn guess_instance_path_icon(
+ ctx: &ViewerContext<'_>,
+ instance_path: &InstancePath,
+) -> &'static icons::Icon {
+ let (query, db) = guess_query_and_db_for_selected_entity(ctx, &instance_path.entity_path);
+ instance_path_icon(&query.timeline(), db, instance_path)
+}
+
+/// Show an instance id and make it selectable.
+pub fn instance_path_button_to(
+ ctx: &ViewerContext<'_>,
+ query: &re_data_store::LatestAtQuery,
+ db: &re_entity_db::EntityDb,
+ ui: &mut egui::Ui,
+ space_view_id: Option,
+ instance_path: &InstancePath,
+ text: impl Into,
+) -> egui::Response {
+ instance_path_button_to_ex(ctx, query, db, ui, space_view_id, instance_path, text, true)
+}
+
+/// Show an instance id and make it selectable.
+#[allow(clippy::too_many_arguments)]
+fn instance_path_button_to_ex(
+ ctx: &ViewerContext<'_>,
+ query: &re_data_store::LatestAtQuery,
+ db: &re_entity_db::EntityDb,
+ ui: &mut egui::Ui,
+ space_view_id: Option,
+ instance_path: &InstancePath,
+ text: impl Into,
+ with_icon: bool,
+) -> egui::Response {
+ let item = if let Some(space_view_id) = space_view_id {
+ Item::DataResult(space_view_id, instance_path.clone())
+ } else {
+ Item::InstancePath(instance_path.clone())
+ };
+
+ let response = if with_icon {
+ ctx.re_ui.selectable_label_with_icon(
+ ui,
+ instance_path_icon(&query.timeline(), db, instance_path),
+ text,
+ ctx.selection().contains_item(&item),
+ re_ui::LabelStyle::Normal,
+ )
+ } else {
+ ui.selectable_label(ctx.selection().contains_item(&item), text)
+ };
+
+ let response = response.on_hover_ui(|ui| {
+ instance_hover_card_ui(ui, ctx, query, db, instance_path);
+ });
+
+ cursor_interact_with_selectable(ctx, response, item)
+}
+
+/// Show the different parts of an instance path and make them selectable.
+pub fn instance_path_parts_buttons(
+ ctx: &ViewerContext<'_>,
+ query: &re_data_store::LatestAtQuery,
+ db: &re_entity_db::EntityDb,
+ ui: &mut egui::Ui,
+ space_view_id: Option,
+ instance_path: &InstancePath,
+) -> egui::Response {
+ let with_icon = false; // too much noise with icons in a path
+
+ ui.horizontal(|ui| {
+ ui.spacing_mut().item_spacing.x = 2.0;
+
+ // Show one single icon up-front instead:
+ ui.add(instance_path_icon(&query.timeline(), db, instance_path).as_image());
+
+ let mut accumulated = Vec::new();
+ for part in instance_path.entity_path.iter() {
+ accumulated.push(part.clone());
+
+ ui.strong("/");
+ instance_path_button_to_ex(
+ ctx,
+ query,
+ db,
+ ui,
+ space_view_id,
+ &InstancePath::entity_all(accumulated.clone().into()),
+ part.syntax_highlighted(ui.style()),
+ with_icon,
+ );
+ }
+
+ if !instance_path.instance.is_all() {
+ ui.weak("[");
+ instance_path_button_to_ex(
+ ctx,
+ query,
+ db,
+ ui,
+ space_view_id,
+ instance_path,
+ instance_path.instance.syntax_highlighted(ui.style()),
+ with_icon,
+ );
+ ui.weak("]");
+ }
+ })
+ .response
+}
+
+fn entity_tree_stats_ui(ui: &mut egui::Ui, timeline: &Timeline, tree: &EntityTree) {
+ use re_format::format_bytes;
+
+ // Show total bytes used in whole subtree
+ let total_bytes = tree.subtree.data_bytes();
+
+ let subtree_caveat = if tree.children.is_empty() {
+ ""
+ } else {
+ " (including subtree)"
+ };
+
+ if total_bytes == 0 {
+ return;
+ }
+
+ let mut data_rate = None;
+
+ // Try to estimate data-rate
+ if let Some(time_histogram) = tree.subtree.time_histogram.get(timeline) {
+ // `num_events` is approximate - we could be logging a Tensor image and a transform
+ // at _almost_ approximately the same time, but it should only count as one fence-post.
+ let num_events = time_histogram.total_count(); // TODO(emilk): we should ask the histogram to count the number of non-zero keys instead.
+
+ if let (Some(min_time), Some(max_time)) =
+ (time_histogram.min_key(), time_histogram.max_key())
+ {
+ if min_time < max_time && 1 < num_events {
+ // Let's do our best to avoid fencepost errors.
+ // If we log 1 MiB once every second, then after three
+ // events we have a span of 2 seconds, and 3 MiB,
+ // but the data rate is still 1 MiB/s.
+ //
+ // <-----2 sec----->
+ // t: 0s 1s 2s
+ // data: 1MiB 1MiB 1MiB
+
+ let duration = max_time - min_time;
+
+ let mut bytes_per_time = total_bytes as f64 / duration as f64;
+
+ // Fencepost adjustment:
+ bytes_per_time *= (num_events - 1) as f64 / num_events as f64;
+
+ data_rate = Some(match timeline.typ() {
+ re_log_types::TimeType::Time => {
+ let bytes_per_second = 1e9 * bytes_per_time;
+
+ format!(
+ "{}/s in '{}'",
+ format_bytes(bytes_per_second),
+ timeline.name()
+ )
+ }
+
+ re_log_types::TimeType::Sequence => {
+ format!("{} / {}", format_bytes(bytes_per_time), timeline.name())
+ }
+ });
+ }
+ }
+ }
+
+ if let Some(data_rate) = data_rate {
+ ui.label(format!(
+ "Using {}{subtree_caveat} ≈ {}",
+ format_bytes(total_bytes as f64),
+ data_rate
+ ));
+ } else {
+ ui.label(format!(
+ "Using {}{subtree_caveat}",
+ format_bytes(total_bytes as f64)
+ ));
+ }
+}
+
+/// Show a component path and make it selectable.
+pub fn component_path_button(
+ ctx: &ViewerContext<'_>,
+ ui: &mut egui::Ui,
+ component_path: &ComponentPath,
+ db: &re_entity_db::EntityDb,
+) -> egui::Response {
+ component_path_button_to(
+ ctx,
+ ui,
+ component_path.component_name.short_name(),
+ component_path,
+ db,
+ )
+}
+
+/// Show a component path and make it selectable.
+pub fn component_path_button_to(
+ ctx: &ViewerContext<'_>,
+ ui: &mut egui::Ui,
+ text: impl Into,
+ component_path: &ComponentPath,
+ db: &re_entity_db::EntityDb,
+) -> egui::Response {
+ let item = Item::ComponentPath(component_path.clone());
+ let is_static = db.is_component_static(component_path).unwrap_or_default();
+ let icon = if is_static {
+ &icons::COMPONENT_STATIC
+ } else {
+ &icons::COMPONENT_TEMPORAL
+ };
+ let response = ctx.re_ui.selectable_label_with_icon(
+ ui,
+ icon,
+ text,
+ ctx.selection().contains_item(&item),
+ re_ui::LabelStyle::Normal,
+ );
+
+ let response = response.on_hover_ui(|ui| {
+ // TODO(egui#4471): better tooltip size management
+ ui.set_max_width(250.0);
+ ui.style_mut().wrap = Some(false);
+
+ // wrap lone item
+ list_item::list_item_scope(ui, "component_path_tooltip", |ui| {
+ list_item::ListItem::new(ctx.re_ui)
+ .interactive(false)
+ .show_flat(
+ ui,
+ list_item::LabelContent::new(if is_static {
+ "Static component"
+ } else {
+ "Temporal component"
+ })
+ .with_icon(icon)
+ .exact_width(true),
+ );
+ });
+
+ ui.label(format!(
+ "Full name: {}",
+ component_path.component_name.full_name()
+ ));
+ });
+
+ cursor_interact_with_selectable(ctx, response, item)
+}
+
+pub fn data_blueprint_button_to(
+ ctx: &ViewerContext<'_>,
+ query: &re_data_store::LatestAtQuery,
+ db: &re_entity_db::EntityDb,
+ ui: &mut egui::Ui,
+ text: impl Into,
+ space_view_id: SpaceViewId,
+ entity_path: &EntityPath,
+) -> egui::Response {
+ let item = Item::DataResult(space_view_id, InstancePath::entity_all(entity_path.clone()));
+ let response = ui
+ .selectable_label(ctx.selection().contains_item(&item), text)
+ .on_hover_ui(|ui| {
+ entity_hover_card_ui(ui, ctx, query, db, entity_path);
+ });
+ cursor_interact_with_selectable(ctx, response, item)
+}
+
+pub fn time_button(
+ ctx: &ViewerContext<'_>,
+ ui: &mut egui::Ui,
+ timeline: &Timeline,
+ value: TimeInt,
+) -> egui::Response {
+ let is_selected = ctx
+ .rec_cfg
+ .time_ctrl
+ .read()
+ .is_time_selected(timeline, value);
+
+ let response = ui.selectable_label(
+ is_selected,
+ timeline.typ().format(value, ctx.app_options.time_zone),
+ );
+ if response.clicked() {
+ ctx.rec_cfg
+ .time_ctrl
+ .write()
+ .set_timeline_and_time(*timeline, value);
+ ctx.rec_cfg.time_ctrl.write().pause();
+ }
+ response
+}
+
+pub fn timeline_button(
+ ctx: &ViewerContext<'_>,
+ ui: &mut egui::Ui,
+ timeline: &Timeline,
+) -> egui::Response {
+ timeline_button_to(ctx, ui, timeline.name().to_string(), timeline)
+}
+
+pub fn timeline_button_to(
+ ctx: &ViewerContext<'_>,
+ ui: &mut egui::Ui,
+ text: impl Into,
+ timeline: &Timeline,
+) -> egui::Response {
+ let is_selected = ctx.rec_cfg.time_ctrl.read().timeline() == timeline;
+
+ let response = ui
+ .selectable_label(is_selected, text)
+ .on_hover_text("Click to switch to this timeline");
+ if response.clicked() {
+ let mut time_ctrl = ctx.rec_cfg.time_ctrl.write();
+ time_ctrl.set_timeline(*timeline);
+ time_ctrl.pause();
+ }
+ response
+}
+
+// TODO(andreas): Move elsewhere, this is not directly part of the item_ui.
+pub fn cursor_interact_with_selectable(
+ ctx: &ViewerContext<'_>,
+ response: egui::Response,
+ item: Item,
+) -> egui::Response {
+ let is_item_hovered =
+ ctx.selection_state().highlight_for_ui_element(&item) == HoverHighlight::Hovered;
+
+ ctx.select_hovered_on_click(&response, item);
+ // TODO(andreas): How to deal with shift click for selecting ranges?
+
+ if is_item_hovered {
+ response.highlight()
+ } else {
+ response
+ }
+}
+
+/// Displays the "hover card" (i.e. big tooltip) for an instance or an entity.
+///
+/// The entity hover card is displayed if the provided instance path doesn't refer to a specific
+/// instance.
+pub fn instance_hover_card_ui(
+ ui: &mut egui::Ui,
+ ctx: &ViewerContext<'_>,
+ query: &re_data_store::LatestAtQuery,
+ db: &re_entity_db::EntityDb,
+ instance_path: &InstancePath,
+) {
+ if !ctx.recording().is_known_entity(&instance_path.entity_path) {
+ ui.label("Unknown entity.");
+ return;
+ }
+
+ let subtype_string = if instance_path.instance.is_all() {
+ "Entity"
+ } else {
+ "Entity instance"
+ };
+ ui.strong(subtype_string);
+ ui.label(instance_path.syntax_highlighted(ui.style()));
+
+ // TODO(emilk): give data_ui an alternate "everything on this timeline" query?
+ // Then we can move the size view into `data_ui`.
+
+ if instance_path.instance.is_all() {
+ if let Some(subtree) = ctx.recording().tree().subtree(&instance_path.entity_path) {
+ entity_tree_stats_ui(ui, &query.timeline(), subtree);
+ }
+ } else {
+ // TODO(emilk): per-component stats
+ }
+
+ instance_path.data_ui(ctx, ui, UiLayout::Tooltip, query, db);
+}
+
+/// Displays the "hover card" (i.e. big tooltip) for an entity.
+pub fn entity_hover_card_ui(
+ ui: &mut egui::Ui,
+ ctx: &ViewerContext<'_>,
+ query: &re_data_store::LatestAtQuery,
+ db: &re_entity_db::EntityDb,
+ entity_path: &EntityPath,
+) {
+ let instance_path = InstancePath::entity_all(entity_path.clone());
+ instance_hover_card_ui(ui, ctx, query, db, &instance_path);
+}
+
+pub fn app_id_button_ui(
+ ctx: &ViewerContext<'_>,
+ ui: &mut egui::Ui,
+ app_id: &ApplicationId,
+) -> egui::Response {
+ let item = Item::AppId(app_id.clone());
+
+ let response = ctx.re_ui.selectable_label_with_icon(
+ ui,
+ &icons::APPLICATION,
+ app_id.to_string(),
+ ctx.selection().contains_item(&item),
+ re_ui::LabelStyle::Normal,
+ );
+
+ let response = response.on_hover_ui(|ui| {
+ app_id.data_ui(
+ ctx,
+ ui,
+ UiLayout::Tooltip,
+ &ctx.current_query(), // unused
+ ctx.recording(), // unused
+ );
+ });
+
+ cursor_interact_with_selectable(ctx, response, item)
+}
+
+pub fn data_source_button_ui(
+ ctx: &ViewerContext<'_>,
+ ui: &mut egui::Ui,
+ data_source: &re_smart_channel::SmartChannelSource,
+) -> egui::Response {
+ let item = Item::DataSource(data_source.clone());
+
+ let response = ctx.re_ui.selectable_label_with_icon(
+ ui,
+ &icons::DATA_SOURCE,
+ data_source.to_string(),
+ ctx.selection().contains_item(&item),
+ re_ui::LabelStyle::Normal,
+ );
+
+ let response = response.on_hover_ui(|ui| {
+ data_source.data_ui(
+ ctx,
+ ui,
+ UiLayout::Tooltip,
+ &ctx.current_query(),
+ ctx.recording(), // unused
+ );
+ });
+
+ cursor_interact_with_selectable(ctx, response, item)
+}
+
+/// This uses [`list_item::ListItem::show_hierarchical`], meaning it comes with built-in
+/// indentation.
+pub fn store_id_button_ui(
+ ctx: &ViewerContext<'_>,
+ ui: &mut egui::Ui,
+ store_id: &re_log_types::StoreId,
+) {
+ if let Some(entity_db) = ctx.store_context.bundle.get(store_id) {
+ entity_db_button_ui(ctx, ui, entity_db, true);
+ } else {
+ ui.label(store_id.to_string());
+ }
+}
+
+/// Show button for a store (recording or blueprint).
+///
+/// You can set `include_app_id` to hide the App Id, but usually you want to show it.
+///
+/// This uses [`list_item::ListItem::show_hierarchical`], meaning it comes with built-in
+/// indentation.
+pub fn entity_db_button_ui(
+ ctx: &ViewerContext<'_>,
+ ui: &mut egui::Ui,
+ entity_db: &re_entity_db::EntityDb,
+ include_app_id: bool,
+) {
+ use crate::{SystemCommand, SystemCommandSender as _};
+ use re_types::SizeBytes as _;
+
+ let app_id_prefix = if include_app_id {
+ entity_db
+ .app_id()
+ .map_or(String::default(), |app_id| format!("{app_id} - "))
+ } else {
+ String::default()
+ };
+
+ let creation_time = entity_db
+ .store_info()
+ .and_then(|info| {
+ info.started
+ .format_time_custom("[hour]:[minute]:[second]", ctx.app_options.time_zone)
+ })
+ .unwrap_or("".to_owned());
+
+ let size = re_format::format_bytes(entity_db.total_size_bytes() as _);
+ let title = format!("{app_id_prefix}{creation_time} - {size}");
+
+ let store_id = entity_db.store_id().clone();
+ let item = crate::Item::StoreId(store_id.clone());
+
+ let icon = match entity_db.store_kind() {
+ re_log_types::StoreKind::Recording => &icons::RECORDING,
+ re_log_types::StoreKind::Blueprint => &icons::BLUEPRINT,
+ };
+
+ let item_content = list_item::LabelContent::new(title)
+ .with_icon_fn(|_re_ui, ui, rect, visuals| {
+ // Color icon based on whether this is the active recording or not:
+ let color = if ctx.store_context.is_active(&store_id) {
+ visuals.fg_stroke.color
+ } else {
+ ui.visuals().widgets.noninteractive.fg_stroke.color
+ };
+ icon.as_image().tint(color).paint_at(ui, rect);
+ })
+ .with_buttons(|re_ui, ui| {
+ // Close-button:
+ let resp =
+ re_ui
+ .small_icon_button(ui, &icons::REMOVE)
+ .on_hover_text(match store_id.kind {
+ re_log_types::StoreKind::Recording => {
+ "Close this recording (unsaved data will be lost)"
+ }
+ re_log_types::StoreKind::Blueprint => {
+ "Close this blueprint (unsaved data will be lost)"
+ }
+ });
+ if resp.clicked() {
+ ctx.command_sender
+ .send_system(SystemCommand::CloseStore(store_id.clone()));
+ }
+ resp
+ });
+
+ let mut list_item =
+ list_item::ListItem::new(ctx.re_ui).selected(ctx.selection().contains_item(&item));
+
+ if ctx.hovered().contains_item(&item) {
+ list_item = list_item.force_hovered(true);
+ }
+
+ let response = list_item::list_item_scope(ui, "entity db button", |ui| {
+ list_item
+ .show_hierarchical(ui, item_content)
+ .on_hover_ui(|ui| {
+ entity_db.data_ui(ctx, ui, UiLayout::Tooltip, &ctx.current_query(), entity_db);
+ })
+ });
+
+ if response.hovered() {
+ ctx.selection_state().set_hovered(item.clone());
+ }
+
+ if response.clicked() {
+ // When we click on a recording, we directly activate it. This is safe to do because
+ // it's non-destructive and recordings are immutable. Switching back is easy.
+ // We don't do the same thing for blueprints as swapping them can be much more disruptive.
+ // It is much less obvious how to undo a blueprint switch and what happened to your original
+ // blueprint.
+ // TODO(jleibs): We should still have an `Activate this Blueprint` button in the selection panel
+ // for the blueprint.
+ if store_id.kind == re_log_types::StoreKind::Recording {
+ ctx.command_sender
+ .send_system(SystemCommand::ActivateRecording(store_id.clone()));
+ }
+
+ ctx.command_sender
+ .send_system(SystemCommand::SetSelection(item));
+ }
+}