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 let component = if ctx.recording().is_known_entity(entity_path) {
37 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 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 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 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 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 let mut show_only_instanced = false;
137
138 if !self.is_all() {
139 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 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 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 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 } 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 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}