use std::sync::Arc;
use re_build_info::CrateVersion;
use re_data_source::{DataSource, FileContents};
use re_entity_db::entity_db::EntityDb;
use re_log_types::{ApplicationId, FileSource, LogMsg, StoreKind};
use re_renderer::WgpuResourcePoolStatistics;
use re_smart_channel::{ReceiveSet, SmartChannelSource};
use re_ui::{toasts, DesignTokens, UICommand, UICommandSender};
use re_viewer_context::{
command_channel,
store_hub::{BlueprintPersistence, StoreHub, StoreHubStats},
AppOptions, CommandReceiver, CommandSender, ComponentUiRegistry, PlayState, SpaceViewClass,
SpaceViewClassRegistry, SpaceViewClassRegistryError, StoreContext, SystemCommand,
SystemCommandSender,
};
use crate::app_blueprint::PanelStateOverrides;
use crate::{
app_blueprint::AppBlueprint, app_state::WelcomeScreenState, background_tasks::BackgroundTasks,
AppState,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum TimeControlCommand {
TogglePlayPause,
StepBack,
StepForward,
Restart,
Follow,
}
#[derive(Clone)]
pub struct StartupOptions {
pub memory_limit: re_memory::MemoryLimit,
pub persist_state: bool,
pub is_in_notebook: bool,
#[cfg(target_arch = "wasm32")]
pub location: Option<eframe::Location>,
#[cfg(not(target_arch = "wasm32"))]
pub screenshot_to_path_then_quit: Option<std::path::PathBuf>,
pub hide_welcome_screen: bool,
#[cfg(not(target_arch = "wasm32"))]
pub resolution_in_points: Option<[f32; 2]>,
pub expect_data_soon: Option<bool>,
pub force_wgpu_backend: Option<String>,
pub video_decoder_hw_acceleration: Option<re_video::decode::DecodeHardwareAcceleration>,
#[cfg(target_arch = "wasm32")]
pub fullscreen_options: Option<crate::web::FullscreenOptions>,
pub panel_state_overrides: PanelStateOverrides,
#[cfg(target_arch = "wasm32")]
pub enable_history: bool,
}
impl Default for StartupOptions {
fn default() -> Self {
Self {
memory_limit: re_memory::MemoryLimit::from_fraction_of_total(0.75),
persist_state: true,
is_in_notebook: false,
#[cfg(target_arch = "wasm32")]
location: None,
#[cfg(not(target_arch = "wasm32"))]
screenshot_to_path_then_quit: None,
hide_welcome_screen: false,
#[cfg(not(target_arch = "wasm32"))]
resolution_in_points: None,
expect_data_soon: None,
force_wgpu_backend: None,
video_decoder_hw_acceleration: None,
#[cfg(target_arch = "wasm32")]
fullscreen_options: Default::default(),
panel_state_overrides: Default::default(),
#[cfg(target_arch = "wasm32")]
enable_history: false,
}
}
}
#[cfg(not(target_arch = "wasm32"))]
const MIN_ZOOM_FACTOR: f32 = 0.2;
#[cfg(not(target_arch = "wasm32"))]
const MAX_ZOOM_FACTOR: f32 = 5.0;
#[cfg(target_arch = "wasm32")]
struct PendingFilePromise {
recommended_application_id: Option<ApplicationId>,
recommended_recording_id: Option<re_log_types::StoreId>,
force_store_info: bool,
promise: poll_promise::Promise<Vec<re_data_source::FileContents>>,
}
pub struct App {
build_info: re_build_info::BuildInfo,
startup_options: StartupOptions,
start_time: web_time::Instant,
ram_limit_warner: re_memory::RamLimitWarner,
pub(crate) egui_ctx: egui::Context,
screenshotter: crate::screenshotter::Screenshotter,
#[cfg(target_arch = "wasm32")]
pub(crate) popstate_listener: Option<crate::history::PopstateListener>,
#[cfg(not(target_arch = "wasm32"))]
profiler: re_tracing::Profiler,
text_log_rx: std::sync::mpsc::Receiver<re_log::LogMsg>,
component_ui_registry: ComponentUiRegistry,
rx: ReceiveSet<LogMsg>,
#[cfg(target_arch = "wasm32")]
open_files_promise: Option<PendingFilePromise>,
pub(crate) state: AppState,
pub(crate) background_tasks: BackgroundTasks,
pub(crate) store_hub: Option<StoreHub>,
toasts: toasts::Toasts,
memory_panel: crate::memory_panel::MemoryPanel,
memory_panel_open: bool,
egui_debug_panel_open: bool,
pub(crate) latest_queue_interest: web_time::Instant,
pub(crate) frame_time_history: egui::util::History<f32>,
pub command_sender: CommandSender,
command_receiver: CommandReceiver,
cmd_palette: re_ui::CommandPalette,
analytics: crate::viewer_analytics::ViewerAnalytics,
space_view_class_registry: SpaceViewClassRegistry,
pub(crate) panel_state_overrides_active: bool,
pub(crate) panel_state_overrides: PanelStateOverrides,
reflection: re_types_core::reflection::Reflection,
}
impl App {
pub fn new(
build_info: re_build_info::BuildInfo,
app_env: &crate::AppEnvironment,
startup_options: StartupOptions,
egui_ctx: egui::Context,
storage: Option<&dyn eframe::Storage>,
) -> Self {
re_tracing::profile_function!();
let analytics =
crate::viewer_analytics::ViewerAnalytics::new(&startup_options, app_env.clone());
let (logger, text_log_rx) = re_log::ChannelLogger::new(re_log::LevelFilter::Info);
if re_log::add_boxed_logger(Box::new(logger)).is_err() {
re_log::debug!(
"re_log not initialized - we won't see any log messages as GUI notifications"
);
}
let mut state: AppState = if startup_options.persist_state {
storage
.and_then(|storage| {
storage.get_string(eframe::APP_KEY).and_then(|value| {
match ron::from_str(&value) {
Ok(value) => Some(value),
Err(err) => {
re_log::warn!("Failed to restore application state. This is expected if you have just upgraded Rerun versions.");
re_log::debug!("Failed to decode RON for app state: {err}");
None
}
}
})
})
.unwrap_or_default()
} else {
AppState::default()
};
if let Some(video_decoder_hw_acceleration) = startup_options.video_decoder_hw_acceleration {
state.app_options.video_decoder_hw_acceleration = video_decoder_hw_acceleration;
}
let mut space_view_class_registry = SpaceViewClassRegistry::default();
if let Err(err) =
populate_space_view_class_registry_with_builtin(&mut space_view_class_registry)
{
re_log::error!(
"Failed to populate the view type registry with built-in space views: {}",
err
);
}
#[allow(unused_mut, clippy::needless_update)] let mut screenshotter = crate::screenshotter::Screenshotter::default();
#[cfg(not(target_arch = "wasm32"))]
if let Some(screenshot_path) = startup_options.screenshot_to_path_then_quit.clone() {
screenshotter.screenshot_to_path_then_quit(&egui_ctx, screenshot_path);
}
let (command_sender, command_receiver) = command_channel();
let mut component_ui_registry = re_component_ui::create_component_ui_registry();
re_data_ui::register_component_uis(&mut component_ui_registry);
let long_time_ago = web_time::Instant::now()
.checked_sub(web_time::Duration::from_secs(1_000_000_000))
.unwrap_or(web_time::Instant::now());
analytics.on_viewer_started(build_info);
let panel_state_overrides = startup_options.panel_state_overrides;
let reflection = crate::reflection::generate_reflection().unwrap_or_else(|err| {
re_log::error!(
"Failed to create list of serialized default values for components: {err}"
);
Default::default()
});
Self {
build_info,
startup_options,
start_time: web_time::Instant::now(),
ram_limit_warner: re_memory::RamLimitWarner::warn_at_fraction_of_max(0.75),
egui_ctx,
screenshotter,
#[cfg(target_arch = "wasm32")]
popstate_listener: None,
#[cfg(not(target_arch = "wasm32"))]
profiler: Default::default(),
text_log_rx,
component_ui_registry,
rx: Default::default(),
#[cfg(target_arch = "wasm32")]
open_files_promise: Default::default(),
state,
background_tasks: Default::default(),
store_hub: Some(StoreHub::new(
blueprint_loader(),
&crate::app_blueprint::setup_welcome_screen_blueprint,
)),
toasts: toasts::Toasts::new(),
memory_panel: Default::default(),
memory_panel_open: false,
egui_debug_panel_open: false,
latest_queue_interest: long_time_ago,
frame_time_history: egui::util::History::new(1..100, 0.5),
command_sender,
command_receiver,
cmd_palette: Default::default(),
space_view_class_registry,
analytics,
panel_state_overrides_active: true,
panel_state_overrides,
reflection,
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn set_profiler(&mut self, profiler: re_tracing::Profiler) {
self.profiler = profiler;
}
pub fn set_examples_manifest_url(&mut self, url: String) {
re_log::info!("Using manifest_url={url:?}");
self.state.set_examples_manifest_url(&self.egui_ctx, url);
}
pub fn build_info(&self) -> &re_build_info::BuildInfo {
&self.build_info
}
pub fn app_options(&self) -> &AppOptions {
self.state.app_options()
}
pub fn app_options_mut(&mut self) -> &mut AppOptions {
self.state.app_options_mut()
}
pub fn is_screenshotting(&self) -> bool {
self.screenshotter.is_screenshotting()
}
pub fn add_receiver(&mut self, rx: re_smart_channel::Receiver<LogMsg>) {
#[cfg(not(target_arch = "wasm32"))]
let rx = crate::wake_up_ui_thread_on_each_msg(rx, self.egui_ctx.clone());
self.rx.add(rx);
}
pub fn msg_receive_set(&self) -> &ReceiveSet<LogMsg> {
&self.rx
}
pub fn add_space_view_class<T: SpaceViewClass + Default + 'static>(
&mut self,
) -> Result<(), SpaceViewClassRegistryError> {
self.space_view_class_registry.add_class::<T>()
}
fn check_keyboard_shortcuts(&self, egui_ctx: &egui::Context) {
if let Some(cmd) = UICommand::listen_for_kb_shortcut(egui_ctx) {
self.command_sender.send_ui(cmd);
}
}
fn run_pending_system_commands(&mut self, store_hub: &mut StoreHub, egui_ctx: &egui::Context) {
while let Some(cmd) = self.command_receiver.recv_system() {
self.run_system_command(cmd, store_hub, egui_ctx);
}
}
fn run_pending_ui_commands(
&mut self,
egui_ctx: &egui::Context,
app_blueprint: &AppBlueprint<'_>,
store_context: Option<&StoreContext<'_>>,
) {
while let Some(cmd) = self.command_receiver.recv_ui() {
self.run_ui_command(egui_ctx, app_blueprint, store_context, cmd);
}
}
#[allow(clippy::unused_self)]
fn run_system_command(
&mut self,
cmd: SystemCommand,
store_hub: &mut StoreHub,
egui_ctx: &egui::Context,
) {
match cmd {
SystemCommand::ActivateApp(app_id) => {
store_hub.set_active_app(app_id);
}
SystemCommand::CloseApp(app_id) => {
store_hub.close_app(&app_id);
}
SystemCommand::ActivateRecording(store_id) => {
store_hub.set_activate_recording(store_id);
}
SystemCommand::CloseStore(store_id) => {
store_hub.remove(&store_id);
}
SystemCommand::CloseAllRecordings => {
store_hub.clear_recordings();
self.rx.retain(|r| match r.source() {
SmartChannelSource::File(_)
| SmartChannelSource::RrdHttpStream { .. }
| SmartChannelSource::RerunGrpcStream { .. } => false,
SmartChannelSource::WsClient { .. }
| SmartChannelSource::JsChannel { .. }
| SmartChannelSource::RrdWebEventListener
| SmartChannelSource::Sdk
| SmartChannelSource::TcpServer { .. }
| SmartChannelSource::Stdin => true,
});
}
SystemCommand::ClearSourceAndItsStores(source) => {
self.rx.retain(|r| r.source() != &source);
store_hub.retain(|db| db.data_source.as_ref() != Some(&source));
}
SystemCommand::AddReceiver(rx) => {
re_log::debug!("Received AddReceiver");
self.add_receiver(rx);
}
SystemCommand::LoadDataSource(data_source) => {
let egui_ctx = egui_ctx.clone();
let waker = Box::new(move || {
egui_ctx.request_repaint_after(std::time::Duration::from_millis(10));
});
match data_source.stream(Some(waker)) {
Ok(rx) => {
self.add_receiver(rx);
}
Err(err) => {
re_log::error!("Failed to open data source: {}", re_error::format(err));
}
}
}
SystemCommand::ResetViewer => self.reset_viewer(store_hub, egui_ctx),
SystemCommand::ClearAndGenerateBlueprint => {
re_log::debug!("Clear and generate new blueprint");
store_hub.clear_default_blueprint();
store_hub.clear_active_blueprint();
}
SystemCommand::ClearActiveBlueprint => {
re_log::debug!("Reset blueprint to default");
store_hub.clear_active_blueprint();
egui_ctx.request_repaint(); }
SystemCommand::UpdateBlueprint(blueprint_id, updates) => {
let blueprint_db = store_hub.entity_db_mut(&blueprint_id);
if self.state.app_options.inspect_blueprint_timeline {
let last_kept_event_time = self.state.blueprint_query_for_viewer().at();
let first_dropped_event_time = last_kept_event_time.inc();
blueprint_db.drop_time_range(
&re_viewer_context::blueprint_timeline(),
re_log_types::ResolvedTimeRange::new(
first_dropped_event_time,
re_chunk::TimeInt::MAX,
),
);
}
for chunk in updates {
match blueprint_db.add_chunk(&Arc::new(chunk)) {
Ok(_store_events) => {}
Err(err) => {
re_log::warn_once!("Failed to store blueprint delta: {err}");
}
}
}
let mut time_ctrl = self.state.blueprint_cfg.time_ctrl.write();
time_ctrl.set_play_state(blueprint_db.times_per_timeline(), PlayState::Following);
}
SystemCommand::DropEntity(blueprint_id, entity_path) => {
let blueprint_db = store_hub.entity_db_mut(&blueprint_id);
blueprint_db.drop_entity_path_recursive(&entity_path);
}
#[cfg(debug_assertions)]
SystemCommand::EnableInspectBlueprintTimeline(show) => {
self.app_options_mut().inspect_blueprint_timeline = show;
}
SystemCommand::SetSelection(item) => {
self.state.selection_state.set_selection(item);
}
SystemCommand::SetActiveTimeline { rec_id, timeline } => {
if let Some(rec_cfg) = self.state.recording_config_mut(&rec_id) {
rec_cfg.time_ctrl.write().set_timeline(timeline);
}
}
SystemCommand::SetFocus(item) => {
self.state.focused_item = Some(item);
}
#[cfg(not(target_arch = "wasm32"))]
SystemCommand::FileSaver(file_saver) => {
if let Err(err) = self.background_tasks.spawn_file_saver(file_saver) {
re_log::error!("Failed to save file: {err}");
}
}
}
}
fn run_ui_command(
&mut self,
egui_ctx: &egui::Context,
app_blueprint: &AppBlueprint<'_>,
store_context: Option<&StoreContext<'_>>,
cmd: UICommand,
) {
let mut force_store_info = false;
let active_application_id = store_context
.and_then(|ctx| {
ctx.hub
.active_app()
.filter(|&app_id| app_id != &StoreHub::welcome_screen_app_id())
.cloned()
})
.or_else(|| Some(uuid::Uuid::new_v4().to_string().into()));
let active_recording_id = store_context
.and_then(|ctx| ctx.hub.active_recording_id().cloned())
.or_else(|| {
force_store_info = true;
Some(re_log_types::StoreId::random(StoreKind::Recording))
});
match cmd {
UICommand::SaveRecording => {
if let Err(err) = save_recording(self, store_context, None) {
re_log::error!("Failed to save recording: {err}");
}
}
UICommand::SaveRecordingSelection => {
if let Err(err) = save_recording(
self,
store_context,
self.state.loop_selection(store_context),
) {
re_log::error!("Failed to save recording: {err}");
}
}
UICommand::SaveBlueprint => {
if let Err(err) = save_blueprint(self, store_context) {
re_log::error!("Failed to save blueprint: {err}");
}
}
#[cfg(not(target_arch = "wasm32"))]
UICommand::Open => {
for file_path in open_file_dialog_native() {
self.command_sender
.send_system(SystemCommand::LoadDataSource(DataSource::FilePath(
FileSource::FileDialog {
recommended_application_id: None,
recommended_recording_id: None,
force_store_info,
},
file_path,
)));
}
}
#[cfg(target_arch = "wasm32")]
UICommand::Open => {
let egui_ctx = egui_ctx.clone();
let promise = poll_promise::Promise::spawn_local(async move {
let file = async_open_rrd_dialog().await;
egui_ctx.request_repaint(); file
});
self.open_files_promise = Some(PendingFilePromise {
recommended_application_id: None,
recommended_recording_id: None,
force_store_info,
promise,
});
}
#[cfg(not(target_arch = "wasm32"))]
UICommand::Import => {
for file_path in open_file_dialog_native() {
self.command_sender
.send_system(SystemCommand::LoadDataSource(DataSource::FilePath(
FileSource::FileDialog {
recommended_application_id: active_application_id.clone(),
recommended_recording_id: active_recording_id.clone(),
force_store_info,
},
file_path,
)));
}
}
#[cfg(target_arch = "wasm32")]
UICommand::Import => {
let egui_ctx = egui_ctx.clone();
let promise = poll_promise::Promise::spawn_local(async move {
let file = async_open_rrd_dialog().await;
egui_ctx.request_repaint(); file
});
self.open_files_promise = Some(PendingFilePromise {
recommended_application_id: active_application_id.clone(),
recommended_recording_id: active_recording_id.clone(),
force_store_info,
promise,
});
}
UICommand::CloseCurrentRecording => {
let cur_rec = store_context.map(|ctx| ctx.recording.store_id());
if let Some(cur_rec) = cur_rec {
self.command_sender
.send_system(SystemCommand::CloseStore(cur_rec.clone()));
}
}
UICommand::CloseAllRecordings => {
self.command_sender
.send_system(SystemCommand::CloseAllRecordings);
}
#[cfg(not(target_arch = "wasm32"))]
UICommand::Quit => {
egui_ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
UICommand::OpenWebHelp => {
egui_ctx.open_url(egui::output::OpenUrl {
url: "https://www.rerun.io/docs/getting-started/navigating-the-viewer"
.to_owned(),
new_tab: true,
});
}
UICommand::OpenRerunDiscord => {
egui_ctx.open_url(egui::output::OpenUrl {
url: "https://discord.gg/PXtCgFBSmH".to_owned(),
new_tab: true,
});
}
UICommand::ResetViewer => self.command_sender.send_system(SystemCommand::ResetViewer),
UICommand::ClearAndGenerateBlueprint => {
self.command_sender
.send_system(SystemCommand::ClearAndGenerateBlueprint);
}
#[cfg(not(target_arch = "wasm32"))]
UICommand::OpenProfiler => {
self.profiler.start();
}
UICommand::ToggleMemoryPanel => {
self.memory_panel_open ^= true;
}
UICommand::TogglePanelStateOverrides => {
self.panel_state_overrides_active ^= true;
}
UICommand::ToggleTopPanel => {
app_blueprint.toggle_top_panel(&self.command_sender);
}
UICommand::ToggleBlueprintPanel => {
app_blueprint.toggle_blueprint_panel(&self.command_sender);
}
UICommand::ToggleSelectionPanel => {
app_blueprint.toggle_selection_panel(&self.command_sender);
}
UICommand::ToggleTimePanel => app_blueprint.toggle_time_panel(&self.command_sender),
UICommand::ToggleChunkStoreBrowser => self.state.show_datastore_ui ^= true,
#[cfg(debug_assertions)]
UICommand::ToggleBlueprintInspectionPanel => {
self.app_options_mut().inspect_blueprint_timeline ^= true;
}
#[cfg(debug_assertions)]
UICommand::ToggleEguiDebugPanel => {
self.egui_debug_panel_open ^= true;
}
UICommand::ToggleFullscreen => {
self.toggle_fullscreen();
}
UICommand::Settings => {
self.state.show_settings_ui = true;
}
#[cfg(not(target_arch = "wasm32"))]
UICommand::ZoomIn => {
let mut zoom_factor = egui_ctx.zoom_factor();
zoom_factor += 0.1;
zoom_factor = zoom_factor.clamp(MIN_ZOOM_FACTOR, MAX_ZOOM_FACTOR);
zoom_factor = (zoom_factor * 10.).round() / 10.;
egui_ctx.set_zoom_factor(zoom_factor);
}
#[cfg(not(target_arch = "wasm32"))]
UICommand::ZoomOut => {
let mut zoom_factor = egui_ctx.zoom_factor();
zoom_factor -= 0.1;
zoom_factor = zoom_factor.clamp(MIN_ZOOM_FACTOR, MAX_ZOOM_FACTOR);
zoom_factor = (zoom_factor * 10.).round() / 10.;
egui_ctx.set_zoom_factor(zoom_factor);
}
#[cfg(not(target_arch = "wasm32"))]
UICommand::ZoomReset => {
egui_ctx.set_zoom_factor(1.0);
}
UICommand::SelectionPrevious => {
self.state.selection_state.select_previous();
}
UICommand::SelectionNext => {
self.state.selection_state.select_next();
}
UICommand::ToggleCommandPalette => {
self.cmd_palette.toggle();
}
UICommand::PlaybackTogglePlayPause => {
self.run_time_control_command(store_context, TimeControlCommand::TogglePlayPause);
}
UICommand::PlaybackFollow => {
self.run_time_control_command(store_context, TimeControlCommand::Follow);
}
UICommand::PlaybackStepBack => {
self.run_time_control_command(store_context, TimeControlCommand::StepBack);
}
UICommand::PlaybackStepForward => {
self.run_time_control_command(store_context, TimeControlCommand::StepForward);
}
UICommand::PlaybackRestart => {
self.run_time_control_command(store_context, TimeControlCommand::Restart);
}
#[cfg(not(target_arch = "wasm32"))]
UICommand::ScreenshotWholeApp => {
self.screenshotter.request_screenshot(egui_ctx);
}
#[cfg(not(target_arch = "wasm32"))]
UICommand::PrintChunkStore => {
if let Some(ctx) = store_context {
let text = format!("{}", ctx.recording.storage_engine().store());
egui_ctx.output_mut(|o| o.copied_text = text.clone());
println!("{text}");
}
}
#[cfg(not(target_arch = "wasm32"))]
UICommand::PrintBlueprintStore => {
if let Some(ctx) = store_context {
let text = format!("{}", ctx.blueprint.storage_engine().store());
egui_ctx.output_mut(|o| o.copied_text = text.clone());
println!("{text}");
}
}
#[cfg(not(target_arch = "wasm32"))]
UICommand::PrintPrimaryCache => {
if let Some(ctx) = store_context {
let text = format!("{:?}", ctx.recording.storage_engine().cache());
egui_ctx.output_mut(|o| o.copied_text = text.clone());
println!("{text}");
}
}
#[cfg(debug_assertions)]
UICommand::ResetEguiMemory => {
egui_ctx.memory_mut(|mem| *mem = Default::default());
re_ui::apply_style_and_install_loaders(egui_ctx);
}
#[cfg(target_arch = "wasm32")]
UICommand::CopyDirectLink => {
self.run_copy_direct_link_command(store_context);
}
#[cfg(target_arch = "wasm32")]
UICommand::RestartWithWebGl => {
if crate::web_tools::set_url_parameter_and_refresh("renderer", "webgl").is_err() {
re_log::error!("Failed to set URL parameter `renderer=webgl` & refresh page.");
}
}
#[cfg(target_arch = "wasm32")]
UICommand::RestartWithWebGpu => {
if crate::web_tools::set_url_parameter_and_refresh("renderer", "webgpu").is_err() {
re_log::error!("Failed to set URL parameter `renderer=webgpu` & refresh page.");
}
}
}
}
fn run_time_control_command(
&mut self,
store_context: Option<&StoreContext<'_>>,
command: TimeControlCommand,
) {
let Some(entity_db) = store_context.as_ref().map(|ctx| ctx.recording) else {
return;
};
let rec_id = entity_db.store_id();
let Some(rec_cfg) = self.state.recording_config_mut(&rec_id) else {
return;
};
let time_ctrl = rec_cfg.time_ctrl.get_mut();
let times_per_timeline = entity_db.times_per_timeline();
match command {
TimeControlCommand::TogglePlayPause => {
time_ctrl.toggle_play_pause(times_per_timeline);
}
TimeControlCommand::Follow => {
time_ctrl.set_play_state(times_per_timeline, PlayState::Following);
}
TimeControlCommand::StepBack => {
time_ctrl.step_time_back(times_per_timeline);
}
TimeControlCommand::StepForward => {
time_ctrl.step_time_fwd(times_per_timeline);
}
TimeControlCommand::Restart => {
time_ctrl.restart(times_per_timeline);
}
}
}
#[cfg(target_arch = "wasm32")]
fn run_copy_direct_link_command(&mut self, store_context: Option<&StoreContext<'_>>) {
let location = web_sys::window().unwrap().location();
let origin = location.origin().unwrap();
let host = location.host().unwrap();
let pathname = location.pathname().unwrap();
let hosted_viewer_path = if self.build_info.is_final() {
format!("version/{}", self.build_info.version)
} else {
format!("commit/{}", self.build_info.short_git_hash())
};
let href = if host == "app.rerun.io" {
format!("https://app.rerun.io/{hosted_viewer_path}")
} else if host == "rerun.io" && pathname.starts_with("/viewer") {
format!("https://rerun.io/viewer/{hosted_viewer_path}")
} else {
format!("{origin}{pathname}")
};
let direct_link = match store_context
.map(|ctx| ctx.recording)
.and_then(|rec| rec.data_source.as_ref())
{
Some(SmartChannelSource::RrdHttpStream { url, .. }) => format!("{href}?url={url}"),
_ => href,
};
self.egui_ctx
.output_mut(|o| o.copied_text = direct_link.clone());
self.toasts.add(toasts::Toast {
kind: toasts::ToastKind::Success,
text: format!("Copied {direct_link:?} to clipboard"),
options: toasts::ToastOptions::with_ttl_in_seconds(4.0),
});
}
fn memory_panel_ui(
&mut self,
ui: &mut egui::Ui,
gpu_resource_stats: &WgpuResourcePoolStatistics,
store_stats: Option<&StoreHubStats>,
) {
let frame = egui::Frame {
fill: ui.visuals().panel_fill,
..DesignTokens::bottom_panel_frame()
};
egui::TopBottomPanel::bottom("memory_panel")
.default_height(300.0)
.resizable(true)
.frame(frame)
.show_animated_inside(ui, self.memory_panel_open, |ui| {
self.memory_panel.ui(
ui,
&self.startup_options.memory_limit,
gpu_resource_stats,
store_stats,
);
});
}
fn egui_debug_panel_ui(&mut self, ui: &mut egui::Ui) {
let egui_ctx = ui.ctx().clone();
egui::SidePanel::left("style_panel")
.default_width(300.0)
.resizable(true)
.frame(DesignTokens::top_panel_frame())
.show_animated_inside(ui, self.egui_debug_panel_open, |ui| {
egui::ScrollArea::vertical().show(ui, |ui| {
if ui
.button("request_discard")
.on_hover_text("Request a second layout pass. Just for testing.")
.clicked()
{
ui.ctx().request_discard("testing");
}
egui::CollapsingHeader::new("egui settings")
.default_open(false)
.show(ui, |ui| {
egui_ctx.settings_ui(ui);
});
egui::CollapsingHeader::new("egui inspection")
.default_open(false)
.show(ui, |ui| {
egui_ctx.inspection_ui(ui);
});
});
});
}
#[allow(clippy::too_many_arguments)]
fn ui(
&mut self,
egui_ctx: &egui::Context,
frame: &eframe::Frame,
app_blueprint: &AppBlueprint<'_>,
gpu_resource_stats: &WgpuResourcePoolStatistics,
store_context: Option<&StoreContext<'_>>,
store_stats: Option<&StoreHubStats>,
) {
let mut main_panel_frame = egui::Frame::default();
if re_ui::CUSTOM_WINDOW_DECORATIONS {
main_panel_frame.inner_margin = 1.0.into();
}
egui::CentralPanel::default()
.frame(main_panel_frame)
.show(egui_ctx, |ui| {
paint_background_fill(ui);
crate::ui::mobile_warning_ui(ui);
crate::ui::top_panel(
frame,
self,
app_blueprint,
store_context,
gpu_resource_stats,
ui,
);
self.memory_panel_ui(ui, gpu_resource_stats, store_stats);
self.egui_debug_panel_ui(ui);
let egui_renderer = {
let render_state = frame.wgpu_render_state().unwrap();
&mut render_state.renderer.write()
};
if let Some(render_ctx) = egui_renderer
.callback_resources
.get_mut::<re_renderer::RenderContext>()
{
if let Some(store_view) = store_context {
let entity_db = store_view.recording;
#[cfg(target_arch = "wasm32")]
let is_history_enabled = self.startup_options.enable_history;
#[cfg(not(target_arch = "wasm32"))]
let is_history_enabled = false;
render_ctx.begin_frame();
self.state.show(
app_blueprint,
ui,
render_ctx,
entity_db,
store_view,
&self.reflection,
&self.component_ui_registry,
&self.space_view_class_registry,
&self.rx,
&self.command_sender,
&WelcomeScreenState {
hide: self.startup_options.hide_welcome_screen,
opacity: self.welcome_screen_opacity(egui_ctx),
},
is_history_enabled,
);
render_ctx.before_submit();
}
}
});
}
fn show_text_logs_as_notifications(&mut self) {
re_tracing::profile_function!();
while let Ok(re_log::LogMsg { level, target, msg }) = self.text_log_rx.try_recv() {
let is_rerun_crate = target.starts_with("rerun") || target.starts_with("re_");
if !is_rerun_crate {
continue;
}
let kind = match level {
re_log::Level::Error => toasts::ToastKind::Error,
re_log::Level::Warn => toasts::ToastKind::Warning,
re_log::Level::Info => toasts::ToastKind::Info,
re_log::Level::Debug | re_log::Level::Trace => {
continue; }
};
self.toasts.add(toasts::Toast {
kind,
text: msg,
options: toasts::ToastOptions::with_ttl_in_seconds(4.0),
});
}
}
fn receive_messages(&mut self, store_hub: &mut StoreHub, egui_ctx: &egui::Context) {
re_tracing::profile_function!();
let start = web_time::Instant::now();
while let Some((channel_source, msg)) = self.rx.try_recv() {
re_log::trace!("Received a message from {channel_source:?}"); let msg = match msg.payload {
re_smart_channel::SmartMessagePayload::Msg(msg) => msg,
re_smart_channel::SmartMessagePayload::Flush { on_flush_done } => {
on_flush_done();
continue;
}
re_smart_channel::SmartMessagePayload::Quit(err) => {
if let Some(err) = err {
let log_msg =
format!("Data source {} has left unexpectedly: {err}", msg.source);
#[cfg(not(target_arch = "wasm32"))]
if err
.downcast_ref::<re_sdk_comms::ConnectionError>()
.is_some_and(|e| {
matches!(e, re_sdk_comms::ConnectionError::UnknownClient)
})
{
re_log::debug!("{log_msg}");
continue;
}
re_log::warn!("{log_msg}");
} else {
re_log::debug!("Data source {} has finished", msg.source);
}
continue;
}
};
let store_id = msg.store_id();
if store_hub.is_active_blueprint(store_id) {
re_log::warn_once!("Loading a blueprint {store_id} that is active. See https://github.com/rerun-io/rerun/issues/5514 for details.");
}
{
let entity_db = store_hub.entity_db_mut(store_id);
if entity_db.data_source.is_none() {
entity_db.data_source = Some((*channel_source).clone());
}
}
match store_hub.entity_db_mut(store_id).add(&msg) {
Ok(store_events) => {
if let Some(caches) = store_hub.active_caches() {
caches.on_store_events(&store_events);
}
self.validate_loaded_events(&store_events);
}
Err(err) => {
re_log::error_once!("Failed to add incoming msg: {err}");
}
}
let entity_db = store_hub.entity_db_mut(store_id);
match &msg {
LogMsg::SetStoreInfo(_) => {
match store_id.kind {
StoreKind::Recording => {
re_log::trace!("Opening a new recording: '{store_id}'");
store_hub.set_active_recording_id(store_id.clone());
self.command_sender.send_system(SystemCommand::SetSelection(
re_viewer_context::Item::StoreId(store_id.clone()),
));
egui_ctx.send_viewport_cmd(
egui::ViewportCommand::RequestUserAttention(
egui::UserAttentionType::Informational,
),
);
}
StoreKind::Blueprint => {
}
}
}
LogMsg::ArrowMsg(_, _) => {
}
LogMsg::BlueprintActivationCommand(cmd) => match store_id.kind {
StoreKind::Recording => {
re_log::debug!(
"Unexpected `BlueprintActivationCommand` message for {store_id}"
);
}
StoreKind::Blueprint => {
if let Some(info) = entity_db.store_info() {
re_log::trace!(
"Activating blueprint that was loaded from {channel_source}"
);
let app_id = info.application_id.clone();
if cmd.make_default {
store_hub
.set_default_blueprint_for_app(&app_id, store_id)
.unwrap_or_else(|err| {
re_log::warn!("Failed to make blueprint default: {err}");
});
}
if cmd.make_active {
store_hub
.set_cloned_blueprint_active_for_app(&app_id, store_id)
.unwrap_or_else(|err| {
re_log::warn!("Failed to make blueprint active: {err}");
});
store_hub.set_active_app(app_id); egui_ctx.send_viewport_cmd(
egui::ViewportCommand::RequestUserAttention(
egui::UserAttentionType::Informational,
),
);
}
} else {
re_log::warn!(
"Got ActivateStore message without first receiving a SetStoreInfo"
);
}
}
},
}
let entity_db = store_hub.entity_db_mut(store_id);
let is_new_store = matches!(&msg, LogMsg::SetStoreInfo(_msg));
if is_new_store && entity_db.store_kind() == StoreKind::Recording {
self.analytics.on_open_recording(entity_db);
}
if start.elapsed() > web_time::Duration::from_millis(10) {
egui_ctx.request_repaint(); break; }
}
}
fn validate_loaded_events(&self, store_events: &[re_chunk_store::ChunkStoreEvent]) {
re_tracing::profile_function!();
for event in store_events {
let chunk = &event.diff.chunk;
for component in chunk.component_names() {
if let Some(archetype_name) = component.indicator_component_archetype() {
if let Some(archetype) = self
.reflection
.archetype_reflection_from_short_name(&archetype_name)
{
for &view_type in archetype.view_types {
if !cfg!(feature = "map_view") && view_type == "MapView" {
re_log::warn_once!("Found map-related archetype, but viewer was not compiled with the `map_view` feature.");
}
}
} else {
re_log::debug_once!("Unknown archetype: {archetype_name}");
}
}
}
}
}
fn purge_memory_if_needed(&mut self, store_hub: &mut StoreHub) {
re_tracing::profile_function!();
fn format_limit(limit: Option<i64>) -> String {
if let Some(bytes) = limit {
format_bytes(bytes as _)
} else {
"∞".to_owned()
}
}
use re_format::format_bytes;
use re_memory::MemoryUse;
let limit = self.startup_options.memory_limit;
let mem_use_before = MemoryUse::capture();
if let Some(minimum_fraction_to_purge) = limit.is_exceeded_by(&mem_use_before) {
re_log::info_once!(
"Reached memory limit of {}, dropping oldest data.",
format_limit(limit.max_bytes)
);
let fraction_to_purge = (minimum_fraction_to_purge + 0.2).clamp(0.25, 1.0);
re_log::trace!("RAM limit: {}", format_limit(limit.max_bytes));
if let Some(resident) = mem_use_before.resident {
re_log::trace!("Resident: {}", format_bytes(resident as _),);
}
if let Some(counted) = mem_use_before.counted {
re_log::trace!("Counted: {}", format_bytes(counted as _));
}
re_tracing::profile_scope!("pruning");
if let Some(counted) = mem_use_before.counted {
re_log::trace!(
"Attempting to purge {:.1}% of used RAM ({})…",
100.0 * fraction_to_purge,
format_bytes(counted as f64 * fraction_to_purge as f64)
);
}
store_hub.purge_fraction_of_ram(fraction_to_purge);
let mem_use_after = MemoryUse::capture();
let freed_memory = mem_use_before - mem_use_after;
if let (Some(counted_before), Some(counted_diff)) =
(mem_use_before.counted, freed_memory.counted)
{
re_log::debug!(
"Freed up {} ({:.1}%)",
format_bytes(counted_diff as _),
100.0 * counted_diff as f32 / counted_before as f32
);
}
self.memory_panel.note_memory_purge();
}
}
fn reset_viewer(&mut self, store_hub: &mut StoreHub, egui_ctx: &egui::Context) {
self.state = Default::default();
store_hub.clear_all_cloned_blueprints();
egui_ctx.memory_mut(|mem| *mem = Default::default());
re_ui::apply_style_and_install_loaders(egui_ctx);
if let Err(err) = crate::reset_viewer_persistence() {
re_log::warn!("Failed to reset viewer: {err}");
}
}
pub fn recording_db(&self) -> Option<&EntityDb> {
self.store_hub
.as_ref()
.and_then(|store_hub| store_hub.active_recording())
}
fn handle_dropping_files(
egui_ctx: &egui::Context,
store_ctx: Option<&StoreContext<'_>>,
command_sender: &CommandSender,
) {
preview_files_being_dropped(egui_ctx);
let dropped_files = egui_ctx.input_mut(|i| std::mem::take(&mut i.raw.dropped_files));
if dropped_files.is_empty() {
return;
}
let mut force_store_info = false;
let active_application_id = store_ctx
.and_then(|ctx| {
ctx.hub
.active_app()
.filter(|&app_id| app_id != &StoreHub::welcome_screen_app_id())
.cloned()
})
.or_else(|| Some(uuid::Uuid::new_v4().to_string().into()));
let active_recording_id = store_ctx
.and_then(|ctx| ctx.hub.active_recording_id().cloned())
.or_else(|| {
force_store_info = true;
Some(re_log_types::StoreId::random(StoreKind::Recording))
});
for file in dropped_files {
if let Some(bytes) = file.bytes {
command_sender.send_system(SystemCommand::LoadDataSource(
DataSource::FileContents(
FileSource::DragAndDrop {
recommended_application_id: active_application_id.clone(),
recommended_recording_id: active_recording_id.clone(),
force_store_info,
},
FileContents {
name: file.name.clone(),
bytes: bytes.clone(),
},
),
));
continue;
}
#[cfg(not(target_arch = "wasm32"))]
if let Some(path) = file.path {
command_sender.send_system(SystemCommand::LoadDataSource(DataSource::FilePath(
FileSource::DragAndDrop {
recommended_application_id: active_application_id.clone(),
recommended_recording_id: active_recording_id.clone(),
force_store_info,
},
path,
)));
}
}
}
fn should_fade_in_welcome_screen(&self) -> bool {
if let Some(expect_data_soon) = self.startup_options.expect_data_soon {
return expect_data_soon;
}
for source in self.rx.sources() {
#[allow(clippy::match_same_arms)]
match &*source {
SmartChannelSource::File(_)
| SmartChannelSource::RrdHttpStream { .. }
| SmartChannelSource::RerunGrpcStream { .. }
| SmartChannelSource::Stdin
| SmartChannelSource::RrdWebEventListener
| SmartChannelSource::Sdk
| SmartChannelSource::WsClient { .. }
| SmartChannelSource::JsChannel { .. } => {
return true; }
SmartChannelSource::TcpServer { .. } => {
}
}
}
false }
fn welcome_screen_opacity(&self, egui_ctx: &egui::Context) -> f32 {
if self.should_fade_in_welcome_screen() {
let sec_since_first_shown = self.start_time.elapsed().as_secs_f32();
let opacity = egui::remap_clamp(sec_since_first_shown, 0.4..=0.6, 0.0..=1.0);
if opacity < 1.0 {
egui_ctx.request_repaint();
}
opacity
} else {
1.0
}
}
#[allow(clippy::unused_self)]
pub(crate) fn toggle_fullscreen(&self) {
#[cfg(not(target_arch = "wasm32"))]
{
let fullscreen = self
.egui_ctx
.input(|i| i.viewport().fullscreen.unwrap_or(false));
self.egui_ctx
.send_viewport_cmd(egui::ViewportCommand::Fullscreen(!fullscreen));
}
#[cfg(target_arch = "wasm32")]
{
if let Some(options) = &self.startup_options.fullscreen_options {
if let Err(err) = options.on_toggle.call() {
re_log::error!("{}", crate::web_tools::string_from_js_value(err));
};
}
}
}
#[cfg(target_arch = "wasm32")]
pub(crate) fn is_fullscreen_allowed(&self) -> bool {
self.startup_options.fullscreen_options.is_some()
}
#[cfg(target_arch = "wasm32")]
pub(crate) fn is_fullscreen_mode(&self) -> bool {
if let Some(options) = &self.startup_options.fullscreen_options {
match options.get_state.call() {
Ok(v) => return v.is_truthy(),
Err(err) => re_log::error_once!("{}", crate::web_tools::string_from_js_value(err)),
}
}
false
}
}
#[cfg(target_arch = "wasm32")]
fn blueprint_loader() -> BlueprintPersistence {
BlueprintPersistence {
loader: None,
saver: None,
validator: Some(Box::new(crate::blueprint::is_valid_blueprint)),
}
}
#[cfg(not(target_arch = "wasm32"))]
fn blueprint_loader() -> BlueprintPersistence {
use re_entity_db::StoreBundle;
fn load_blueprint_from_disk(app_id: &ApplicationId) -> anyhow::Result<Option<StoreBundle>> {
let blueprint_path = crate::saving::default_blueprint_path(app_id)?;
if !blueprint_path.exists() {
return Ok(None);
}
re_log::debug!("Trying to load blueprint for {app_id} from {blueprint_path:?}");
let with_notifications = false;
if let Some(bundle) =
crate::loading::load_blueprint_file(&blueprint_path, with_notifications)
{
for store in bundle.entity_dbs() {
if store.store_kind() == StoreKind::Blueprint
&& !crate::blueprint::is_valid_blueprint(store)
{
re_log::warn_once!("Blueprint for {app_id} at {blueprint_path:?} appears invalid - will ignore. This is expected if you have just upgraded Rerun versions.");
return Ok(None);
}
}
Ok(Some(bundle))
} else {
Ok(None)
}
}
#[cfg(not(target_arch = "wasm32"))]
fn save_blueprint_to_disk(app_id: &ApplicationId, blueprint: &EntityDb) -> anyhow::Result<()> {
let blueprint_path = crate::saving::default_blueprint_path(app_id)?;
let messages = blueprint.to_messages(None);
let rrd_version = blueprint
.store_info()
.and_then(|info| info.store_version)
.unwrap_or(re_build_info::CrateVersion::LOCAL);
crate::saving::encode_to_file(rrd_version, &blueprint_path, messages)?;
re_log::debug!("Saved blueprint for {app_id} to {blueprint_path:?}");
Ok(())
}
BlueprintPersistence {
loader: Some(Box::new(load_blueprint_from_disk)),
saver: Some(Box::new(save_blueprint_to_disk)),
validator: Some(Box::new(crate::blueprint::is_valid_blueprint)),
}
}
impl eframe::App for App {
fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
[0.0; 4] }
fn save(&mut self, storage: &mut dyn eframe::Storage) {
if !self.startup_options.persist_state {
return;
}
re_tracing::profile_function!();
eframe::set_value(storage, eframe::APP_KEY, &self.state);
if let Some(hub) = &mut self.store_hub {
if self.state.app_options.blueprint_gc {
hub.gc_blueprints();
}
if let Err(err) = hub.save_app_blueprints() {
re_log::error!("Saving blueprints failed: {err}");
}
} else {
re_log::error!("Could not save blueprints: the store hub is not available");
}
}
fn update(&mut self, egui_ctx: &egui::Context, frame: &mut eframe::Frame) {
if let Some(seconds) = frame.info().cpu_usage {
self.frame_time_history
.add(egui_ctx.input(|i| i.time), seconds);
}
#[cfg(target_arch = "wasm32")]
if self.startup_options.enable_history {
let back_pressed =
egui_ctx.input(|i| i.pointer.button_pressed(egui::PointerButton::Extra1));
let fwd_pressed =
egui_ctx.input(|i| i.pointer.button_pressed(egui::PointerButton::Extra2));
if back_pressed {
crate::history::go_back();
}
if fwd_pressed {
crate::history::go_forward();
}
}
let mut store_hub = self.store_hub.take().unwrap();
#[cfg(not(target_arch = "wasm32"))]
if let Some(resolution_in_points) = self.startup_options.resolution_in_points.take() {
egui_ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(
resolution_in_points.into(),
));
}
#[cfg(not(target_arch = "wasm32"))]
if self.screenshotter.update(egui_ctx).quit {
egui_ctx.send_viewport_cmd(egui::ViewportCommand::Close);
return;
}
if self.startup_options.memory_limit.is_unlimited() {
self.ram_limit_warner.update();
}
#[cfg(target_arch = "wasm32")]
if let Some(PendingFilePromise {
recommended_application_id,
recommended_recording_id,
force_store_info,
promise,
}) = &self.open_files_promise
{
if let Some(files) = promise.ready() {
for file in files {
self.command_sender
.send_system(SystemCommand::LoadDataSource(DataSource::FileContents(
FileSource::FileDialog {
recommended_application_id: recommended_application_id.clone(),
recommended_recording_id: recommended_recording_id.clone(),
force_store_info: *force_store_info,
},
file.clone(),
)));
}
self.open_files_promise = None;
}
}
let gpu_resource_stats = {
re_tracing::profile_scope!("gpu_resource_stats");
let egui_renderer = {
let render_state = frame.wgpu_render_state().unwrap();
&mut render_state.renderer.read()
};
let render_ctx = egui_renderer
.callback_resources
.get::<re_renderer::RenderContext>()
.unwrap();
render_ctx.gpu_resources.statistics()
};
let store_stats = self.memory_panel_open.then(|| store_hub.stats());
self.memory_panel
.update(&gpu_resource_stats, store_stats.as_ref());
self.check_keyboard_shortcuts(egui_ctx);
self.purge_memory_if_needed(&mut store_hub);
{
let egui_renderer = {
let render_state = frame.wgpu_render_state().unwrap();
&mut render_state.renderer.read()
};
let render_ctx = egui_renderer
.callback_resources
.get::<re_renderer::RenderContext>()
.unwrap();
let renderer_active_frame_idx = render_ctx.active_frame_idx().wrapping_add(1);
store_hub.begin_frame(renderer_active_frame_idx);
}
self.show_text_logs_as_notifications();
self.receive_messages(&mut store_hub, egui_ctx);
if self.app_options().blueprint_gc {
store_hub.gc_blueprints();
}
store_hub.purge_empty();
self.state.cleanup(&store_hub);
file_saver_progress_ui(egui_ctx, &mut self.background_tasks); if store_hub.active_app().is_none() {
let apps: std::collections::BTreeSet<&ApplicationId> = store_hub
.store_bundle()
.entity_dbs()
.filter_map(|db| db.app_id())
.filter(|&app_id| app_id != &StoreHub::welcome_screen_app_id())
.collect();
if let Some(app_id) = apps.first().copied() {
store_hub.set_active_app(app_id.clone());
} else {
store_hub.set_active_app(StoreHub::welcome_screen_app_id());
}
}
{
let store_context = store_hub.read_context();
let app_blueprint = AppBlueprint::new(
store_context.as_ref(),
&self.state.blueprint_query_for_viewer(),
egui_ctx,
self.panel_state_overrides_active
.then_some(self.panel_state_overrides),
);
self.ui(
egui_ctx,
frame,
&app_blueprint,
&gpu_resource_stats,
store_context.as_ref(),
store_stats.as_ref(),
);
if re_ui::CUSTOM_WINDOW_DECORATIONS {
paint_native_window_frame(egui_ctx);
}
if !self.screenshotter.is_screenshotting() {
self.toasts.show(egui_ctx);
}
if let Some(cmd) = self.cmd_palette.show(egui_ctx) {
self.command_sender.send_ui(cmd);
}
Self::handle_dropping_files(egui_ctx, store_context.as_ref(), &self.command_sender);
self.run_pending_ui_commands(egui_ctx, &app_blueprint, store_context.as_ref());
}
self.run_pending_system_commands(&mut store_hub, egui_ctx);
self.store_hub = Some(store_hub);
#[cfg(not(target_arch = "wasm32"))]
egui_ctx.input(|i| {
for event in &i.raw.events {
if let egui::Event::Screenshot { image, .. } = event {
self.screenshotter.save(image);
}
}
});
egui_ctx.output_mut(|o| {
if let Some(open_url) = &mut o.open_url {
open_url.new_tab = true;
}
});
}
#[cfg(target_arch = "wasm32")]
fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
Some(&mut *self)
}
}
fn populate_space_view_class_registry_with_builtin(
space_view_class_registry: &mut SpaceViewClassRegistry,
) -> Result<(), SpaceViewClassRegistryError> {
re_tracing::profile_function!();
space_view_class_registry.add_class::<re_space_view_bar_chart::BarChartSpaceView>()?;
#[cfg(feature = "map_view")]
space_view_class_registry.add_class::<re_space_view_map::MapSpaceView>()?;
space_view_class_registry.add_class::<re_space_view_spatial::SpatialSpaceView2D>()?;
space_view_class_registry.add_class::<re_space_view_spatial::SpatialSpaceView3D>()?;
space_view_class_registry.add_class::<re_space_view_tensor::TensorSpaceView>()?;
space_view_class_registry.add_class::<re_space_view_text_document::TextDocumentSpaceView>()?;
space_view_class_registry.add_class::<re_space_view_text_log::TextSpaceView>()?;
space_view_class_registry.add_class::<re_space_view_time_series::TimeSeriesSpaceView>()?;
space_view_class_registry.add_class::<re_space_view_dataframe::DataframeSpaceView>()?;
Ok(())
}
fn paint_background_fill(ui: &egui::Ui) {
ui.painter().rect_filled(
ui.max_rect().shrink(0.5),
re_ui::DesignTokens::native_window_rounding(),
ui.visuals().panel_fill,
);
}
fn paint_native_window_frame(egui_ctx: &egui::Context) {
let painter = egui::Painter::new(
egui_ctx.clone(),
egui::LayerId::new(egui::Order::TOP, egui::Id::new("native_window_frame")),
egui::Rect::EVERYTHING,
);
painter.rect_stroke(
egui_ctx.screen_rect().shrink(0.5),
re_ui::DesignTokens::native_window_rounding(),
re_ui::design_tokens().native_frame_stroke,
);
}
fn preview_files_being_dropped(egui_ctx: &egui::Context) {
use egui::{Align2, Color32, Id, LayerId, Order, TextStyle};
if !egui_ctx.input(|i| i.raw.hovered_files.is_empty()) {
use std::fmt::Write as _;
let mut text = "Drop to load:\n".to_owned();
egui_ctx.input(|input| {
for file in &input.raw.hovered_files {
if let Some(path) = &file.path {
write!(text, "\n{}", path.display()).ok();
} else if !file.mime.is_empty() {
write!(text, "\n{}", file.mime).ok();
}
}
});
let painter =
egui_ctx.layer_painter(LayerId::new(Order::Foreground, Id::new("file_drop_target")));
let screen_rect = egui_ctx.screen_rect();
painter.rect_filled(screen_rect, 0.0, Color32::from_black_alpha(192));
painter.text(
screen_rect.center(),
Align2::CENTER_CENTER,
text,
TextStyle::Body.resolve(&egui_ctx.style()),
Color32::WHITE,
);
}
}
fn file_saver_progress_ui(egui_ctx: &egui::Context, background_tasks: &mut BackgroundTasks) {
if background_tasks.is_file_save_in_progress() {
if let Some(res) = background_tasks.poll_file_saver_promise() {
match res {
Ok(path) => {
re_log::info!("File saved to {path:?}."); }
Err(err) => {
re_log::error!("{err}"); }
}
} else {
egui::Window::new("file_saver_spin")
.anchor(egui::Align2::RIGHT_BOTTOM, egui::Vec2::ZERO)
.title_bar(false)
.enabled(false)
.auto_sized()
.show(egui_ctx, |ui| {
ui.horizontal(|ui| {
ui.spinner();
ui.label("Writing file to disk…");
})
});
}
}
}
#[cfg(not(target_arch = "wasm32"))]
fn open_file_dialog_native() -> Vec<std::path::PathBuf> {
re_tracing::profile_function!();
let supported: Vec<_> = if re_data_loader::iter_external_loaders().len() == 0 {
re_data_loader::supported_extensions().collect()
} else {
vec![]
};
let mut dialog = rfd::FileDialog::new();
if !supported.is_empty() {
dialog = dialog.add_filter("Supported files", &supported);
}
dialog.pick_files().unwrap_or_default()
}
#[cfg(target_arch = "wasm32")]
async fn async_open_rrd_dialog() -> Vec<re_data_source::FileContents> {
let supported: Vec<_> = re_data_loader::supported_extensions().collect();
let files = rfd::AsyncFileDialog::new()
.add_filter("Supported files", &supported)
.pick_files()
.await
.unwrap_or_default();
let mut file_contents = Vec::with_capacity(files.len());
for file in files {
let file_name = file.file_name();
re_log::debug!("Reading {file_name}…");
let bytes = file.read().await;
re_log::debug!(
"{file_name} was {}",
re_format::format_bytes(bytes.len() as _)
);
file_contents.push(re_data_source::FileContents {
name: file_name,
bytes: bytes.into(),
});
}
file_contents
}
fn save_recording(
app: &mut App,
store_context: Option<&StoreContext<'_>>,
loop_selection: Option<(re_entity_db::Timeline, re_log_types::ResolvedTimeRangeF)>,
) -> anyhow::Result<()> {
let Some(entity_db) = store_context.as_ref().map(|view| view.recording) else {
anyhow::bail!("No recording data to save");
};
let rrd_version = entity_db
.store_info()
.and_then(|info| info.store_version)
.unwrap_or(re_build_info::CrateVersion::LOCAL);
let file_name = "data.rrd";
let title = if loop_selection.is_some() {
"Save loop selection"
} else {
"Save recording"
};
save_entity_db(
app,
rrd_version,
file_name.to_owned(),
title.to_owned(),
entity_db.to_messages(loop_selection),
)
}
fn save_blueprint(app: &mut App, store_context: Option<&StoreContext<'_>>) -> anyhow::Result<()> {
let Some(store_context) = store_context else {
anyhow::bail!("No blueprint to save");
};
re_tracing::profile_function!();
let rrd_version = store_context
.blueprint
.store_info()
.and_then(|info| info.store_version)
.unwrap_or(re_build_info::CrateVersion::LOCAL);
let new_store_id = re_log_types::StoreId::random(StoreKind::Blueprint);
let messages = store_context.blueprint.to_messages(None).map(|mut msg| {
if let Ok(msg) = &mut msg {
msg.set_store_id(new_store_id.clone());
};
msg
});
let file_name = format!(
"{}.rbl",
crate::saving::sanitize_app_id(&store_context.app_id)
);
let title = "Save blueprint";
save_entity_db(app, rrd_version, file_name, title.to_owned(), messages)
}
#[allow(clippy::needless_pass_by_ref_mut)] #[allow(clippy::unnecessary_wraps)] fn save_entity_db(
#[allow(unused_variables)] app: &mut App, rrd_version: CrateVersion,
file_name: String,
title: String,
messages: impl Iterator<Item = re_chunk::ChunkResult<LogMsg>>,
) -> anyhow::Result<()> {
re_tracing::profile_function!();
let messages = messages.collect::<Vec<_>>();
#[cfg(target_arch = "wasm32")]
{
wasm_bindgen_futures::spawn_local(async move {
if let Err(err) =
async_save_dialog(rrd_version, &file_name, &title, messages.into_iter()).await
{
re_log::error!("File saving failed: {err}");
}
});
}
#[cfg(not(target_arch = "wasm32"))]
{
let path = {
re_tracing::profile_scope!("file_dialog");
rfd::FileDialog::new()
.set_file_name(file_name)
.set_title(title)
.save_file()
};
if let Some(path) = path {
app.background_tasks.spawn_file_saver(move || {
crate::saving::encode_to_file(rrd_version, &path, messages.into_iter())?;
Ok(path)
})?;
}
}
Ok(())
}
#[cfg(target_arch = "wasm32")]
async fn async_save_dialog(
rrd_version: CrateVersion,
file_name: &str,
title: &str,
messages: impl Iterator<Item = re_chunk::ChunkResult<LogMsg>>,
) -> anyhow::Result<()> {
use anyhow::Context as _;
let file_handle = rfd::AsyncFileDialog::new()
.set_file_name(file_name)
.set_title(title)
.save_file()
.await;
let Some(file_handle) = file_handle else {
return Ok(()); };
let bytes = re_log_encoding::encoder::encode_as_bytes(
rrd_version,
re_log_encoding::EncodingOptions::COMPRESSED,
messages,
)?;
file_handle.write(&bytes).await.context("Failed to save")
}