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. + +[![Latest version](https://img.shields.io/crates/v/re_data_ui.svg)](https://crates.io/crates/re_data_ui) +[![Documentation](https://docs.rs/re_data_ui/badge.svg)](https://docs.rs/re_data_ui) +![MIT](https://img.shields.io/badge/license-MIT-blue.svg) +![Apache](https://img.shields.io/badge/license-Apache-blue.svg) + +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)); + } +}