1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
use std::fmt::Write as _;
use indexmap::IndexMap;
use itertools::Itertools as _;
use re_chunk::EntityPath;
use re_entity_db::EntityDb;
use re_log_types::StoreKind;
use re_sdk_types::external::glam;
use re_tf::TransformFrameIdHash;
use crate::{DataResultInteractionAddress, Item, ViewId, resolve_mono_instance_path_item};
/// Context information that a view might attach to an item from [`ItemCollection`] and useful
/// for how a selection might be displayed and interacted with.
#[derive(Clone, Debug, PartialEq)]
pub enum ItemContext {
/// Hovering/Selecting in a 2D space.
TwoD {
/// The target frame of the 2D (sub)space in which the pointer is hovering.
space_2d_target_frame: TransformFrameIdHash,
/// Where in this 2D space (+ depth)?
pos: glam::Vec3,
},
/// Hovering/Selecting in a 3D space.
ThreeD {
/// The target frame of the 3D space in which the pointer is hovering.
space_3d_target_frame: TransformFrameIdHash,
/// The point in 3D space that is hovered, if any.
pos: Option<glam::Vec3>,
/// Path to an entity that is currently tracked by the eye-camera.
/// (None for a free floating Eye)
tracked_entity: Option<EntityPath>,
/// Corresponding 2D spaces and pixel coordinates (with Z=depth)
point_in_2d_spaces: Vec<(TransformFrameIdHash, Option<glam::Vec3>)>,
},
/// Hovering/selecting in one of the streams trees.
StreamsTree {
/// Which store does this streams tree correspond to?
store_kind: StoreKind,
/// The current entity filter session id, if any.
filter_session_id: Option<egui::Id>,
},
/// Hovering/selecting in the blueprint tree.
BlueprintTree {
/// The current entity filter session id, if any.
filter_session_id: Option<egui::Id>,
},
}
/// An ordered collection of [`Item`] and optional associated context objects.
///
/// Used to store what is currently selected and/or hovered.
#[derive(Debug, Default, Clone, PartialEq)]
pub struct ItemCollection(IndexMap<Item, Option<ItemContext>>);
impl From<Item> for ItemCollection {
#[inline]
fn from(val: Item) -> Self {
Self([(val, None)].into())
}
}
impl ItemCollection {
pub fn from_items_and_context(
items: impl IntoIterator<Item = (Item, Option<ItemContext>)>,
) -> Self {
Self(items.into_iter().collect())
}
/// Is this view the selected one (and no other)?
pub fn is_view_the_only_selected(&self, needle: &ViewId) -> bool {
let mut is_selected = false;
for item in self.iter_items() {
if item.view_id() == Some(*needle) {
is_selected = true;
} else {
return false; // More than one view selected
}
}
is_selected
}
}
impl IntoIterator for ItemCollection {
type Item = (Item, Option<ItemContext>);
type IntoIter = indexmap::map::IntoIter<Item, Option<ItemContext>>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl ItemCollection {
/// For each item in this selection, if it refers to the first element of an instance with a
/// single element, resolve it to a unindexed entity path.
pub fn into_mono_instance_path_items(
self,
entity_db: &EntityDb,
query: &re_chunk_store::LatestAtQuery,
) -> Self {
Self(
self.0
.into_iter()
.map(|(item, item_context)| {
(
resolve_mono_instance_path_item(entity_db, query, &item),
item_context,
)
})
.collect(),
)
}
/// The first selected object if any.
pub fn first_item(&self) -> Option<&Item> {
self.0.keys().next()
}
/// Check if the selection contains a single item and returns it if so.
pub fn single_item(&self) -> Option<&Item> {
if self.len() == 1 {
self.first_item()
} else {
None
}
}
pub fn iter_items(&self) -> impl Iterator<Item = &Item> {
self.0.keys()
}
pub fn iter_item_context(&self) -> impl Iterator<Item = &ItemContext> {
self.0
.iter()
.filter_map(|(_, item_context)| item_context.as_ref())
}
pub fn context_for_item(&self, item: &Item) -> Option<&ItemContext> {
self.0.get(item).and_then(Option::as_ref)
}
/// Returns true if the exact selection is part of the current selection.
pub fn contains_item(&self, needle: &Item) -> bool {
self.0.iter().any(|(item, _)| item == needle)
}
/// Retains elements that fulfill a certain condition.
pub fn retain(&mut self, f: impl FnMut(&Item, &mut Option<ItemContext>) -> bool) {
self.0.retain(f);
}
/// Returns the number of items in the selection.
pub fn len(&self) -> usize {
self.0.len()
}
/// Check if the selection is empty.
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
/// Returns an iterator over the items and their selected context.
pub fn iter(&self) -> impl Iterator<Item = (&Item, &Option<ItemContext>)> {
self.0.iter()
}
/// Returns a mutable iterator over the items and their selected context.
pub fn iter_mut(&mut self) -> impl Iterator<Item = (&Item, &mut Option<ItemContext>)> {
self.0.iter_mut()
}
/// Extend the selection with more items.
pub fn extend(&mut self, other: impl IntoIterator<Item = (Item, Option<ItemContext>)>) {
self.0.extend(other);
}
/// Tries to copy a description of the selection to the clipboard.
///
/// Only certain elements are copyable right now.
pub fn copy_to_clipboard(&self, egui_ctx: &egui::Context) {
if self.is_empty() {
return;
}
use re_log_channel::LogSource;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum ClipboardTextDesc {
FilePath,
Url,
AppId,
StoreId,
EntityPath,
ComponentPath,
}
#[expect(clippy::match_same_arms)]
let clipboard_texts_per_type = self
.iter()
.filter_map(|(item, _)| match item {
Item::Container(_) => None,
Item::ComponentPath(component_path) => {
Some((ClipboardTextDesc::ComponentPath, component_path.to_string()))
}
Item::View(_) => None,
// TODO(lucasmerlin): Should these be copyable as URLs?
Item::RedapServer(_) => None,
Item::RedapEntry { .. } => None,
Item::TableId(_) => None, // TODO(grtlr): Make `TableId`s copyable too
Item::DataSource(source) => match source {
LogSource::File { path } => {
Some((ClipboardTextDesc::FilePath, path.to_string_lossy().into()))
}
LogSource::HttpStream { url } => Some((ClipboardTextDesc::Url, url.clone())),
LogSource::RrdWebEvent => None,
LogSource::JsChannel { .. } => None,
LogSource::Sdk => None,
LogSource::Stdin => None,
LogSource::RedapGrpcStream { uri, .. } => {
Some((ClipboardTextDesc::Url, uri.to_string()))
}
LogSource::MessageProxy(uri) => Some((ClipboardTextDesc::Url, uri.to_string())),
},
Item::AppId(id) => Some((ClipboardTextDesc::AppId, id.to_string())),
// TODO(ab): it is not very meaningful to copy the `StoreId` representation, but
// that's the best we can do for now. In the future, we should have URIs for
// in-memory recordings, and that's what we should copy here.
Item::StoreId(id) => Some((ClipboardTextDesc::StoreId, format!("{id:?}"))),
Item::DataResult(DataResultInteractionAddress { instance_path, .. })
| Item::InstancePath(instance_path) => Some((
ClipboardTextDesc::EntityPath,
instance_path.entity_path.to_string(),
)),
})
.chunk_by(|(desc, _)| *desc);
let mut clipboard_text = String::new();
let mut content_description = String::new();
for (desc, entries) in &clipboard_texts_per_type {
let entries = entries.map(|(_, text)| text).collect_vec();
let desc = match desc {
ClipboardTextDesc::FilePath => "file path",
ClipboardTextDesc::Url => "URL",
ClipboardTextDesc::AppId => "app id",
ClipboardTextDesc::StoreId => "store id",
ClipboardTextDesc::EntityPath => "entity path",
ClipboardTextDesc::ComponentPath => "component path",
};
if !content_description.is_empty() {
content_description.push_str(", ");
}
if entries.len() == 1 {
// Singular
content_description.push_str(desc);
} else {
// Plural
write!(content_description, "{desc}s").ok();
}
let texts = entries.into_iter().join("\n");
if !clipboard_text.is_empty() {
clipboard_text.push('\n');
}
clipboard_text.push_str(&texts);
}
if !clipboard_text.is_empty() {
re_log::info!(
"Copied {content_description} to clipboard:\n{}",
&clipboard_text
);
egui_ctx.copy_text(clipboard_text);
}
}
}