re_data_ui 0.37.0

Provides ui elements for Rerun component data for the Rerun Viewer.
Documentation
use egui::{Atoms, WidgetText};
use re_entity_db::InstancePath;
use re_entity_db::entity_db::EntityDbClass;
use re_log_types::{ComponentPath, EntityPath, TableId};
use re_sdk_types::archetypes::RecordingInfo;
use re_sdk_types::components::{Name, Timestamp};
use re_ui::list_item::LabelContent;
use re_ui::syntax_highlighting::{
    InstanceInBrackets as InstanceWithBrackets, SyntaxHighlightedBuilder,
};
use re_ui::{HasDesignTokens as _, SyntaxHighlighting as _, icons};
use re_viewer_context::{
    ContainerId, Contents, DataResultInteractionAddress, Item, RedapEntryKind, ViewId,
    ViewerContext, contents_name_style,
};
use re_viewport_blueprint::ViewportBlueprint;

use crate::item_ui::guess_instance_path_icon;

pub fn is_component_static(ctx: &ViewerContext<'_>, component_path: &ComponentPath) -> bool {
    let ComponentPath {
        entity_path,
        component,
    } = component_path;
    ctx.guess_store_view_context_for_entity(entity_path)
        .db
        .storage_engine()
        .store()
        .entity_has_static_component(entity_path, *component)
}

#[must_use]
pub struct ItemTitle {
    pub icon: &'static re_ui::Icon,
    pub label: egui::WidgetText,
    pub label_style: Option<re_ui::LabelStyle>,
    pub tooltip: Option<WidgetText>,
}

impl ItemTitle {
    pub fn from_item(
        ctx: &ViewerContext<'_>,
        viewport: &ViewportBlueprint,
        style: &egui::Style,
        item: &Item,
    ) -> Self {
        match &item {
            Item::AppId(app_id) => Self::new(app_id.to_string(), &icons::APPLICATION),

            Item::DataSource(data_source) => {
                Self::new(data_source.to_string(), &icons::DATA_SOURCE)
            }

            Item::StoreId(store_id) => Self::from_store_id(ctx, store_id),
            Item::TableId(table_id) => Self::from_table_id(ctx, table_id),

            Item::InstancePath(instance_path) => {
                Self::from_instance_path(ctx, style, instance_path)
            }

            Item::ComponentPath(component_path) => Self::from_component_path(ctx, component_path),

            Item::Container(container_id) => Self::from_container_id(viewport, container_id),

            Item::View(view_id) => Self::from_view_id(ctx, viewport, view_id),

            Item::DataResult(DataResultInteractionAddress {
                view_id,
                instance_path,
                visualizer: _, // Can't distinguish visualizer here since we don't name them.
            }) => {
                let item_title = Self::from_instance_path(ctx, style, instance_path);
                if let Some(view) = viewport.view(view_id) {
                    item_title.with_tooltip(
                        SyntaxHighlightedBuilder::new()
                            .with(instance_path)
                            .with_body(" in view ")
                            .with(&view.display_name_or_default())
                            .into_widget_text(&ctx.egui_ctx().global_style()),
                    )
                } else {
                    item_title
                }
            }

            // TODO(#10566): There should be an `EntryName` in this `Item` arm.
            Item::RedapEntry { kind, .. } => match kind {
                RedapEntryKind::Entry(id) => Self::new(id.to_string(), &icons::DATASET),
                RedapEntryKind::Folder(path_prefix) => {
                    Self::new(path_prefix.clone(), &icons::DATASET)
                }
            },

            // TODO(lucasmerlin): Icon?
            Item::RedapServer(origin) => Self::new(origin.to_string(), &icons::DATASET),
        }
    }

    pub fn from_table_id(_ctx: &ViewerContext<'_>, table_id: &TableId) -> Self {
        Self::new(table_id.as_str(), &icons::ENTITY_RESERVED).with_tooltip(table_id.as_str())
    }

    pub fn from_store_id(ctx: &ViewerContext<'_>, store_id: &re_log_types::StoreId) -> Self {
        // Match the priority used in the sources panel:
        // 1. User-set recording name
        // 2. Dataset segment ID (for remote data)
        // 3. Start timestamp (most useful local fallback)
        // 4. Application ID as last resort
        let title = if let Some(entity_db) = ctx.store_bundle().get(store_id) {
            if let Some(name) = entity_db
                .recording_info_property::<Name>(RecordingInfo::descriptor_name().component)
            {
                name.to_string()
            } else if let EntityDbClass::DatasetSegment(url) = entity_db.store_class()
                && let Some(segment_id) = &url.segment_id
            {
                segment_id.as_str().to_owned()
            } else if let Some(started) = entity_db.recording_info_property::<Timestamp>(
                RecordingInfo::descriptor_start_time().component,
            ) {
                // Use the same %H:%M:%S format the recording panel shows.
                let time = re_log_types::Timestamp::from(started.0)
                    .to_jiff_zoned(ctx.app_options().timestamp_format)
                    .strftime("%H:%M:%S")
                    .to_string();
                format!("{}{time}", store_id.application_id())
            } else {
                store_id.application_id().to_string()
            }
        } else {
            store_id.application_id().to_string()
        };

        let icon = match store_id.kind() {
            re_log_types::StoreKind::Recording => &icons::RECORDING,
            re_log_types::StoreKind::Blueprint => &icons::BLUEPRINT,
        };

        Self::new(title, icon).with_tooltip(format!(
            "Store kind: {}\nApplication ID: {}\nRecording ID: {}",
            store_id.kind(),
            store_id.application_id(),
            store_id.recording_id(),
        ))
    }

    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 is_static = is_component_static(ctx, component_path);

        let ComponentPath {
            entity_path,
            component,
        } = component_path;

        Self::new(
            component.as_str(),
            if is_static {
                &icons::COMPONENT_STATIC
            } else {
                &icons::COMPONENT_TEMPORAL
            },
        )
        .with_tooltip(format!("{entity_path}:{component}"))
    }

    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::View(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::VIEW_UNKNOWN,
            )
            .with_tooltip("Failed to find container in blueprint")
        }
    }

    fn from_view_id(
        ctx: &ViewerContext<'_>,
        viewport: &ViewportBlueprint,
        view_id: &ViewId,
    ) -> Self {
        if let Some(view) = viewport.view(view_id) {
            let view_class = view.class(ctx.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.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::VIEW_UNKNOWN)
                .with_tooltip("Failed to find view in blueprint")
        }
    }

    /// The icon, label and label style as the content of a list item.
    pub fn to_label_content(&self) -> LabelContent<'static> {
        let mut content = LabelContent::new(self.label.clone()).with_icon(self.icon);
        if let Some(label_style) = self.label_style {
            content = content.label_style(label_style);
        }
        content
    }

    /// The icon and the label as atoms, ready for a button or a label.
    pub fn into_atoms(self, style: &egui::Style, icon_tint: egui::Color32) -> Atoms<'static> {
        Atoms::new((
            self.icon
                .as_image()
                .fit_to_exact_size(style.tokens().small_icon_size)
                .tint(icon_tint),
            self.label,
        ))
    }

    fn new(name: impl Into<egui::WidgetText>, icon: &'static re_ui::Icon) -> Self {
        Self {
            label: name.into(),
            tooltip: None,
            icon,
            label_style: None,
        }
    }

    #[inline]
    fn with_tooltip(mut self, tooltip: impl Into<WidgetText>) -> Self {
        self.tooltip = Some(tooltip.into());
        self
    }

    #[inline]
    fn with_label_style(mut self, label_style: re_ui::LabelStyle) -> Self {
        self.label_style = Some(label_style);
        self
    }
}

// ----------------------------------------------------------------------------

/// The chevron that separates the parts of a [`QualifiedItemTitle`].
pub fn part_separator_image(tint: egui::Color32) -> egui::Image<'static> {
    icons::CHEVRON
        .as_image()
        .fit_to_exact_size(egui::Vec2::splat(8.0))
        .tint(tint)
}

/// A title that stands on its own, made of one or more [`ItemTitle`] parts, outermost first.
///
/// [`ItemTitle::from_item`] assumes the surrounding UI supplies the context — the selection panel
/// draws breadcrumbs above the title, so the title itself shows only the last part of an entity
/// path, and only the index of an instance. In a flat list (a menu, a history) there is no such
/// context, so here paths are spelled out in full, and an item that is made of two things — a
/// component path, a data result — becomes two parts.
#[must_use]
pub struct QualifiedItemTitle {
    /// Outermost part first.
    pub parts: Vec<ItemTitle>,
}

impl QualifiedItemTitle {
    pub fn from_item(
        ctx: &ViewerContext<'_>,
        viewport: &ViewportBlueprint,
        style: &egui::Style,
        item: &Item,
    ) -> Self {
        match item {
            Item::InstancePath(instance_path) => Self::single(
                ItemTitle::new(
                    instance_path.syntax_highlighted(style),
                    guess_instance_path_icon(ctx, instance_path),
                )
                .with_tooltip(instance_path.syntax_highlighted(style)),
            ),

            // Break up into entity path and component:
            Item::ComponentPath(component_path) => {
                let mut title = Self::from_item(
                    ctx,
                    viewport,
                    style,
                    &Item::from(component_path.entity_path.clone()),
                );
                title.parts.push(ItemTitle {
                    label: component_path.component.syntax_highlighted(style).into(),
                    ..ItemTitle::from_component_path(ctx, component_path)
                });
                title
            }

            // Break up into view and instance path:
            Item::DataResult(DataResultInteractionAddress {
                view_id,
                instance_path,
                visualizer: _, // Can't distinguish visualizer here since we don't name them.
            }) => {
                let mut title = Self::from_item(ctx, viewport, style, &Item::View(*view_id));
                title.parts.extend(
                    Self::from_item(
                        ctx,
                        viewport,
                        style,
                        &Item::InstancePath(instance_path.clone()),
                    )
                    .parts,
                );
                title
            }

            _ => Self::single(ItemTitle::from_item(ctx, viewport, style, item)),
        }
    }

    /// All parts as atoms, the separator between them, ready for a button or a label.
    pub fn into_atoms(self, style: &egui::Style, icon_tint: egui::Color32) -> Atoms<'static> {
        let mut atoms = Atoms::default();

        for (i, part) in self.parts.into_iter().enumerate() {
            if i != 0 {
                atoms.push_right(part_separator_image(icon_tint));
            }
            atoms.extend_right(part.into_atoms(style, icon_tint));
        }

        atoms
    }

    fn single(title: ItemTitle) -> Self {
        Self { parts: vec![title] }
    }
}