re_data_ui/
instance_path.rs

1use std::collections::BTreeMap;
2
3use egui::RichText;
4use itertools::Itertools as _;
5use re_capabilities::MainThreadToken;
6
7use re_chunk_store::UnitChunkShared;
8use re_entity_db::InstancePath;
9use re_log_types::ComponentPath;
10use re_types::{
11    ArchetypeName, Component as _, ComponentDescriptor, components,
12    reflection::ComponentDescriptorExt as _,
13};
14use re_ui::list_item::ListItemContentButtonsExt as _;
15use re_ui::{UiExt as _, design_tokens_of_visuals, list_item};
16use re_viewer_context::{HoverHighlight, Item, UiLayout, ViewerContext};
17
18use super::DataUi;
19use crate::extra_data_ui::ExtraDataUi;
20
21impl DataUi for InstancePath {
22    fn data_ui(
23        &self,
24        ctx: &ViewerContext<'_>,
25        ui: &mut egui::Ui,
26        ui_layout: UiLayout,
27        query: &re_chunk_store::LatestAtQuery,
28        db: &re_entity_db::EntityDb,
29    ) {
30        let Self {
31            entity_path,
32            instance,
33        } = self;
34
35        // NOTE: the passed in `db` is usually the recording; NOT the blueprint.
36        let component = if ctx.recording().is_known_entity(entity_path) {
37            // We are looking at an entity in the recording
38            ctx.recording_engine()
39                .store()
40                .all_components_on_timeline(&query.timeline(), entity_path)
41        } else if ctx.blueprint_db().is_known_entity(entity_path) {
42            // We are looking at an entity in the blueprint
43            ctx.blueprint_db()
44                .storage_engine()
45                .store()
46                .all_components_on_timeline(&query.timeline(), entity_path)
47        } else {
48            ui.error_label(format!("Unknown entity: {entity_path:?}"));
49            return;
50        };
51        let Some(unordered_components) = component else {
52            // This is fine - e.g. we're looking at `/world` and the user has only logged to `/world/car`.
53            ui_layout.label(
54                ui,
55                format!(
56                    "{self} has no own components on timeline {:?}, but its children do",
57                    query.timeline()
58                ),
59            );
60            return;
61        };
62
63        let components_by_archetype = {
64            let recording_engine = ctx.recording_engine();
65            let store = recording_engine.store();
66            crate::sorted_component_list_by_archetype_for_ui(
67                ctx.reflection(),
68                unordered_components
69                    .iter()
70                    .filter_map(|c| store.entity_component_descriptor(entity_path, *c)),
71            )
72        };
73
74        let mut query_results = db.storage_engine().cache().latest_at(
75            query,
76            entity_path,
77            unordered_components.iter().copied(),
78        );
79
80        // Keep previously established order.
81        let mut components_by_archetype: BTreeMap<
82            Option<ArchetypeName>,
83            Vec<(ComponentDescriptor, UnitChunkShared)>,
84        > = components_by_archetype
85            .into_iter()
86            .map(|(archetype, components)| {
87                (
88                    archetype,
89                    components
90                        .into_iter()
91                        .filter_map(|c| {
92                            query_results
93                                .components
94                                .remove(&c.component)
95                                .map(|chunk| (c, chunk))
96                        })
97                        .collect(),
98                )
99            })
100            .collect();
101
102        if components_by_archetype.is_empty() {
103            let typ = db.timeline_type(&query.timeline());
104            ui_layout.label(
105                ui,
106                format!(
107                    "Nothing logged at {} = {}",
108                    query.timeline(),
109                    typ.format(query.at(), ctx.app_options().timestamp_format),
110                ),
111            );
112            return;
113        }
114
115        // Showing more than this takes up too much space
116        const MAX_COMPONENTS_IN_TOOLTIP: usize = 3;
117
118        if ui_layout.is_single_line() {
119            let archetype_count = components_by_archetype.len();
120            let component_count = unordered_components.len();
121            ui_layout.label(
122                ui,
123                format!(
124                    "{} archetype{} with {} total component{}",
125                    archetype_count,
126                    if archetype_count > 1 { "s" } else { "" },
127                    component_count,
128                    if component_count > 1 { "s" } else { "" }
129                ),
130            );
131        } else if ui_layout == UiLayout::Tooltip
132            && MAX_COMPONENTS_IN_TOOLTIP < unordered_components.len()
133        {
134            // Too many to show all in a tooltip.
135
136            let mut show_only_instanced = false;
137
138            if !self.is_all() {
139                // Focus on the components that have different values per instance (non-splatted components):
140                let instanced_components_by_archetype: BTreeMap<
141                    Option<ArchetypeName>,
142                    Vec<(ComponentDescriptor, UnitChunkShared)>,
143                > = components_by_archetype
144                    .iter()
145                    .filter_map(|(archetype_name, archetype_components)| {
146                        let instanced_archetype_components = archetype_components
147                            .iter()
148                            .filter(|(descr, unit)| unit.num_instances(descr.component) > 1)
149                            .cloned()
150                            .collect_vec();
151                        if instanced_archetype_components.is_empty() {
152                            None
153                        } else {
154                            Some((*archetype_name, instanced_archetype_components))
155                        }
156                    })
157                    .collect();
158
159                let num_instanced_components = instanced_components_by_archetype
160                    .values()
161                    .map(|v| v.len())
162                    .sum::<usize>();
163
164                show_only_instanced = num_instanced_components <= MAX_COMPONENTS_IN_TOOLTIP;
165
166                if show_only_instanced {
167                    component_list_ui(
168                        ctx,
169                        ui,
170                        ui_layout,
171                        query,
172                        db,
173                        entity_path,
174                        instance,
175                        &instanced_components_by_archetype,
176                    );
177
178                    let num_skipped = unordered_components.len() - num_instanced_components;
179                    ui.label(format!(
180                        "…plus {num_skipped} more {}",
181                        if num_skipped == 1 {
182                            "component"
183                        } else {
184                            "components"
185                        }
186                    ));
187                }
188            }
189
190            if !show_only_instanced {
191                // Show just a rough summary:
192
193                let component_count = unordered_components.len();
194                ui.list_item_label(format!(
195                    "{} component{}",
196                    component_count,
197                    if component_count > 1 { "s" } else { "" }
198                ));
199
200                let archetype_count = components_by_archetype.len();
201                ui.list_item_label(format!(
202                    "{} archetype{}: {}",
203                    archetype_count,
204                    if archetype_count > 1 { "s" } else { "" },
205                    components_by_archetype
206                        .keys()
207                        .map(|archetype| {
208                            if let Some(archetype) = archetype {
209                                archetype.short_name()
210                            } else {
211                                "<Without archetype>"
212                            }
213                        })
214                        .join(", ")
215                ));
216            }
217        } else {
218            // TODO(#7026): Instances today are too poorly defined:
219            // For many archetypes it makes sense to slice through all their component arrays with the same index.
220            // However, there are cases when there are multiple dimensions of slicing that make sense.
221            // This is most obvious for meshes & graph nodes where there are different dimensions for vertices/edges/etc.
222            //
223            // For graph nodes this is particularly glaring since our indicices imply nodes today and
224            // unlike with meshes it's very easy to hover & select individual nodes.
225            // In order to work around the GraphEdges showing up associated with random nodes, we just hide them here.
226            // (this is obviously a hack and these relationships should be formalized such that they are accessible to the UI, see ticket link above)
227            if !self.is_all() {
228                for components in components_by_archetype.values_mut() {
229                    components.retain(|(component, _chunk)| {
230                        component.component_type != Some(components::GraphEdge::name())
231                    });
232                }
233            }
234
235            component_list_ui(
236                ctx,
237                ui,
238                ui_layout,
239                query,
240                db,
241                entity_path,
242                instance,
243                &components_by_archetype,
244            );
245        }
246
247        if instance.is_all() {
248            // There are some examples where we need to combine several archetypes for a single preview.
249            // For instance `VideoFrameReference` and `VideoAsset` are used together for a single preview.
250            let all_components = components_by_archetype
251                .values()
252                .flatten()
253                .cloned()
254                .collect::<Vec<_>>();
255
256            for (descr, shared) in &all_components {
257                if let Some(data) = ExtraDataUi::from_components(
258                    ctx,
259                    query,
260                    entity_path,
261                    descr,
262                    shared,
263                    &all_components,
264                ) {
265                    data.data_ui(ctx, ui, ui_layout, query, entity_path);
266                }
267            }
268        }
269    }
270}
271
272#[expect(clippy::too_many_arguments)]
273fn component_list_ui(
274    ctx: &ViewerContext<'_>,
275    ui: &mut egui::Ui,
276    ui_layout: UiLayout,
277    query: &re_chunk_store::LatestAtQuery,
278    db: &re_entity_db::EntityDb,
279    entity_path: &re_log_types::EntityPath,
280    instance: &re_log_types::Instance,
281    components_by_archetype: &BTreeMap<
282        Option<ArchetypeName>,
283        Vec<(ComponentDescriptor, UnitChunkShared)>,
284    >,
285) {
286    re_ui::list_item::list_item_scope(
287        ui,
288        egui::Id::from("component list").with(entity_path),
289        |ui| {
290            for (archetype, archetype_components) in components_by_archetype {
291                if archetype.is_none() && components_by_archetype.len() == 1 {
292                    // They are all without archetype, so we can skip the label.
293                } else {
294                    archetype_label_list_item_ui(ui, archetype);
295                }
296
297                for (component_descr, unit) in archetype_components {
298                    component_ui(
299                        ctx,
300                        ui,
301                        ui_layout,
302                        query,
303                        db,
304                        entity_path,
305                        instance,
306                        archetype_components,
307                        component_descr,
308                        unit,
309                    );
310                }
311            }
312        },
313    );
314}
315
316#[expect(clippy::too_many_arguments)]
317fn component_ui(
318    ctx: &ViewerContext<'_>,
319    ui: &mut egui::Ui,
320    ui_layout: UiLayout,
321    query: &re_chunk_store::LatestAtQuery,
322    db: &re_entity_db::EntityDb,
323    entity_path: &re_log_types::EntityPath,
324    instance: &re_log_types::Instance,
325    archetype_components: &[(ComponentDescriptor, UnitChunkShared)],
326    component_descr: &ComponentDescriptor,
327    unit: &UnitChunkShared,
328) {
329    let interactive = ui_layout != UiLayout::Tooltip;
330
331    let component_path = ComponentPath::new(entity_path.clone(), component_descr.component);
332
333    let is_static = db
334        .storage_engine()
335        .store()
336        .entity_has_static_component(entity_path, component_descr.component);
337    let icon = if is_static {
338        &re_ui::icons::COMPONENT_STATIC
339    } else {
340        &re_ui::icons::COMPONENT_TEMPORAL
341    };
342    let item = Item::ComponentPath(component_path);
343
344    let mut list_item = ui.list_item().interactive(interactive);
345
346    if interactive {
347        let is_hovered =
348            ctx.selection_state().highlight_for_ui_element(&item) == HoverHighlight::Hovered;
349        list_item = list_item.force_hovered(is_hovered);
350    }
351
352    let data = ExtraDataUi::from_components(
353        ctx,
354        query,
355        entity_path,
356        component_descr,
357        unit,
358        archetype_components,
359    );
360
361    let mut content =
362        re_ui::list_item::PropertyContent::new(component_descr.archetype_field_name())
363            .with_icon(icon)
364            .value_fn(|ui, _| {
365                if instance.is_all() {
366                    crate::ComponentPathLatestAtResults {
367                        component_path: ComponentPath::new(
368                            entity_path.clone(),
369                            component_descr.component,
370                        ),
371                        unit,
372                    }
373                    .data_ui(ctx, ui, UiLayout::List, query, db);
374                } else {
375                    ctx.component_ui_registry().component_ui(
376                        ctx,
377                        ui,
378                        UiLayout::List,
379                        query,
380                        db,
381                        entity_path,
382                        component_descr,
383                        unit,
384                        instance,
385                    );
386                }
387            });
388
389    if let Some(data) = &data
390        && ui_layout == UiLayout::SelectionPanel
391    {
392        content = data
393            .add_inline_buttons(ctx, MainThreadToken::from_egui_ui(ui), entity_path, content)
394            .with_always_show_buttons(true);
395    }
396
397    let response = list_item.show_flat(ui, content).on_hover_ui(|ui| {
398        if let Some(component_type) = component_descr.component_type {
399            component_type.data_ui_recording(ctx, ui, UiLayout::Tooltip);
400        }
401
402        if let Some(array) = unit.component_batch_raw(component_descr.component) {
403            re_ui::list_item::list_item_scope(ui, component_descr, |ui| {
404                ui.list_item_flat_noninteractive(
405                    re_ui::list_item::PropertyContent::new("Data type").value_text(
406                        // TODO(#11071): use re_arrow_ui to format the datatype here
407                        re_arrow_util::format_data_type(array.data_type()),
408                    ),
409                );
410            });
411        }
412    });
413
414    if interactive {
415        ctx.handle_select_hover_drag_interactions(&response, item, false);
416    }
417}
418
419pub fn archetype_label_list_item_ui(ui: &mut egui::Ui, archetype: &Option<ArchetypeName>) {
420    ui.list_item()
421        .with_y_offset(1.0)
422        .with_height(20.0)
423        .interactive(false)
424        .show_flat(
425            ui,
426            list_item::LabelContent::new(
427                RichText::new(
428                    archetype
429                        .map(|a| a.short_name())
430                        .unwrap_or("Without archetype"),
431                )
432                .size(10.0)
433                .color(design_tokens_of_visuals(ui.visuals()).list_item_strong_text),
434            ),
435        );
436}