diff --git a/Cargo.lock b/Cargo.lock index 3a77741792c0..13ed1656b5ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1923,7 +1923,7 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "ecolor" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=c5ac7d301a90afbaec843ee04fb43d8a0956cc90#c5ac7d301a90afbaec843ee04fb43d8a0956cc90" +source = "git+https://github.com/emilk/egui.git?rev=577ee8d22810752540636febac5660a5119c6550#577ee8d22810752540636febac5660a5119c6550" dependencies = [ "bytemuck", "color-hex", @@ -1940,7 +1940,7 @@ checksum = "18aade80d5e09429040243ce1143ddc08a92d7a22820ac512610410a4dd5214f" [[package]] name = "eframe" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=c5ac7d301a90afbaec843ee04fb43d8a0956cc90#c5ac7d301a90afbaec843ee04fb43d8a0956cc90" +source = "git+https://github.com/emilk/egui.git?rev=577ee8d22810752540636febac5660a5119c6550#577ee8d22810752540636febac5660a5119c6550" dependencies = [ "ahash", "bytemuck", @@ -1979,7 +1979,7 @@ dependencies = [ [[package]] name = "egui" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=c5ac7d301a90afbaec843ee04fb43d8a0956cc90#c5ac7d301a90afbaec843ee04fb43d8a0956cc90" +source = "git+https://github.com/emilk/egui.git?rev=577ee8d22810752540636febac5660a5119c6550#577ee8d22810752540636febac5660a5119c6550" dependencies = [ "accesskit", "ahash", @@ -1996,7 +1996,7 @@ dependencies = [ [[package]] name = "egui-wgpu" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=c5ac7d301a90afbaec843ee04fb43d8a0956cc90#c5ac7d301a90afbaec843ee04fb43d8a0956cc90" +source = "git+https://github.com/emilk/egui.git?rev=577ee8d22810752540636febac5660a5119c6550#577ee8d22810752540636febac5660a5119c6550" dependencies = [ "ahash", "bytemuck", @@ -2015,7 +2015,7 @@ dependencies = [ [[package]] name = "egui-winit" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=c5ac7d301a90afbaec843ee04fb43d8a0956cc90#c5ac7d301a90afbaec843ee04fb43d8a0956cc90" +source = "git+https://github.com/emilk/egui.git?rev=577ee8d22810752540636febac5660a5119c6550#577ee8d22810752540636febac5660a5119c6550" dependencies = [ "accesskit_winit", "ahash", @@ -2057,7 +2057,7 @@ dependencies = [ [[package]] name = "egui_extras" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=c5ac7d301a90afbaec843ee04fb43d8a0956cc90#c5ac7d301a90afbaec843ee04fb43d8a0956cc90" +source = "git+https://github.com/emilk/egui.git?rev=577ee8d22810752540636febac5660a5119c6550#577ee8d22810752540636febac5660a5119c6550" dependencies = [ "ahash", "egui", @@ -2074,7 +2074,7 @@ dependencies = [ [[package]] name = "egui_glow" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=c5ac7d301a90afbaec843ee04fb43d8a0956cc90#c5ac7d301a90afbaec843ee04fb43d8a0956cc90" +source = "git+https://github.com/emilk/egui.git?rev=577ee8d22810752540636febac5660a5119c6550#577ee8d22810752540636febac5660a5119c6550" dependencies = [ "ahash", "bytemuck", @@ -2092,7 +2092,7 @@ dependencies = [ [[package]] name = "egui_kittest" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=c5ac7d301a90afbaec843ee04fb43d8a0956cc90#c5ac7d301a90afbaec843ee04fb43d8a0956cc90" +source = "git+https://github.com/emilk/egui.git?rev=577ee8d22810752540636febac5660a5119c6550#577ee8d22810752540636febac5660a5119c6550" dependencies = [ "dify", "egui", @@ -2161,7 +2161,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "emath" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=c5ac7d301a90afbaec843ee04fb43d8a0956cc90#c5ac7d301a90afbaec843ee04fb43d8a0956cc90" +source = "git+https://github.com/emilk/egui.git?rev=577ee8d22810752540636febac5660a5119c6550#577ee8d22810752540636febac5660a5119c6550" dependencies = [ "bytemuck", "serde", @@ -2277,7 +2277,7 @@ dependencies = [ [[package]] name = "epaint" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=c5ac7d301a90afbaec843ee04fb43d8a0956cc90#c5ac7d301a90afbaec843ee04fb43d8a0956cc90" +source = "git+https://github.com/emilk/egui.git?rev=577ee8d22810752540636febac5660a5119c6550#577ee8d22810752540636febac5660a5119c6550" dependencies = [ "ab_glyph", "ahash", @@ -2296,7 +2296,7 @@ dependencies = [ [[package]] name = "epaint_default_fonts" version = "0.29.1" -source = "git+https://github.com/emilk/egui.git?rev=c5ac7d301a90afbaec843ee04fb43d8a0956cc90#c5ac7d301a90afbaec843ee04fb43d8a0956cc90" +source = "git+https://github.com/emilk/egui.git?rev=577ee8d22810752540636febac5660a5119c6550#577ee8d22810752540636febac5660a5119c6550" [[package]] name = "equivalent" diff --git a/Cargo.toml b/Cargo.toml index d027a3a764ab..3a0ad88ae8cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -559,13 +559,13 @@ significant_drop_tightening = "allow" # An update of parking_lot made this trigg # As a last resport, patch with a commit to our own repository. # ALWAYS document what PR the commit hash is part of, or when it was merged into the upstream trunk. -ecolor = { git = "https://github.com/emilk/egui.git", rev = "c5ac7d301a90afbaec843ee04fb43d8a0956cc90" } # egui master 2024-12-03 -eframe = { git = "https://github.com/emilk/egui.git", rev = "c5ac7d301a90afbaec843ee04fb43d8a0956cc90" } # egui master 2024-12-03 -egui = { git = "https://github.com/emilk/egui.git", rev = "c5ac7d301a90afbaec843ee04fb43d8a0956cc90" } # egui master 2024-12-03 -egui_extras = { git = "https://github.com/emilk/egui.git", rev = "c5ac7d301a90afbaec843ee04fb43d8a0956cc90" } # egui master 2024-12-03 -egui_kittest = { git = "https://github.com/emilk/egui.git", rev = "c5ac7d301a90afbaec843ee04fb43d8a0956cc90" } # egui master 2024-12-03 -egui-wgpu = { git = "https://github.com/emilk/egui.git", rev = "c5ac7d301a90afbaec843ee04fb43d8a0956cc90" } # egui master 2024-12-03 -emath = { git = "https://github.com/emilk/egui.git", rev = "c5ac7d301a90afbaec843ee04fb43d8a0956cc90" } # egui master 2024-12-03 +ecolor = { git = "https://github.com/emilk/egui.git", rev = "577ee8d22810752540636febac5660a5119c6550" } # egui master 2024-12-04 +eframe = { git = "https://github.com/emilk/egui.git", rev = "577ee8d22810752540636febac5660a5119c6550" } # egui master 2024-12-04 +egui = { git = "https://github.com/emilk/egui.git", rev = "577ee8d22810752540636febac5660a5119c6550" } # egui master 2024-12-04 +egui_extras = { git = "https://github.com/emilk/egui.git", rev = "577ee8d22810752540636febac5660a5119c6550" } # egui master 2024-12-04 +egui_kittest = { git = "https://github.com/emilk/egui.git", rev = "577ee8d22810752540636febac5660a5119c6550" } # egui master 2024-12-04 +egui-wgpu = { git = "https://github.com/emilk/egui.git", rev = "577ee8d22810752540636febac5660a5119c6550" } # egui master 2024-12-04 +emath = { git = "https://github.com/emilk/egui.git", rev = "577ee8d22810752540636febac5660a5119c6550" } # egui master 2024-12-04 # Useful while developing: # ecolor = { path = "../../egui/crates/ecolor" } diff --git a/crates/store/re_entity_db/src/instance_path.rs b/crates/store/re_entity_db/src/instance_path.rs index c11e86676187..96145eb62bb8 100644 --- a/crates/store/re_entity_db/src/instance_path.rs +++ b/crates/store/re_entity_db/src/instance_path.rs @@ -72,6 +72,15 @@ impl InstancePath { instance: self.instance, } } + + /// Human-readable description of the kind + pub fn kind(&self) -> &'static str { + if self.instance.is_specific() { + "Entity instance" + } else { + "Entity" + } + } } impl std::fmt::Display for InstancePath { diff --git a/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs b/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs index c9a2a6798e59..b6ea2ae356d4 100644 --- a/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs +++ b/crates/viewer/re_blueprint_tree/src/blueprint_tree.rs @@ -9,7 +9,7 @@ use re_log_types::EntityPath; use re_types::blueprint::components::Visible; use re_ui::{drag_and_drop::DropTarget, list_item, ContextExt as _, DesignTokens, UiExt as _}; use re_viewer_context::{ - contents_name_style, icon_for_container_kind, CollapseScope, Contents, DataResultTree, + contents_name_style, icon_for_container_kind, CollapseScope, Contents, DataResultNodeOrPath, SystemCommandSender, }; use re_viewer_context::{ @@ -18,35 +18,6 @@ use re_viewer_context::{ use re_viewport_blueprint::ui::show_add_space_view_or_container_modal; use re_viewport_blueprint::{SpaceViewBlueprint, ViewportBlueprint}; -enum DataResultNodeOrPath<'a> { - Path(&'a EntityPath), - DataResultNode(&'a DataResultNode), -} - -impl<'a> DataResultNodeOrPath<'a> { - fn from_path_lookup(result_tree: &'a DataResultTree, path: &'a EntityPath) -> Self { - result_tree - .lookup_node_by_path(path) - .map_or(DataResultNodeOrPath::Path(path), |node| { - DataResultNodeOrPath::DataResultNode(node) - }) - } - - fn path(&self) -> &'a EntityPath { - match self { - DataResultNodeOrPath::Path(path) => path, - DataResultNodeOrPath::DataResultNode(node) => &node.data_result.entity_path, - } - } - - fn data_result_node(&self) -> Option<&'a DataResultNode> { - match self { - DataResultNodeOrPath::Path(_) => None, - DataResultNodeOrPath::DataResultNode(node) => Some(node), - } - } -} - /// Holds the state of the blueprint tree UI. #[derive(Default)] pub struct BlueprintTree { diff --git a/crates/viewer/re_data_ui/src/item_ui.rs b/crates/viewer/re_data_ui/src/item_ui.rs index 024f57baaf63..0bafd68873a2 100644 --- a/crates/viewer/re_data_ui/src/item_ui.rs +++ b/crates/viewer/re_data_ui/src/item_ui.rs @@ -62,30 +62,43 @@ pub fn entity_path_parts_buttons( space_view_id: Option, entity_path: &EntityPath, ) -> egui::Response { - let with_icon = false; // too much noise with icons in a path + let with_individual_icons = false; // too much noise with icons in a path ui.horizontal(|ui| { - ui.spacing_mut().item_spacing.x = 2.0; + { + 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()); + // The last part points to the selected entity, but that's ugly, so remove the highlight: + let visuals = ui.visuals_mut(); + visuals.selection.bg_fill = egui::Color32::TRANSPARENT; + visuals.selection.stroke = visuals.widgets.inactive.fg_stroke; + } - let mut accumulated = Vec::new(); - for part in entity_path.iter() { - accumulated.push(part.clone()); + if !with_individual_icons { + // 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()); + } + if entity_path.is_root() { 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, - ); + } else { + 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_individual_icons, + ); + } } }) .response diff --git a/crates/viewer/re_selection_panel/src/item_heading_with_breadcrumbs.rs b/crates/viewer/re_selection_panel/src/item_heading_with_breadcrumbs.rs new file mode 100644 index 000000000000..ec9280376111 --- /dev/null +++ b/crates/viewer/re_selection_panel/src/item_heading_with_breadcrumbs.rs @@ -0,0 +1,317 @@ +//! The heading of each item in the selection panel. +//! +//! It consists of a blue background (selected background color), +//! and within it there are "bread-crumbs" that show the hierarchy of the item. +//! +//! A > B > C > D > item +//! +//! Each bread-crumb is just an icon or a letter. +//! The item is an icon and a name. +//! Each bread-crumb is clickable, as is the last item. +//! +//! The bread crumbs hierarchy should be identical to the hierarchy in the +//! either the blueprint tree panel, or the streams/time panel. + +use re_chunk::EntityPath; +use re_data_ui::item_ui::{cursor_interact_with_selectable, guess_instance_path_icon}; +use re_entity_db::InstancePath; +use re_log_types::EntityPathPart; +use re_ui::{icons, list_item, DesignTokens, SyntaxHighlighting, UiExt as _}; +use re_viewer_context::{Contents, Item, SpaceViewId, ViewerContext}; +use re_viewport_blueprint::ViewportBlueprint; + +use crate::item_title::ItemTitle; + +const ICON_SCALE: f32 = 0.5; // Because we save all icons as 2x + +/// We show this above each item section +pub fn item_heading_with_breadcrumbs( + ctx: &ViewerContext<'_>, + viewport: &ViewportBlueprint, + ui: &mut egui::Ui, + item: &Item, +) { + re_tracing::profile_function!(); + + ui.list_item() + .with_height(DesignTokens::title_bar_height()) + .interactive(false) + .selected(true) + .show_flat( + ui, + list_item::CustomContent::new(|ui, context| { + ui.allocate_new_ui( + egui::UiBuilder::new() + .max_rect(context.rect) + .layout(egui::Layout::left_to_right(egui::Align::Center)), + |ui| { + ui.spacing_mut().item_spacing.x = 4.0; + + { + // No background rectangles, even for hovered items + let visuals = ui.visuals_mut(); + visuals.widgets.active.bg_fill = egui::Color32::TRANSPARENT; + visuals.widgets.active.weak_bg_fill = egui::Color32::TRANSPARENT; + visuals.widgets.hovered.bg_fill = egui::Color32::TRANSPARENT; + visuals.widgets.hovered.weak_bg_fill = egui::Color32::TRANSPARENT; + } + + // First the C>R>U>M>B>S> + { + let previous_style = ui.style().clone(); + // Dimmer colors for breadcrumbs + let visuals = ui.visuals_mut(); + visuals.widgets.inactive.fg_stroke.color = egui::hex_color!("#6A8CD0"); // TODO(#3133): use design tokens + item_bread_crumbs_ui(ctx, viewport, ui, item); + ui.set_style(previous_style); + } + + // Then the full name of the main item: + last_part_of_item_heading(ctx, viewport, ui, item); + }, + ); + }), + ); +} + +// Show the bread crumbs leading to (but not including) the final item. +fn item_bread_crumbs_ui( + ctx: &ViewerContext<'_>, + viewport: &ViewportBlueprint, + ui: &mut egui::Ui, + item: &Item, +) { + match item { + Item::AppId(_) | Item::DataSource(_) | Item::StoreId(_) => { + // These have no bread crumbs, at least not currently. + // I guess one could argue that the `StoreId` should have the `AppId` as its ancestor? + } + Item::InstancePath(instance_path) => { + let InstancePath { + entity_path, + instance, + } = instance_path; + + if instance.is_all() { + // Entity path. Exclude the last part from the breadcrumbs, + // as we will show it in full later on. + if let [all_but_last @ .., _] = entity_path.as_slice() { + entity_path_breadcrumbs(ctx, ui, None, &EntityPath::root(), all_but_last, true); + } + } else { + // Instance path. + // Show the full entity path, and save the `[instance_nr]` for later. + entity_path_breadcrumbs( + ctx, + ui, + None, + &EntityPath::root(), + entity_path.as_slice(), + true, + ); + } + } + Item::ComponentPath(component_path) => { + entity_path_breadcrumbs( + ctx, + ui, + None, + &EntityPath::root(), + component_path.entity_path.as_slice(), + true, + ); + } + Item::Container(container_id) => { + if let Some(parent) = viewport.parent(&Contents::Container(*container_id)) { + viewport_breadcrumbs(ctx, viewport, ui, Contents::Container(parent)); + } + } + Item::SpaceView(view_id) => { + if let Some(parent) = viewport.parent(&Contents::SpaceView(*view_id)) { + viewport_breadcrumbs(ctx, viewport, ui, Contents::Container(parent)); + } + } + Item::DataResult(view_id, instance_path) => { + viewport_breadcrumbs(ctx, viewport, ui, Contents::SpaceView(*view_id)); + + let InstancePath { + entity_path, + instance, + } = instance_path; + + if let Some(view) = viewport.view(view_id) { + let common_ancestor = instance_path + .entity_path + .common_ancestor(&view.space_origin); + + let relative = &entity_path.as_slice()[common_ancestor.len()..]; + + let is_projection = !entity_path.starts_with(&view.space_origin); + // TODO(#4491): the projection breadcrumbs are wrong for nuscenes (but correct for arkit!), + // at least if we consider the blueprint tree panel as "correct". + // I fear we need to use the undocumented `DataResultNodeOrPath` and friends to match the + // hierarchy of the blueprint tree panel. + + if instance.is_all() { + // Entity path. Exclude the last part from the breadcrumbs, + // as we will show it in full later on. + if let [all_but_last @ .., _] = relative { + entity_path_breadcrumbs( + ctx, + ui, + Some(*view_id), + &common_ancestor, + all_but_last, + !is_projection, + ); + } + } else { + // Instance path. + // Show the full entity path, and save the `[instance_nr]` for later. + entity_path_breadcrumbs( + ctx, + ui, + Some(*view_id), + &common_ancestor, + relative, + !is_projection, + ); + } + } + } + } +} + +// Show the actual item, after all the bread crumbs: +fn last_part_of_item_heading( + ctx: &ViewerContext<'_>, + viewport: &ViewportBlueprint, + ui: &mut egui::Ui, + item: &Item, +) { + let ItemTitle { + icon, + label, + label_style: _, // Intentionally ignored + tooltip, + } = ItemTitle::from_item(ctx, viewport, ui.style(), item); + + let with_icon = match item { + Item::AppId { .. } + | Item::DataSource { .. } + | Item::Container { .. } + | Item::SpaceView { .. } + | Item::StoreId { .. } => true, + + Item::InstancePath { .. } | Item::DataResult { .. } | Item::ComponentPath { .. } => false, + }; + + let button = if with_icon { + egui::Button::image_and_text(icon.as_image().fit_to_original_size(ICON_SCALE), label) + .image_tint_follows_text_color(true) + } else { + egui::Button::new(label) + }; + let mut response = ui.add(button); + if let Some(tooltip) = tooltip { + response = response.on_hover_text(tooltip); + } + cursor_interact_with_selectable(ctx, response, item.clone()); +} + +/// The breadcrumbs of containers and views in the viewport. +fn viewport_breadcrumbs( + ctx: &ViewerContext<'_>, + viewport: &ViewportBlueprint, + ui: &mut egui::Ui, + contents: Contents, +) { + let item = Item::from(contents); + + if let Some(parent) = viewport.parent(&contents) { + // Recurse! + viewport_breadcrumbs(ctx, viewport, ui, parent.into()); + } + + let ItemTitle { + icon, + label: _, // ignored: we just show the icon for breadcrumbs + label_style: _, // no label + tooltip, + } = ItemTitle::from_contents(ctx, viewport, &contents); + + let mut response = ui.add( + egui::Button::image(icon.as_image().fit_to_original_size(ICON_SCALE)) + .image_tint_follows_text_color(true), + ); + if let Some(tooltip) = tooltip { + response = response.on_hover_text(tooltip); + } + cursor_interact_with_selectable(ctx, response, item); + + separator_icon_ui(ui, icons::BREADCRUMBS_SEPARATOR); +} + +fn separator_icon_ui(ui: &mut egui::Ui, icon: re_ui::Icon) { + ui.add( + icon.as_image() + .fit_to_original_size(ICON_SCALE) + .tint(ui.visuals().text_color().gamma_multiply(0.65)), + ); +} + +/// The breadcrumbs of an entity path, +/// that may or may not be part of a view. +fn entity_path_breadcrumbs( + ctx: &ViewerContext<'_>, + ui: &mut egui::Ui, + // If we are in a view + view_id: Option, + // Everything is relative to this + origin: &EntityPath, + // Show crumbs for all of these + entity_parts: &[EntityPathPart], + include_root: bool, +) { + if let [ancestry @ .., _] = entity_parts { + // Recurse! + + if !ancestry.is_empty() || include_root { + entity_path_breadcrumbs(ctx, ui, view_id, origin, ancestry, include_root); + } + } + + let full_entity_path = origin.join(&EntityPath::new(entity_parts.to_vec())); + + let button = if let Some(last) = full_entity_path.last() { + let first_char = last.unescaped_str().chars().next().unwrap_or('?'); + egui::Button::new(first_char.to_string()) + } else { + // Root + let icon = if view_id.is_some() { + // Inside a space view, we show the root with an icon + // that matches the one in the blueprint tree panel. + guess_instance_path_icon(ctx, &InstancePath::from(full_entity_path.clone())) + } else { + // For a streams hierarchy, we show the root using a different icon, + // just to make it clear that this is a different kind of hierarchy. + &icons::RECORDING // streams hierarchy + }; + egui::Button::image(icon.as_image().fit_to_original_size(ICON_SCALE)) + .image_tint_follows_text_color(true) + }; + + let response = ui.add(button); + let response = response.on_hover_ui(|ui| { + ui.label(full_entity_path.syntax_highlighted(ui.style())); + }); + + let item = if let Some(view_id) = view_id { + Item::DataResult(view_id, full_entity_path.into()) + } else { + Item::from(full_entity_path) + }; + cursor_interact_with_selectable(ctx, response, item); + + separator_icon_ui(ui, icons::BREADCRUMBS_SEPARATOR); +} diff --git a/crates/viewer/re_selection_panel/src/item_title.rs b/crates/viewer/re_selection_panel/src/item_title.rs index 9204a4e81006..4ab4750200e1 100644 --- a/crates/viewer/re_selection_panel/src/item_title.rs +++ b/crates/viewer/re_selection_panel/src/item_title.rs @@ -1,14 +1,25 @@ +use egui::WidgetText; + +use re_chunk::EntityPath; use re_data_ui::item_ui::{guess_instance_path_icon, guess_query_and_db_for_selected_entity}; -use re_ui::{icons, list_item, DesignTokens, SyntaxHighlighting as _, UiExt as _}; -use re_viewer_context::{contents_name_style, Item, SystemCommandSender as _, ViewerContext}; +use re_entity_db::InstancePath; +use re_log_types::ComponentPath; +use re_ui::{ + icons, + syntax_highlighting::{InstanceInBrackets as InstanceWithBrackets, SyntaxHighlightedBuilder}, + SyntaxHighlighting as _, +}; +use re_viewer_context::{ + contents_name_style, ContainerId, Contents, Item, SpaceViewId, ViewerContext, +}; use re_viewport_blueprint::ViewportBlueprint; #[must_use] pub struct ItemTitle { - name: egui::WidgetText, - hover: Option, - icon: &'static re_ui::Icon, - label_style: Option, + pub icon: &'static re_ui::Icon, + pub label: egui::WidgetText, + pub label_style: Option, + pub tooltip: Option, } impl ItemTitle { @@ -17,174 +28,204 @@ impl ItemTitle { viewport: &ViewportBlueprint, style: &egui::Style, item: &Item, - ) -> Option { + ) -> Self { match &item { Item::AppId(app_id) => { let title = app_id.to_string(); - Some(Self::new(title, &icons::APPLICATION)) + Self::new(title, &icons::APPLICATION) } Item::DataSource(data_source) => { let title = data_source.to_string(); - Some(Self::new(title, &icons::DATA_SOURCE)) + Self::new(title, &icons::DATA_SOURCE) } - Item::StoreId(store_id) => { - let id_str = format!("{} ID: {}", store_id.kind, store_id); - - let title = if let Some(entity_db) = ctx.store_context.bundle.get(store_id) { - if let Some(info) = entity_db.store_info() { - let time = info - .started - .format_time_custom( - "[hour]:[minute]:[second]", - ctx.app_options.time_zone, - ) - .unwrap_or("".to_owned()); - - format!("{} - {}", info.application_id, time) - } else { - id_str.clone() - } - } else { - id_str.clone() - }; - - let icon = match store_id.kind { - re_log_types::StoreKind::Recording => &icons::RECORDING, - re_log_types::StoreKind::Blueprint => &icons::BLUEPRINT, - }; + Item::StoreId(store_id) => Self::from_store_id(ctx, store_id), - Some(Self::new(title, icon).with_tooltip(id_str)) + Item::InstancePath(instance_path) => { + Self::from_instance_path(ctx, style, instance_path) } - Item::Container(container_id) => { - if let Some(container_blueprint) = viewport.container(container_id) { - let hover_text = - if let Some(display_name) = container_blueprint.display_name.as_ref() { - format!( - "{:?} container {display_name:?}", - container_blueprint.container_kind, - ) - } else { - format!("Unnamed {:?} container", container_blueprint.container_kind,) - }; - - let container_name = container_blueprint.display_name_or_default(); - Some( - Self::new( - container_name.as_ref(), - re_viewer_context::icon_for_container_kind( - &container_blueprint.container_kind, - ), - ) - .with_label_style(contents_name_style(&container_name)) - .with_tooltip(hover_text), - ) - } else { - None - } - } + Item::ComponentPath(component_path) => Self::from_component_path(ctx, component_path), - Item::ComponentPath(component_path) => { - let entity_path = &component_path.entity_path; - let component_name = &component_path.component_name; - - let (_query, db) = guess_query_and_db_for_selected_entity(ctx, entity_path); - let is_static = db - .storage_engine() - .store() - .entity_has_static_component(entity_path, component_name); - - Some( - Self::new( - component_name.short_name(), - if is_static { - &icons::COMPONENT_STATIC - } else { - &icons::COMPONENT_TEMPORAL - }, - ) - .with_tooltip(format!( - "{} component {} of entity '{}'", - if is_static { "Static" } else { "Temporal" }, - component_name.full_name(), - entity_path - )), - ) - } + Item::Container(container_id) => Self::from_container_id(viewport, container_id), - Item::SpaceView(view_id) => { + Item::SpaceView(view_id) => Self::from_view_id(ctx, viewport, view_id), + + Item::DataResult(view_id, instance_path) => { + let item_title = Self::from_instance_path(ctx, style, instance_path); if let Some(view) = viewport.view(view_id) { - let view_class = view.class(ctx.space_view_class_registry); - - let hover_text = if let Some(display_name) = view.display_name.as_ref() { - format!( - "Space view {:?} of type {}", - display_name, - view_class.display_name() - ) - } else { - format!("Unnamed view of type {}", view_class.display_name()) - }; - - let view_name = view.display_name_or_default(); - - Some( - Self::new( - view_name.as_ref(), - view.class(ctx.space_view_class_registry).icon(), - ) - .with_label_style(contents_name_style(&view_name)) - .with_tooltip(hover_text), + item_title.with_tooltip( + SyntaxHighlightedBuilder::new(ctx.egui_ctx.style()) + .append(instance_path) + .append(&format!(" in view '{}'", view.display_name_or_default())), ) } else { - None + item_title } } + } + } - Item::InstancePath(instance_path) => { - let typ = item.kind(); - let name = instance_path.syntax_highlighted(style); + pub fn from_store_id(ctx: &ViewerContext<'_>, store_id: &re_log_types::StoreId) -> Self { + let id_str = format!("{} ID: {}", store_id.kind, store_id); - Some( - Self::new(name, guess_instance_path_icon(ctx, instance_path)) - .with_tooltip(format!("{typ} '{instance_path}'")), - ) + let title = if let Some(entity_db) = ctx.store_context.bundle.get(store_id) { + if let Some(info) = entity_db.store_info() { + let time = info + .started + .format_time_custom("[hour]:[minute]:[second]", ctx.app_options.time_zone) + .unwrap_or("".to_owned()); + + format!("{} - {}", info.application_id, time) + } else { + id_str.clone() } + } else { + id_str.clone() + }; - Item::DataResult(view_id, instance_path) => { - let name = instance_path.syntax_highlighted(style); + let icon = match store_id.kind { + re_log_types::StoreKind::Recording => &icons::RECORDING, + re_log_types::StoreKind::Blueprint => &icons::BLUEPRINT, + }; - if let Some(view) = viewport.view(view_id) { - let typ = item.kind(); - Some( - Self::new(name, guess_instance_path_icon(ctx, instance_path)).with_tooltip( - format!( - "{typ} '{instance_path}' as shown in view {:?}", - view.display_name - ), - ), - ) - } else { - None - } + Self::new(title, icon).with_tooltip(id_str) + } + + pub fn from_instance_path( + ctx: &ViewerContext<'_>, + style: &egui::Style, + instance_path: &InstancePath, + ) -> Self { + let InstancePath { + entity_path, + instance, + } = instance_path; + + let name = if instance.is_all() { + // Entity path + if let Some(last) = entity_path.last() { + last.syntax_highlighted(style) + } else { + EntityPath::root().syntax_highlighted(style) } + } else { + // Instance path + InstanceWithBrackets(*instance).syntax_highlighted(style) + }; + + Self::new(name, guess_instance_path_icon(ctx, instance_path)) + .with_tooltip(instance_path.syntax_highlighted(style)) + } + + pub fn from_component_path(ctx: &ViewerContext<'_>, component_path: &ComponentPath) -> Self { + let entity_path = &component_path.entity_path; + let component_name = &component_path.component_name; + + let (_query, db) = guess_query_and_db_for_selected_entity(ctx, entity_path); + let is_static = db + .storage_engine() + .store() + .entity_has_static_component(entity_path, component_name); + + Self::new( + component_name.short_name(), + if is_static { + &icons::COMPONENT_STATIC + } else { + &icons::COMPONENT_TEMPORAL + }, + ) + .with_tooltip(format!( + "{} component {} of entity '{}'", + if is_static { "Static" } else { "Temporal" }, + component_name.full_name(), + entity_path + )) + } + + pub fn from_contents( + ctx: &ViewerContext<'_>, + viewport: &ViewportBlueprint, + contents: &Contents, + ) -> Self { + match contents { + Contents::Container(container_id) => Self::from_container_id(viewport, container_id), + Contents::SpaceView(view_id) => Self::from_view_id(ctx, viewport, view_id), + } + } + + pub fn from_container_id(viewport: &ViewportBlueprint, container_id: &ContainerId) -> Self { + if let Some(container_blueprint) = viewport.container(container_id) { + let hover_text = if let Some(display_name) = container_blueprint.display_name.as_ref() { + format!( + "{:?} container {display_name:?}", + container_blueprint.container_kind, + ) + } else { + format!("{:?} container", container_blueprint.container_kind,) + }; + + let container_name = container_blueprint.display_name_or_default(); + Self::new( + container_name.as_ref(), + re_viewer_context::icon_for_container_kind(&container_blueprint.container_kind), + ) + .with_label_style(contents_name_style(&container_name)) + .with_tooltip(hover_text) + } else { + Self::new( + format!("Unknown container {container_id}"), + &icons::SPACE_VIEW_UNKNOWN, + ) + .with_tooltip("Failed to find container in blueprint") + } + } + + fn from_view_id( + ctx: &ViewerContext<'_>, + viewport: &ViewportBlueprint, + view_id: &SpaceViewId, + ) -> Self { + if let Some(view) = viewport.view(view_id) { + let view_class = view.class(ctx.space_view_class_registry); + + let hover_text = if let Some(display_name) = view.display_name.as_ref() { + format!("{} view {display_name:?}", view_class.display_name(),) + } else { + format!("{} view", view_class.display_name()) + }; + + let view_name = view.display_name_or_default(); + + Self::new( + view_name.as_ref(), + view.class(ctx.space_view_class_registry).icon(), + ) + .with_label_style(contents_name_style(&view_name)) + .with_tooltip(hover_text) + } else { + Self::new( + format!("Unknown view {view_id}"), + &icons::SPACE_VIEW_UNKNOWN, + ) + .with_tooltip("Failed to find view in blueprint") } } fn new(name: impl Into, icon: &'static re_ui::Icon) -> Self { Self { - name: name.into(), - hover: None, + label: name.into(), + tooltip: None, icon, label_style: None, } } #[inline] - fn with_tooltip(mut self, hover: impl Into) -> Self { - self.hover = Some(hover.into()); + fn with_tooltip(mut self, tooltip: impl Into) -> Self { + self.tooltip = Some(tooltip.into()); self } @@ -193,36 +234,4 @@ impl ItemTitle { self.label_style = Some(label_style); self } - - pub fn ui(self, ctx: &ViewerContext<'_>, ui: &mut egui::Ui, item: &Item) { - let Self { - name, - hover, - icon, - label_style, - } = self; - - let mut content = list_item::LabelContent::new(name).with_icon(icon); - - if let Some(label_style) = label_style { - content = content.label_style(label_style); - } - - let response = ui - .list_item() - .with_height(DesignTokens::title_bar_height()) - .selected(true) - .show_flat(ui, content); - - if response.clicked() { - // If the user has multiple things selected but only wants to have one thing selected, - // this is how they can do it. - ctx.command_sender - .send_system(re_viewer_context::SystemCommand::SetSelection(item.clone())); - } - - if let Some(hover) = hover { - response.on_hover_text(hover); - } - } } diff --git a/crates/viewer/re_selection_panel/src/lib.rs b/crates/viewer/re_selection_panel/src/lib.rs index 02b267f6ea28..04acf4d478c6 100644 --- a/crates/viewer/re_selection_panel/src/lib.rs +++ b/crates/viewer/re_selection_panel/src/lib.rs @@ -1,6 +1,7 @@ //! The UI for the selection panel. mod defaults_ui; +mod item_heading_with_breadcrumbs; mod item_title; mod selection_panel; mod space_view_entity_picker; @@ -8,7 +9,6 @@ mod space_view_space_origin_ui; mod visible_time_range_ui; mod visualizer_ui; -pub use item_title::ItemTitle; pub use selection_panel::SelectionPanel; #[cfg(test)] diff --git a/crates/viewer/re_selection_panel/src/selection_panel.rs b/crates/viewer/re_selection_panel/src/selection_panel.rs index 0d6145e76564..0930de663823 100644 --- a/crates/viewer/re_selection_panel/src/selection_panel.rs +++ b/crates/viewer/re_selection_panel/src/selection_panel.rs @@ -2,9 +2,12 @@ use egui::NumExt as _; use egui_tiles::ContainerKind; use re_context_menu::{context_menu_ui_for_item, SelectionUpdateBehavior}; -use re_data_ui::{item_ui, item_ui::guess_query_and_db_for_selected_entity, DataUi}; +use re_data_ui::{ + item_ui::{self, cursor_interact_with_selectable, guess_query_and_db_for_selected_entity}, + DataUi, +}; use re_entity_db::{EntityPath, InstancePath}; -use re_log_types::EntityPathFilter; +use re_log_types::{ComponentPath, EntityPathFilter}; use re_types::blueprint::components::Interactive; use re_ui::{ icons, @@ -18,11 +21,14 @@ use re_viewer_context::{ }; use re_viewport_blueprint::{ui::show_add_space_view_or_container_modal, ViewportBlueprint}; -use crate::{defaults_ui::view_components_defaults_section_ui, visualizer_ui::visualizer_ui}; -use crate::{space_view_entity_picker::SpaceViewEntityPicker, ItemTitle}; use crate::{ - visible_time_range_ui::visible_time_range_ui_for_data_result, - visible_time_range_ui::visible_time_range_ui_for_view, + defaults_ui::view_components_defaults_section_ui, + item_heading_with_breadcrumbs::item_heading_with_breadcrumbs, + space_view_entity_picker::SpaceViewEntityPicker, + visible_time_range_ui::{ + visible_time_range_ui_for_data_result, visible_time_range_ui_for_view, + }, + visualizer_ui::visualizer_ui, }; // --- @@ -113,12 +119,7 @@ impl SelectionPanel { }; for (i, item) in selection.iter_items().enumerate() { list_item::list_item_scope(ui, item, |ui| { - if let Some(item_title) = ItemTitle::from_item(ctx, viewport, ui.style(), item) { - item_title.ui(ctx, ui, item); - } else { - re_log::warn_once!("Failed to create item title for {item:?}"); - return; // WEIRD - } + item_heading_with_breadcrumbs(ctx, viewport, ui, item); self.item_ui(ctx, viewport, view_states, ui, item, ui_layout); }); @@ -140,8 +141,10 @@ impl SelectionPanel { ) { match item { Item::ComponentPath(component_path) => { - let entity_path = &component_path.entity_path; - let component_name = &component_path.component_name; + let ComponentPath { + entity_path, + component_name, + } = component_path; let (query, db) = guess_query_and_db_for_selected_entity(ctx, entity_path); let is_static = db @@ -149,6 +152,12 @@ impl SelectionPanel { .store() .entity_has_static_component(entity_path, component_name); + ui.list_item_flat_noninteractive(PropertyContent::new("Parent entity").value_fn( + |ui, _| { + item_ui::entity_path_parts_buttons(ctx, &query, db, ui, None, entity_path); + }, + )); + ui.list_item_flat_noninteractive( PropertyContent::new("Component type").value_text(if is_static { "Static" @@ -157,35 +166,31 @@ impl SelectionPanel { }), ); - ui.list_item_flat_noninteractive(PropertyContent::new("Parent entity").value_fn( - |ui, _| { - item_ui::entity_path_button(ctx, &query, db, ui, None, entity_path); - }, - )); - list_existing_data_blueprints(ctx, viewport, ui, &entity_path.clone().into()); } Item::InstancePath(instance_path) => { - let is_instance = !instance_path.instance.is_all(); - let parent = if is_instance { - Some(instance_path.entity_path.clone()) - } else { - instance_path.entity_path.parent() - }; - if let Some(parent) = parent { - if !parent.is_root() { - let (query, db) = - guess_query_and_db_for_selected_entity(ctx, &instance_path.entity_path); + let (query, db) = + guess_query_and_db_for_selected_entity(ctx, &instance_path.entity_path); - ui.list_item_flat_noninteractive(PropertyContent::new("Parent").value_fn( - |ui, _| { - item_ui::entity_path_parts_buttons( - ctx, &query, db, ui, None, &parent, - ); - }, - )); - } + ui.list_item_flat_noninteractive(PropertyContent::new("Entity path").value_fn( + |ui, _| { + item_ui::entity_path_parts_buttons( + ctx, + &query, + db, + ui, + None, + &instance_path.entity_path, + ); + }, + )); + + if instance_path.instance.is_specific() { + ui.list_item_flat_noninteractive( + PropertyContent::new("Instance") + .value_text(instance_path.instance.to_string()), + ); } list_existing_data_blueprints(ctx, viewport, ui, instance_path); @@ -203,61 +208,36 @@ impl SelectionPanel { } Item::DataResult(view_id, instance_path) => { - if let Some(view) = viewport.view(view_id) { - let is_instance = !instance_path.instance.is_all(); - let parent = if is_instance { - Some(instance_path.entity_path.clone()) - } else { - instance_path.entity_path.parent() - }; - if let Some(parent) = parent { - if !parent.is_root() { - ui.list_item_flat_noninteractive( - PropertyContent::new("Parent").value_fn(|ui, _| { - let (query, db) = guess_query_and_db_for_selected_entity( - ctx, - &instance_path.entity_path, - ); - - item_ui::entity_path_parts_buttons( - ctx, - &query, - db, - ui, - Some(*view_id), - &parent, - ); - }), - ); - } - } + ui.list_item_flat_noninteractive(PropertyContent::new("Stream entity").value_fn( + |ui, _| { + let (query, db) = + guess_query_and_db_for_selected_entity(ctx, &instance_path.entity_path); - ui.list_item_flat_noninteractive(PropertyContent::new("In view").value_fn( - |ui, _| { - space_view_button(ctx, ui, view); - }, - )); - } + item_ui::entity_path_parts_buttons( + ctx, + &query, + db, + ui, + None, + &instance_path.entity_path, + ); + }, + )); - if instance_path.is_all() { - ui.list_item_flat_noninteractive(PropertyContent::new("Entity").value_fn( + if instance_path.instance.is_specific() { + ui.list_item_flat_noninteractive(PropertyContent::new("Instance").value_fn( |ui, _| { - let (query, db) = guess_query_and_db_for_selected_entity( + let response = ui.button(instance_path.instance.to_string()); + cursor_interact_with_selectable( ctx, - &instance_path.entity_path, - ); - - item_ui::entity_path_button( - ctx, - &query, - db, - ui, - None, - &instance_path.entity_path, + response, + Item::from(instance_path.clone()), ); }, )); + } + if instance_path.is_all() { let entity_path = &instance_path.entity_path; let query_result = ctx.lookup_query_result(*view_id); let data_result = query_result diff --git a/crates/viewer/re_ui/data/icons/breadcrumbs_separator.png b/crates/viewer/re_ui/data/icons/breadcrumbs_separator.png new file mode 100644 index 000000000000..fac47a6b5458 Binary files /dev/null and b/crates/viewer/re_ui/data/icons/breadcrumbs_separator.png differ diff --git a/crates/viewer/re_ui/src/icons.rs b/crates/viewer/re_ui/src/icons.rs index 7b5c2770f5c7..8ec9dcdab2fc 100644 --- a/crates/viewer/re_ui/src/icons.rs +++ b/crates/viewer/re_ui/src/icons.rs @@ -118,3 +118,6 @@ pub const BLUEPRINT: Icon = icon_from_path!("../data/icons/blueprint.png"); pub const GITHUB: Icon = icon_from_path!("../data/icons/github.png"); pub const VIDEO_ERROR: Icon = icon_from_path!("../data/icons/video_error.png"); + +/// `>` +pub const BREADCRUMBS_SEPARATOR: Icon = icon_from_path!("../data/icons/breadcrumbs_separator.png"); diff --git a/crates/viewer/re_ui/src/lib.rs b/crates/viewer/re_ui/src/lib.rs index fd1d14c9ec93..a6e155863e2b 100644 --- a/crates/viewer/re_ui/src/lib.rs +++ b/crates/viewer/re_ui/src/lib.rs @@ -2,16 +2,15 @@ mod command; mod command_palette; -mod design_tokens; -mod syntax_highlighting; - mod context_ext; +mod design_tokens; pub mod drag_and_drop; pub mod icons; pub mod list_item; mod markdown_utils; pub mod modal; mod section_collapsing_header; +pub mod syntax_highlighting; pub mod toasts; mod ui_ext; pub mod zoom_pan_area; diff --git a/crates/viewer/re_ui/src/list_item/list_item.rs b/crates/viewer/re_ui/src/list_item/list_item.rs index dc32530862c4..cdee8a6b3f06 100644 --- a/crates/viewer/re_ui/src/list_item/list_item.rs +++ b/crates/viewer/re_ui/src/list_item/list_item.rs @@ -412,6 +412,8 @@ impl ListItem { } else { None } + } else if selected { + Some(visuals.weak_bg_fill) } else { None } diff --git a/crates/viewer/re_ui/src/list_item/scope.rs b/crates/viewer/re_ui/src/list_item/scope.rs index 6a7cf185855b..e84067a843e1 100644 --- a/crates/viewer/re_ui/src/list_item/scope.rs +++ b/crates/viewer/re_ui/src/list_item/scope.rs @@ -70,7 +70,15 @@ impl LayoutStatistics { /// Read the saved accumulated value. fn read(ctx: &egui::Context, scope_id: egui::Id) -> Self { - ctx.data(|reader| reader.get_temp(scope_id).unwrap_or_default()) + if let Some(slf) = ctx.data(|reader| reader.get_temp(scope_id)) { + slf + } else { + // First time we do layout in this scope. + // The layout will likely be weird this pass, + // so discard and do another pass to avoid jitter: + ctx.request_discard("Missing re_ui::LayoutStatistics"); + Default::default() + } } /// Update the accumulator. diff --git a/crates/viewer/re_ui/src/syntax_highlighting.rs b/crates/viewer/re_ui/src/syntax_highlighting.rs index f2b97d0cd266..af3d9d76e85b 100644 --- a/crates/viewer/re_ui/src/syntax_highlighting.rs +++ b/crates/viewer/re_ui/src/syntax_highlighting.rs @@ -1,8 +1,11 @@ +use std::sync::Arc; + use re_entity_db::InstancePath; use re_log_types::{EntityPath, EntityPathPart, Instance}; use egui::{text::LayoutJob, Color32, Style, TextFormat}; +// ---------------------------------------------------------------------------- pub trait SyntaxHighlighting { fn syntax_highlighted(&self, style: &Style) -> LayoutJob { let mut job = LayoutJob::default(); @@ -13,6 +16,54 @@ pub trait SyntaxHighlighting { fn syntax_highlight_into(&self, style: &Style, job: &mut LayoutJob); } +// ---------------------------------------------------------------------------- + +/// Easily build syntax-highlighted text. +pub struct SyntaxHighlightedBuilder { + pub style: Arc