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
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
use std::panic::Location;
use re_chunk::EntityPath;
use re_chunk_store::external::re_chunk::Chunk;
use re_data_source::LogDataSource;
use re_log_channel::LogReceiver;
use re_log_types::StoreId;
use re_ui::{UICommand, UICommandSender};
use crate::time_control::TimeControlCommand;
use crate::{AuthContext, RecordingOrTable, ScreenshotTarget, ViewId};
// ----------------------------------------------------------------------------
/// Commands used by internal system components
// TODO(jleibs): Is there a better crate for this?
#[derive(strum_macros::IntoStaticStr)]
pub enum SystemCommand {
/// Make this the active application.
ActivateApp(re_log_types::ApplicationId),
/// Close this app and all its recordings.
CloseApp(re_log_types::ApplicationId),
/// Load data from a given data source.
///
/// Will not load any new data if the source is already one of the active data sources.
LoadDataSource(LogDataSource),
/// Add a new receiver for log messages.
AddReceiver(LogReceiver),
/// Add a new server to the redap browser.
AddRedapServer(re_uri::Origin),
/// Open a modal to edit this redap server.
EditRedapServerModal(EditRedapServerModalCommand),
ChangeDisplayMode(crate::DisplayMode),
/// Activates the setting display mode.
OpenSettings,
/// Activates the chunk store display mode.
OpenChunkStoreBrowser,
/// Sets the display mode to what it is at startup.
ResetDisplayMode,
/// Reset the `Viewer` to the default state
ResetViewer,
/// Clear the active blueprint.
///
/// This may have two outcomes:
/// - If a default blueprint is set, it will be used.
/// - Otherwise, the heuristics will be enabled.
///
/// To force using the heuristics, use [`Self::ClearActiveBlueprintAndEnableHeuristics`].
///
/// UI note: because of the above ambiguity, controls for this command should only be enabled if
/// a default blueprint is set or the behavior is explicitly explained.
ClearActiveBlueprint,
/// Clear the active blueprint and enable heuristics.
///
/// The final outcome of this is to set the active blueprint to the heuristics. This command
/// does not affect the default blueprint if any was set.
ClearActiveBlueprintAndEnableHeuristics,
/// Switch to this [`RecordingOrTable`].
ActivateRecordingOrTable(RecordingOrTable),
/// Close an [`RecordingOrTable`] and free its memory.
CloseRecordingOrTable(RecordingOrTable),
/// Close all stores and show the welcome screen again.
CloseAllEntries,
/// Add more data to a store (blueprint or recording).
///
/// Edit recordings with case: we generally regard recordings as immutable.
///
/// For blueprints,the [`StoreId`] should generally be the currently selected blueprint.
///
/// Instead of using this directly, consider using `save_blueprint_archetype` or similar.
AppendToStore(StoreId, Vec<Chunk>),
UndoBlueprint {
blueprint_id: StoreId,
},
RedoBlueprint {
blueprint_id: StoreId,
},
/// Drop a specific entity from a store.
///
/// Also drops all recursive children.
///
/// The [`StoreId`] should generally be the currently selected blueprint
/// but is tracked manually to ensure self-consistency if the blueprint
/// is both modified and changed in the same frame.
DropEntity(StoreId, EntityPath),
/// Show a timeline of the blueprint data.
#[cfg(debug_assertions)]
EnableInspectBlueprintTimeline(bool),
/// Navigate to time/entities/anchors/etc. that are set in a [`re_uri::Fragment`].
SetUrlFragment {
store_id: StoreId,
fragment: re_uri::Fragment,
},
/// Copies the given url to the clipboard.
///
/// On web this adds the viewer url as the base url.
CopyViewerUrl(String),
/// Set the item selection.
SetSelection(SetSelection),
TimeControlCommands {
store_id: StoreId,
time_commands: Vec<TimeControlCommand>,
},
/// Sets the focus to the given item.
///
/// The focused item is cleared out every frame.
/// Focusing is triggered either explicitly by ui-elements saying so
/// or by double-clicking on a button representing an item.
///
/// Unlike item selection, item focusing is not global state.
/// It may however have stateful effects in certain views,
/// e.g. the 3D view may follow the last focused item as it moves,
/// or a frame may be highlighted for a few frames.
///
/// Just like selection highlighting, the exact behavior of focusing is up to the receiving views.
SetFocus(crate::Item),
/// Show a notification to the user
ShowNotification(re_ui::notifications::Notification),
/// Add a task, run on a background thread, that saves something to disk.
#[cfg(not(target_arch = "wasm32"))]
FileSaver(Box<dyn FnOnce() -> anyhow::Result<std::path::PathBuf> + Send + 'static>),
/// Notify about authentication changes.
OnAuthChanged(Option<AuthContext>),
/// Set authentication credentials from an external source.
SetAuthCredentials {
access_token: String,
email: String,
},
/// Logout from rerun cloud
Logout,
/// Save a screenshot to a file.
SaveScreenshot {
/// Where to save the screenshot.
target: ScreenshotTarget,
/// Optional view id to screenshot a specific view.
/// If None, screenshots the entire viewer.
view_id: Option<ViewId>,
},
}
impl SystemCommand {
pub fn clear_selection() -> Self {
Self::set_selection(crate::ItemCollection::default())
}
pub fn set_selection(selection: impl Into<SetSelection>) -> Self {
Self::SetSelection(selection.into())
}
}
/// What triggered this item to be selected?
///
/// See [`crate::ViewerContext::handle_select_focus_sync`] why this is useful.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SelectionSource {
ListItemNavigation,
Other,
}
pub struct SetSelection {
pub selection: crate::ItemCollection,
pub source: SelectionSource,
}
impl SetSelection {
pub fn new(selection: impl Into<crate::ItemCollection>) -> Self {
Self {
selection: selection.into(),
source: SelectionSource::Other,
}
}
pub fn with_source(mut self, source: SelectionSource) -> Self {
self.source = source;
self
}
}
impl<T: Into<crate::ItemCollection>> From<T> for SetSelection {
fn from(selection: T) -> Self {
Self {
selection: selection.into(),
source: SelectionSource::Other,
}
}
}
impl SystemCommand {
/// A short debug name for this command.
pub fn debug_name(&self) -> &'static str {
self.into()
}
}
impl std::fmt::Debug for SystemCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// not all variant contents can be made `Debug`, so we only output the variant name
f.write_str(self.into())
}
}
/// Interface for sending [`SystemCommand`] messages.
pub trait SystemCommandSender {
fn send_system(&self, command: SystemCommand);
}
// ----------------------------------------------------------------------------
pub type StaticLocation = &'static Location<'static>;
/// Sender that queues up the execution of commands.
#[derive(Clone)]
pub struct CommandSender {
system_sender: crossbeam::channel::Sender<(StaticLocation, SystemCommand)>,
ui_sender: crossbeam::channel::Sender<UICommand>,
}
/// Receiver for the [`CommandSender`]
pub struct CommandReceiver {
system_receiver: crossbeam::channel::Receiver<(StaticLocation, SystemCommand)>,
ui_receiver: crossbeam::channel::Receiver<UICommand>,
}
impl CommandReceiver {
/// Receive a [`SystemCommand`] to be executed if any is queued.
///
/// Includes where it was sent from.
pub fn recv_system(&self) -> Option<(StaticLocation, SystemCommand)> {
// The only way this can fail (other than being empty)
// is if the sender has been dropped.
self.system_receiver.try_recv().ok()
}
/// Receive a [`UICommand`] to be executed if any is queued.
pub fn recv_ui(&self) -> Option<UICommand> {
// The only way this can fail (other than being empty)
// is if the sender has been dropped.
self.ui_receiver.try_recv().ok()
}
}
/// Creates a new command channel.
pub fn command_channel() -> (CommandSender, CommandReceiver) {
// We need an unbounded channel here, because we often send messages on the same
// thread as we receive them on (the main GUI thread).
#![cfg_attr(not(target_arch = "wasm32"), expect(clippy::disallowed_methods))]
let (system_sender, system_receiver) = crossbeam::channel::unbounded();
let (ui_sender, ui_receiver) = crossbeam::channel::unbounded();
(
CommandSender {
system_sender,
ui_sender,
},
CommandReceiver {
system_receiver,
ui_receiver,
},
)
}
// ----------------------------------------------------------------------------
impl SystemCommandSender for CommandSender {
/// Send a command to be executed.
#[track_caller]
fn send_system(&self, command: SystemCommand) {
// The only way this can fail is if the receiver has been dropped.
re_quota_channel::send_crossbeam(&self.system_sender, (Location::caller(), command)).ok();
}
}
impl UICommandSender for CommandSender {
/// Send a command to be executed.
fn send_ui(&self, command: UICommand) {
// The only way this can fail is if the receiver has been dropped.
re_quota_channel::send_crossbeam(&self.ui_sender, command).ok();
}
}
/// Command to open the edit redap server modal.
///
/// This exists as a separate struct to make it convenient to funnel it through the redap browser
/// command system.
#[derive(Debug)]
pub struct EditRedapServerModalCommand {
/// Which server should be edited?
pub origin: re_uri::Origin,
/// Provide a custom url to open when the server was successfully edited.
///
/// By default, the server dataset table is opened.
pub open_on_success: Option<String>,
/// Optional custom title for the modal.
pub title: Option<String>,
}
impl EditRedapServerModalCommand {
pub fn new(origin: re_uri::Origin) -> Self {
Self {
origin,
open_on_success: None,
title: None,
}
}
}