use std::sync::Arc;
use egui::{FocusDirection, Key};
use re_async::AsyncRuntimeHandle;
use re_auth::credentials::CredentialsProvider as _;
use re_build_info::CrateVersion;
use re_byte_size::{MemUsageTree, MemUsageTreeCapture};
use re_capabilities::MainThreadToken;
use re_data_source::{AuthErrorHandler, LogDataSource};
use re_entity_db::InstancePath;
use re_entity_db::entity_db::EntityDb;
use re_log_channel::{LogReceiverSet, RecordingOpenBehavior, SaveScreenshotError};
use re_log_types::{ApplicationId, FileSource, RecordingId, StoreId};
use re_redap_client::ConnectionRegistryHandle;
use re_sdk_types::blueprint::components::PlayState;
use re_types_core::reflection::ComponentReflectionMap;
use re_ui::{ContextExt as _, UICommand, UICommandSender as _, notifications};
use re_viewer_context::open_url::{OpenUrlOptions, ViewerOpenUrl};
use re_viewer_context::store_hub::{BlueprintPersistence, StoreHub};
use re_viewer_context::{
AppBlueprintCtx, AppOptions, AuthContext, CommandReceiver, CommandSender, ComponentUiRegistry,
EditRedapServerModalCommand, FallbackProviderRegistry, Item, NeedsRepaint, Route,
SystemCommand, SystemCommandSender as _, TimeControlCommand, ViewClass, ViewClassRegistry,
ViewClassRegistryError, command_channel,
};
use crate::app_blueprint::{AppBlueprint, PanelStateOverrides};
use crate::background_tasks::BackgroundTasks;
use crate::event::ViewerEventDispatcher;
use crate::latency_tracker::ServerLatencyTrackers;
use crate::startup_options::StartupOptions;
use crate::{AppState, command_palette::CommandPaletteAction};
mod add_data_source;
mod command_handling;
mod logic;
mod ui;
const RERUN_VERSION_KEY: &str = "rerun.version";
const REDAP_TOKEN_KEY: &str = "rerun.redap_token";
fn pending_timeline_shortcut_key() -> egui::Id {
egui::Id::new("rerun_pending_timeline_shortcut")
}
#[cfg(target_arch = "wasm32")]
struct PendingFilePromise {
recommended_store_id: Option<StoreId>,
force_store_info: bool,
promise: poll_promise::Promise<Vec<web_sys::File>>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum WindowDecorationsRequest {
NotSent,
Native,
Custom,
}
pub struct App {
#[allow(clippy::allow_attributes, dead_code)] main_thread_token: MainThreadToken,
build_info: re_build_info::BuildInfo,
app_env: crate::AppEnvironment,
startup_options: StartupOptions,
start_time: web_time::Instant,
ram_limit_warner: re_memory::RamLimitWarner,
pub(crate) egui_ctx: egui::Context,
screenshotter: crate::screenshotter::Screenshotter,
texture_readback: crate::texture_readback::TextureReadbacks,
pending_screenshot_notifiers: std::collections::HashMap<
camino::Utf8PathBuf,
futures::channel::mpsc::UnboundedSender<Result<(), SaveScreenshotError>>,
>,
#[cfg(target_arch = "wasm32")]
pub(crate) popstate_listener: Option<crate::web_history::PopstateListener>,
#[cfg(not(target_arch = "wasm32"))]
profiler: re_tracing::Profiler,
#[cfg(not(target_arch = "wasm32"))]
profile_capture: Option<re_tracing::ProfileCapture>,
text_log_rx: crossbeam::channel::Receiver<re_log::LogMsg>,
component_ui_registry: ComponentUiRegistry,
component_fallback_registry: FallbackProviderRegistry,
rx_log: LogReceiverSet,
#[cfg(target_arch = "wasm32")]
open_files_promise: Option<PendingFilePromise>,
pub(crate) state: AppState,
pub(crate) background_tasks: BackgroundTasks,
pub(crate) store_hub: Option<StoreHub>,
pub(crate) notifications: notifications::NotificationUi,
dev_panel: crate::dev_panel::DevPanel,
dev_panel_open: bool,
pub(crate) external_memory_users: crate::external_memory::ExternalMemoryUsers,
cached_app_overhead_bytes: Option<u64>,
egui_debug_panel_open: bool,
pub(crate) latest_latency_interest: Option<web_time::Instant>,
pub(crate) frame_time_history: egui::util::History<f32>,
last_window_theme: Option<egui::SystemTheme>,
window_decorations_request: WindowDecorationsRequest,
pub command_sender: CommandSender,
command_receiver: CommandReceiver,
cmd_palette: re_ui::CommandPalette,
view_class_registry: ViewClassRegistry,
pub(crate) panel_state_overrides_active: bool,
pub(crate) panel_state_overrides: PanelStateOverrides,
reflection: re_types_core::reflection::Reflection,
pub event_dispatcher: Option<ViewerEventDispatcher>,
connection_registry: ConnectionRegistryHandle,
pub(crate) server_latency_trackers: ServerLatencyTrackers,
async_runtime: AsyncRuntimeHandle,
}
impl App {
pub fn new(
main_thread_token: MainThreadToken,
build_info: re_build_info::BuildInfo,
app_env: crate::AppEnvironment,
startup_options: StartupOptions,
creation_context: &eframe::CreationContext<'_>,
connection_registry: Option<ConnectionRegistryHandle>,
async_runtime: AsyncRuntimeHandle,
) -> Self {
Self::with_commands(
main_thread_token,
build_info,
app_env,
startup_options,
creation_context,
connection_registry,
async_runtime,
crate::register_text_log_receiver(),
command_channel(),
)
}
pub fn with_commands(
main_thread_token: MainThreadToken,
build_info: re_build_info::BuildInfo,
app_env: crate::AppEnvironment,
startup_options: StartupOptions,
creation_context: &eframe::CreationContext<'_>,
connection_registry: Option<ConnectionRegistryHandle>,
async_runtime: AsyncRuntimeHandle,
text_log_rx: crossbeam::channel::Receiver<re_log::LogMsg>,
command_channel: (CommandSender, CommandReceiver),
) -> Self {
re_tracing::profile_function!();
let is_test = app_env.is_test();
let connection_registry_was_provided = connection_registry.is_some();
let connection_registry = connection_registry
.unwrap_or_else(re_redap_client::ConnectionRegistry::new_with_stored_credentials);
if connection_registry.should_use_stored_credentials() {
let command_sender = command_channel.0.clone();
re_auth::credentials::subscribe_auth_changes(move |user| {
command_sender.send_system(SystemCommand::OnAuthChanged(user.map(|user| {
AuthContext {
email: user.email,
org_name: user.org_name,
}
})));
});
async_runtime.spawn_future(async move {
re_auth::credentials::CliCredentialsProvider::new()
.get_token()
.await
.ok();
});
}
if connection_registry.should_use_stored_credentials()
&& let Some(storage) = creation_context.storage
&& let Some(tokens) = eframe::get_value(storage, REDAP_TOKEN_KEY)
{
connection_registry.load_tokens(tokens);
}
let mut state: AppState = if startup_options.persist_state {
creation_context.storage
.and_then(|storage| {
let value = storage.get_string(eframe::APP_KEY)?;
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 startup_options.persist_state {
if let Some(storage) = creation_context.storage {
let current_version = build_info.version;
let previous_version: Option<CrateVersion> =
storage.get_string(RERUN_VERSION_KEY).and_then(|version| {
let version = Box::leak(version.into_boxed_str());
CrateVersion::try_parse(version).ok()
});
if previous_version
.is_none_or(|previous_version| previous_version < CrateVersion::new(0, 24, 0))
{
re_log::debug!(
"Upgrading from {} to {}.",
previous_version.map_or_else(|| "<unknown>".to_owned(), |v| v.to_string()),
current_version
);
creation_context
.egui_ctx
.options_mut(|o| o.theme_preference = egui::ThemePreference::System);
}
}
}
if let Some(video_decoder_hw_acceleration) = startup_options.video_decoder_hw_acceleration {
state.app_options.video.hw_acceleration = video_decoder_hw_acceleration;
}
if is_test {
creation_context.egui_ctx.mark_as_test();
state.app_options = AppOptions::test();
}
let connection_registry = {
if (!connection_registry_was_provided || cfg!(target_arch = "wasm32"))
&& connection_registry.internal_origin().is_none()
{
let catalog = cfg_select! {
target_arch = "wasm32" => { crate::internal_catalog::build() }
_ => {
crate::internal_catalog::build(std::net::SocketAddr::from((
std::net::Ipv4Addr::LOCALHOST,
re_uri::DEFAULT_PROXY_PORT,
)))
}
};
connection_registry.with_internal((catalog.origin, catalog.connection))
} else {
connection_registry
}
};
let reflection = re_sdk_types::reflection::generate_reflection().unwrap_or_else(|err| {
re_log::error!(
"Failed to create list of serialized default values for components: {err}"
);
Default::default()
});
let component_reflection = Arc::new(reflection.components.clone());
let mut component_fallback_registry =
re_component_fallbacks::create_component_fallback_registry();
let view_class_registry = crate::default_views::create_view_class_registry(
&reflection,
&state.app_options,
&mut component_fallback_registry,
)
.unwrap_or_else(|err| {
re_log::error!("Failed to create view class registry: {err}");
Default::default()
});
#[allow(clippy::allow_attributes, 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(&creation_context.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 (_adapter_backend, _device_tier) = creation_context.wgpu_render_state.as_ref().map_or(
(
wgpu::Backend::Noop,
re_renderer::device_caps::DeviceCapabilityTier::Limited,
),
|render_state| {
let egui_renderer = render_state.renderer.read();
let render_ctx = egui_renderer
.callback_resources
.get::<re_renderer::RenderContext>();
(
render_state.adapter.get_info().backend,
render_ctx.map_or(
re_renderer::device_caps::DeviceCapabilityTier::Limited,
|ctx| ctx.device_caps().tier,
),
)
},
);
#[cfg(feature = "analytics")]
if let Some(analytics) = re_analytics::Analytics::global_or_init() {
use crate::viewer_analytics::event;
analytics.record(event::identify(
analytics.config(),
build_info.clone(),
&app_env,
));
analytics.record(event::viewer_started(
&app_env,
&creation_context.egui_ctx,
_adapter_backend,
_device_tier,
));
}
let panel_state_overrides = startup_options.panel_state_overrides;
let event_dispatcher = startup_options
.on_event
.clone()
.map(ViewerEventDispatcher::new);
if !state.redap_servers.is_empty() {
command_sender.send_ui(UICommand::ExpandBlueprintPanel);
}
creation_context.egui_ctx.on_end_pass(
"remove copied text formatting",
Arc::new(|ctx| {
ctx.output_mut(|o| {
for command in &mut o.commands {
if let egui::output::OutputCommand::CopyText(text) = command {
*text = re_format::remove_number_formatting(text);
}
}
});
}),
);
{
creation_context.egui_ctx.on_begin_pass(
"rerun-kb-shortcuts",
Arc::new(move |ctx| {
let reset_focus_direction = ctx.input_mut(|i| {
i.key_pressed(Key::ArrowLeft) || i.key_pressed(Key::ArrowRight)
});
if reset_focus_direction {
ctx.memory_mut(|mem| {
mem.move_focus(FocusDirection::None);
});
}
if let Some(kind) = re_ui::consume_timeline_shortcut(ctx) {
ctx.data_mut(|data| {
data.insert_temp(pending_timeline_shortcut_key(), kind);
});
}
}),
);
}
Self {
main_thread_token,
build_info,
app_env,
startup_options,
start_time: web_time::Instant::now(),
ram_limit_warner: re_memory::RamLimitWarner::warn_at_fraction_of_max(0.75),
egui_ctx: creation_context.egui_ctx.clone(),
screenshotter,
texture_readback: Default::default(),
pending_screenshot_notifiers: Default::default(),
#[cfg(target_arch = "wasm32")]
popstate_listener: None,
#[cfg(not(target_arch = "wasm32"))]
profiler: Default::default(),
#[cfg(not(target_arch = "wasm32"))]
profile_capture: None,
text_log_rx,
component_ui_registry,
component_fallback_registry,
rx_log: Default::default(),
#[cfg(target_arch = "wasm32")]
open_files_promise: Default::default(),
state,
background_tasks: Default::default(),
store_hub: Some(StoreHub::new(
if is_test {
noop_blueprint_loader(component_reflection)
} else {
blueprint_loader(component_reflection)
},
&crate::app_blueprint::setup_welcome_screen_blueprint,
)),
notifications: notifications::NotificationUi::new(creation_context.egui_ctx.clone()),
dev_panel: Default::default(),
dev_panel_open: false,
external_memory_users: crate::external_memory::ExternalMemoryUsers::default_users(),
cached_app_overhead_bytes: None,
egui_debug_panel_open: false,
latest_latency_interest: None,
frame_time_history: egui::util::History::new(1..100, 0.5),
last_window_theme: None,
window_decorations_request: WindowDecorationsRequest::NotSent,
command_sender,
command_receiver,
cmd_palette: Default::default(),
view_class_registry,
panel_state_overrides_active: true,
panel_state_overrides,
reflection,
event_dispatcher,
connection_registry,
server_latency_trackers: ServerLatencyTrackers::default(),
async_runtime,
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn set_profiler(&mut self, profiler: re_tracing::Profiler) {
self.profiler = profiler;
}
pub fn connection_registry(&self) -> &ConnectionRegistryHandle {
&self.connection_registry
}
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 startup_options(&self) -> &StartupOptions {
&self.startup_options
}
pub fn app_options(&self) -> &AppOptions {
self.state.app_options()
}
pub fn reflection(&self) -> &re_types_core::reflection::Reflection {
&self.reflection
}
pub fn app_options_mut(&mut self) -> &mut AppOptions {
self.state.app_options_mut()
}
pub fn app_env(&self) -> &crate::AppEnvironment {
&self.app_env
}
pub(crate) fn custom_window_frame(&self) -> bool {
self.custom_window_decorations() && !cfg!(target_os = "windows")
}
pub fn active_recording_id(&self) -> Option<&StoreId> {
self.state.active_recording_id()
}
fn select_and_navigate_to(&self, item: &Item) {
self.command_sender
.send_system(SystemCommand::set_selection(item.clone()));
if let Some(route) = Route::from_item(item) {
self.command_sender
.send_system(SystemCommand::SetRoute(route));
}
}
pub fn open_url_or_file(&self, url: &str) {
match ViewerOpenUrl::parse_with_options(
url,
&re_data_source::FromUriOptions {
accept_extensionless_http: true,
},
) {
Ok(url) => {
url.open(
&self.egui_ctx,
&OpenUrlOptions {
recording_open_behavior: RecordingOpenBehavior::OpenAndSelect,
show_loader: true,
},
&self.command_sender,
);
}
Err(err) => {
if err.to_string().contains(url) {
re_log::error!("{err}");
} else {
re_log::error!(?url, "Failed to open URL: {err}");
}
}
}
}
pub fn is_screenshotting(&self) -> bool {
self.screenshotter.is_screenshotting()
}
fn move_time(&mut self) {
let stable_dt = self.egui_ctx.input(|i| i.stable_dt);
let Some(store_hub) = &self.store_hub else {
return;
};
if let Some(store_id) = self.active_recording_id()
&& let Some(blueprint) = store_hub.active_blueprint_for_app(store_id.application_id())
{
let default_blueprint = store_hub.default_blueprint_for_app(store_id.application_id());
let blueprint_query = self
.state
.get_blueprint_query_for_viewer(blueprint)
.unwrap_or_else(|| {
re_chunk::LatestAtQuery::latest(re_viewer_context::blueprint_timeline())
});
let bp_ctx = AppBlueprintCtx {
command_sender: &self.command_sender,
current_blueprint: blueprint,
default_blueprint,
blueprint_query,
};
if let Some(recording) = store_hub.entity_db(store_id) {
let more_data_is_streaming_in =
recording.data_source.as_ref().is_some_and(|store_source| {
self.rx_log
.sources()
.iter()
.any(|s| s.as_ref() == store_source)
});
let time_ctrl = self.state.time_control_mut(recording, &bp_ctx);
let response = time_ctrl.update(
recording,
&re_viewer_context::TimeControlUpdateParams {
stable_dt,
more_data_is_streaming_in,
is_buffering: recording.is_buffering(),
should_diff_state: true,
},
Some(&bp_ctx),
);
if response.needs_repaint == NeedsRepaint::Yes {
self.egui_ctx.request_repaint();
}
command_handling::handle_time_ctrl_event(
recording,
self.event_dispatcher.as_ref(),
&response,
);
}
if self.app_options().inspect_blueprint_timeline {
let re_viewer_context::TimeControlResponse {
needs_repaint,
playing_change: _,
timeline_change: _,
time_change: _,
} = self.state.blueprint_time_control.update(
blueprint,
&re_viewer_context::TimeControlUpdateParams {
stable_dt,
more_data_is_streaming_in: true,
is_buffering: false,
should_diff_state: false,
},
None::<&AppBlueprintCtx<'_>>,
);
if needs_repaint == NeedsRepaint::Yes {
self.egui_ctx.request_repaint();
}
let undo_state = self
.state
.blueprint_undo_state
.entry(blueprint.store_id().clone())
.or_default();
if self.state.blueprint_time_control.play_state() == PlayState::Following {
undo_state.redo_all();
} else if let Some(time) = self.state.blueprint_time_control.time_int() {
undo_state.set_redo_time(time);
}
}
}
if self
.state
.update_preview_time_controls(store_hub, stable_dt)
== re_viewer_context::NeedsRepaint::Yes
{
self.egui_ctx.request_repaint();
}
}
pub fn msg_receive_set(&self) -> &LogReceiverSet {
&self.rx_log
}
pub fn component_ui_registry_mut(&mut self) -> &mut ComponentUiRegistry {
&mut self.component_ui_registry
}
pub fn add_archetype_reflection(
&mut self,
archetype_name: re_sdk_types::ArchetypeName,
archetype_reflection: re_sdk_types::reflection::ArchetypeReflection,
) {
for field in &archetype_reflection.fields {
let descriptor = field.component_descriptor(archetype_name);
self.reflection
.component_identifiers
.insert(descriptor.component, descriptor);
}
self.reflection
.archetypes
.insert(archetype_name, archetype_reflection);
}
pub fn add_view_class<T: ViewClass + Default + 'static>(
&mut self,
) -> Result<(), ViewClassRegistryError> {
self.view_class_registry.add_class::<T>(
&self.reflection,
&self.state.app_options,
&mut self.component_fallback_registry,
)
}
pub fn extend_view_class(
&mut self,
view_class: re_sdk_types::ViewClassIdentifier,
register_fn: impl FnOnce(
&mut re_viewer_context::ViewSystemRegistrator<'_>,
) -> Result<(), ViewClassRegistryError>,
) -> Result<(), ViewClassRegistryError> {
self.view_class_registry.extend_class(
view_class,
&self.reflection,
&self.state.app_options,
&mut self.component_fallback_registry,
register_fn,
)
}
fn update_history(&mut self, store_hub: &StoreHub) {
if self.startup_options().web_history_enabled() {
#[cfg(target_arch = "wasm32")]
if !self.egui_ctx.egui_is_using_pointer()
&& self
.egui_ctx
.input(|input| !input.any_touches() && input.keys_down.is_empty())
{
self.update_web_history(store_hub);
}
} else {
self.update_viewer_history(store_hub);
}
}
fn update_viewer_history(&mut self, store_hub: &StoreHub) {
let route = self.state.navigation.current();
let time_ctrl = route
.recording_id()
.and_then(|id| self.state.time_control(id));
let selection = self.state.selection_state.selected_items();
let Ok(url) = ViewerOpenUrl::from_context_expanded(store_hub, route, time_ctrl, selection)
else {
return;
};
self.state.history.update_current_url(url);
}
#[cfg(target_arch = "wasm32")]
fn update_web_history(&self, store_hub: &StoreHub) {
let route = self.state.navigation.current();
let time_ctrl = route
.recording_id()
.and_then(|id| self.state.time_control(id));
let selection = self.state.selection_state.selected_items();
let Ok(url) = ViewerOpenUrl::from_context_expanded(store_hub, route, time_ctrl, selection)
.map(|mut url| {
if let Some(fragment) = url.fragment_mut() {
fragment.when = time_ctrl.and_then(|time_ctrl| {
Some((
*time_ctrl.timeline_name(),
re_log_types::TimeCell {
typ: time_ctrl.time_type()?,
value: time_ctrl.last_paused_time()?.floor().into(),
},
))
});
}
url
})
.and_then(|url| url.sharable_url(None))
else {
return;
};
re_log::trace!("Updating navigation bar");
use re_log::ResultExt as _;
use crate::web_history::{HistoryEntry, HistoryExt as _};
use crate::web_tools::JsResultExt as _;
fn strip_fragment(url: &str) -> &str {
url.rsplit_once("%23").map_or(url, |(url, _)| url)
}
if let Some(history) = re_web::browser::history().ok_or_log_error() {
let current_entry = history.current_entry().ok_or_log_js_error().flatten();
let new_entry = HistoryEntry::new(url);
if Some(&new_entry) != current_entry.as_ref() {
if current_entry
.and_then(|entry| {
Some((
entry.to_query_string().ok_or_log_js_error()?,
new_entry.to_query_string().ok_or_log_js_error()?,
))
})
.is_some_and(|(current, new)| strip_fragment(¤t) == strip_fragment(&new))
{
history.replace_entry(new_entry).ok_or_log_js_error();
} else {
history.push_entry(new_entry).ok_or_log_js_error();
}
}
}
}
pub fn auth_error_handler(sender: CommandSender) -> AuthErrorHandler {
Arc::new(move |url, _err| {
sender.send_system(SystemCommand::EditRedapServerModal(
EditRedapServerModalCommand {
origin: url.origin.clone(),
open_on_success: Some(url.to_string()),
title: Some("Authenticate to see this recording".to_owned()),
},
));
})
}
fn go_to_dataset_data(&self, store_id: StoreId, fragment: re_uri::Fragment) {
let time_commands = TimeControlCommand::from_url_fragment(&fragment);
if let Some(selection) = fragment.selection {
let re_log_types::DataPath {
entity_path,
instance,
component,
} = selection;
let item = if let Some(component) = component {
Item::from(re_log_types::ComponentPath::new(entity_path, component))
} else if let Some(instance) = instance {
Item::from(InstancePath::instance(entity_path, instance))
} else {
Item::from(entity_path)
};
self.command_sender
.send_system(SystemCommand::set_selection(item));
}
if !time_commands.is_empty() {
self.command_sender
.send_system(SystemCommand::TimeControlCommands {
store_id,
time_commands,
});
}
}
pub fn recording_db(&self) -> Option<&EntityDb> {
let store_hub = self.store_hub.as_ref()?;
let recording_id = self.active_recording_id()?;
store_hub.entity_db(recording_id)
}
pub fn current_query(&self) -> Option<re_chunk_store::LatestAtQuery> {
let store_id = self.active_recording_id()?;
self.state
.time_controls
.get(store_id)
.map(|tc| tc.current_query())
}
fn handle_dropping_files(
egui_ctx: &egui::Context,
command_sender: &CommandSender,
route: &Route,
) {
#![allow(clippy::allow_attributes, clippy::needless_continue)]
ui::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;
}
egui_ctx.request_repaint();
let mut force_store_info = false;
for file in dropped_files {
#[cfg(target_arch = "wasm32")]
let Some(web_file) = file.web_file().cloned() else {
continue;
};
let active_store_id = route
.recording_id()
.cloned()
.filter(|store_id| store_id.application_id() != StoreHub::welcome_screen_app_id())
.unwrap_or_else(|| {
let application_id =
ApplicationId::new_or_unknown(file.path().display().to_string());
let recording_id = RecordingId::random();
force_store_info = true;
StoreId::recording(application_id, recording_id)
});
cfg_select! {
target_arch = "wasm32" => {
command_sender.send_system(SystemCommand::LoadDataSource(
LogDataSource::File {
file_source: FileSource::DragAndDrop {
recommended_store_id: Some(active_store_id),
force_store_info,
},
path: file.path().to_owned(),
file: web_file,
},
));
}
_ => {
command_sender.send_system(SystemCommand::LoadDataSource(
LogDataSource::File {
file_source: FileSource::DragAndDrop {
recommended_store_id: Some(active_store_id),
force_store_info,
},
path: file.path().to_owned(),
},
));
}
}
}
}
#[allow(clippy::allow_attributes, clippy::needless_pass_by_ref_mut)] fn process_screenshot_result(
&mut self,
image: &Arc<egui::ColorImage>,
user_data: &egui::UserData,
) {
use re_viewer_context::ScreenshotInfo;
if let Some(info) = user_data
.data
.as_ref()
.and_then(|data| data.downcast_ref::<ScreenshotInfo>())
{
let ScreenshotInfo {
ui_rect,
pixels_per_point,
name,
target,
notify,
} = (*info).clone();
#[cfg(target_arch = "wasm32")]
let _ = notify;
let rgba = if let Some(ui_rect) = ui_rect {
Arc::new(image.region(&ui_rect, Some(pixels_per_point)))
} else {
image.clone()
};
match target {
re_viewer_context::ScreenshotTarget::CopyToClipboard => {
self.egui_ctx.copy_image((*rgba).clone());
}
re_viewer_context::ScreenshotTarget::SaveToPathFromFileDialog => {
use image::ImageEncoder as _;
let mut png_bytes: Vec<u8> = Vec::new();
if let Err(err) = image::codecs::png::PngEncoder::new(&mut png_bytes)
.write_image(
rgba.as_raw(),
rgba.width() as u32,
rgba.height() as u32,
image::ExtendedColorType::Rgba8,
)
{
re_log::error!("Failed to encode screenshot as PNG: {err}");
} else {
let file_name = format!("{name}.png");
self.command_sender.save_file_dialog(
self.main_thread_token,
&file_name,
"Save screenshot".to_owned(),
png_bytes,
);
}
}
re_viewer_context::ScreenshotTarget::SaveToPath(file_path) => {
cfg_select! {
target_arch = "wasm32" => {
re_log::error!(
"Saving screenshots to a path is not supported on web. Attempted to save to: {file_path:?}"
);
}
_ => {
let rgba = rgba.clone();
let notifier = self.pending_screenshot_notifiers.remove(&file_path);
let Some(rgba_image) = image::RgbaImage::from_vec(
rgba.width() as _,
rgba.height() as _,
bytemuck::pod_collect_to_vec(&rgba.pixels),
) else {
re_log::error!("Failed to create image from screenshot data");
if let Some(notifier) = notifier {
notifier
.unbounded_send(Err(SaveScreenshotError::InvalidImageData))
.ok();
}
return;
};
let rgb_image = image::DynamicImage::ImageRgba8(rgba_image).to_rgb8();
let result = match rgb_image.save(&file_path) {
Ok(()) => {
if notify {
re_log::info!("Saved screenshot to {file_path:?}");
} else {
re_log::debug!("Saved screenshot to {file_path:?}");
}
Ok(())
}
Err(err) => {
re_log::error!(?file_path, "Failed to save screenshot: {err}");
std::fs::remove_file(&file_path).ok();
Err(SaveScreenshotError::SaveToPathFailed {
path: file_path.to_string(),
reason: err.to_string(),
})
}
};
if let Some(notifier) = notifier {
notifier.unbounded_send(result).ok();
}
}
}
}
}
} else {
#[cfg(not(target_arch = "wasm32"))] if user_data
.data
.as_ref()
.is_some_and(|data| data.is::<crate::screenshotter::FullAppScreenshot>())
{
self.screenshotter.save(&self.egui_ctx, image);
}
}
}
}
impl eframe::App for App {
fn clear_color(&self, visuals: &egui::Visuals) -> [f32; 4] {
if self.custom_window_decorations() {
[0.; 4] } else if visuals.dark_mode {
[0., 0., 0., 1.]
} else {
[1., 1., 1., 1.]
}
}
fn save(&mut self, storage: &mut dyn eframe::Storage) {
if !self.startup_options.persist_state {
return;
}
re_tracing::profile_function!();
storage.set_string(RERUN_VERSION_KEY, self.build_info.version.to_string());
eframe::set_value(storage, eframe::APP_KEY, &self.state);
eframe::set_value(
storage,
REDAP_TOKEN_KEY,
&self.connection_registry.dump_tokens(),
);
if let Some(hub) = &mut self.store_hub {
if self.state.app_options.blueprint_gc {
hub.gc_blueprints(&self.state.blueprint_undo_state);
}
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 logic(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
self.logic_impl(ctx, frame);
}
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
#[cfg(all(not(target_arch = "wasm32"), feature = "perf_telemetry_tracy"))]
re_perf_telemetry::external::tracing_tracy::client::frame_mark();
#[cfg(not(target_arch = "wasm32"))]
if let Some(capture) = &self.profile_capture {
if capture.is_done() {
if let Some(capture) = self.profile_capture.take()
&& let Err(err) = save_profile_trace(&capture.finish())
{
re_log::error!("Failed to save profile trace: {err}");
}
} else {
ui.ctx().request_repaint();
}
}
if let Some(seconds) = frame.info().cpu_usage {
self.frame_time_history.add(ui.input(|i| i.time), seconds);
}
let mem_usage_tree = self
.dev_panel_open
.then(|| re_byte_size::NamedMemUsageTree::new("App", self.capture_mem_usage_tree()));
self.external_memory_users.update();
#[cfg(target_arch = "wasm32")]
if self.startup_options.enable_history {
let back_pressed = ui.input(|i| i.pointer.button_pressed(egui::PointerButton::Extra1));
let fwd_pressed = ui.input(|i| i.pointer.button_pressed(egui::PointerButton::Extra2));
if back_pressed {
crate::web_history::go_back();
}
if fwd_pressed {
crate::web_history::go_forward();
}
}
self.server_latency_trackers
.update(&self.async_runtime, &self.connection_registry);
self.move_time();
let mut store_hub = self
.store_hub
.take()
.expect("Failed to take store hub from the Viewer");
store_hub.update_data_source_order(&self.rx_log.sources());
#[cfg(not(target_arch = "wasm32"))]
if let Some(resolution_in_points) = self.startup_options.resolution_in_points.take() {
ui.send_viewport_cmd(egui::ViewportCommand::InnerSize(
resolution_in_points.into(),
));
}
#[cfg(not(target_arch = "wasm32"))]
if self.screenshotter.update(ui).quit {
ui.send_viewport_cmd(egui::ViewportCommand::Close);
return;
}
if self.app_options().memory_limit.is_unlimited() {
self.ram_limit_warner.update();
}
#[cfg(target_arch = "wasm32")]
if let Some(PendingFilePromise {
recommended_store_id,
force_store_info,
promise,
}) = &self.open_files_promise
&& let Some(files) = promise.ready()
{
for file in files {
self.command_sender
.send_system(SystemCommand::LoadDataSource(LogDataSource::File {
file_source: FileSource::FileDialog {
recommended_store_id: recommended_store_id.clone(),
force_store_info: *force_store_info,
},
path: file.name().into(),
file: file.clone(),
}));
}
self.open_files_promise = None;
}
let gpu_resource_stats = {
re_tracing::profile_scope!("gpu_resource_stats");
let egui_renderer = frame
.wgpu_render_state()
.expect("Failed to get frame render state")
.renderer
.read();
let render_ctx = egui_renderer
.callback_resources
.get::<re_renderer::RenderContext>()
.expect("Failed to get render context");
render_ctx.gpu_resources.statistics()
};
let store_stats = self.dev_panel_open.then(|| store_hub.stats());
let store_bundle_for_streaming = self
.dev_panel_open
.then(|| store_hub.store_bundle() as &re_entity_db::StoreBundle);
self.dev_panel.update(
&gpu_resource_stats,
store_stats.as_ref(),
store_bundle_for_streaming,
);
self.purge_memory_if_needed(&mut store_hub);
let is_start_of_new_frame = ui.current_pass_index() == 0;
if is_start_of_new_frame {
store_hub.begin_frame_caches(self.active_recording_id()); self.state.app_caches.begin_frame();
}
ui::file_saver_progress_ui(ui, &mut self.background_tasks);
if let Route::Loading(source) = self.state.navigation.current() {
if !self.msg_receive_set().contains(source) {
let loaded_recording = store_hub
.find_recording_store_by_source(source)
.map(|db| db.store_id().clone());
if let Some(store_id) = loaded_recording {
re_log::debug!("Stream completed, navigating to loaded recording {store_id:?}");
store_hub.load_blueprint_and_caches(&store_id, &self.view_class_registry);
self.state.navigation.replace(Route::LocalRecording {
recording_id: store_id,
});
} else if let Some(re_uri::RedapUri::DatasetData(uri)) = source.redap_uri()
&& self.connection_registry.error_for_uri(uri).is_some()
{
} else {
re_log::debug!("No recording found from loading source, resetting navigation");
self.state.navigation.reset();
}
}
} else if !matches!(
self.state.navigation.current(),
Route::ChunkStoreBrowser { .. }
) {
let route_is_valid = self
.state
.navigation
.current()
.recording_id()
.is_none_or(|recording_id| store_hub.entity_db(recording_id).is_some());
if !route_is_valid {
let any_other_app_id: Option<ApplicationId> = store_hub
.store_bundle()
.entity_dbs()
.map(|db| db.application_id())
.filter(|app_id| *app_id != StoreHub::welcome_screen_app_id())
.min()
.cloned();
if let Some(app_id) = any_other_app_id {
store_hub.load_persisted_blueprints_for_app(&app_id);
if let Some(recording_id) = store_hub.earliest_recording_for_app(&app_id) {
store_hub
.load_blueprint_and_caches(&recording_id, &self.view_class_registry);
self.state
.selection_state
.set_selection(Item::StoreId(recording_id.clone()));
self.state
.navigation
.replace(Route::LocalRecording { recording_id });
} else {
self.state.navigation.reset();
}
} else {
self.state.navigation.reset();
}
}
}
{
let active_route = self.state.navigation.current();
let active_time_ctrl = active_route
.recording_id()
.and_then(|id| self.state.time_controls.get(id).cloned())
.unwrap_or_default();
let (storage_context, store_context) =
store_hub.read_context(active_route, &active_time_ctrl);
let blueprint = store_context.as_ref().map(|ctx| ctx.blueprint);
let blueprint_query = self.state.blueprint_query_for_viewer(blueprint);
let app_blueprint = AppBlueprint::new(
blueprint,
&blueprint_query,
ui,
self.panel_state_overrides_active
.then_some(self.panel_state_overrides),
);
self.ui_impl(
ui,
frame,
&app_blueprint,
&gpu_resource_stats,
store_context.as_ref(),
&storage_context,
mem_usage_tree,
store_stats.as_ref(),
);
if self.custom_window_frame() {
ui::paint_custom_window_frame(ui);
}
let selected_redap_server = if let Some(Item::RedapServer(origin)) =
self.state.selection_state.selected_items().single_item()
{
Some(origin.clone())
} else {
None
};
let active_recording_id = store_context
.as_ref()
.map(|ctx| ctx.recording_store_id().clone());
let current_redap_entry = match self.state.navigation.current() {
Route::RedapEntry { origin, kind } => {
kind.entry_id().map(|entry_id| (origin.clone(), entry_id))
}
_ => None,
};
let cmd_env = re_ui::CommandEnvironment {
recording: active_recording_id.clone(),
has_editable_redap_server: selected_redap_server
.as_ref()
.is_some_and(|origin| !self.state.redap_servers.is_internal_server(origin)),
redap_server: selected_redap_server,
redap_entry: current_redap_entry,
};
{
use re_ui::{
RecordingCommandSender as _, RedapServerCommandSender as _,
TableCommandSender as _,
};
if let Some(resolved) = re_ui::listen_for_kb_shortcuts(ui.ctx(), &cmd_env) {
match resolved {
re_ui::ResolvedCommand::Ui(cmd) => self.command_sender.send_ui(cmd),
re_ui::ResolvedCommand::Recording(cmd) => {
self.command_sender.send_recording_command(cmd);
}
re_ui::ResolvedCommand::RedapServer(cmd) => {
self.command_sender.send_redap_server_command(cmd);
}
re_ui::ResolvedCommand::Table(cmd) => {
self.command_sender.send_table_command(cmd);
}
}
}
let pending_timeline = ui.ctx().data_mut(|data| {
let key = pending_timeline_shortcut_key();
let kind = data.get_temp::<re_ui::RecordingCommandKind>(key);
data.remove::<re_ui::RecordingCommandKind>(key);
kind
});
if let Some(cmd) = pending_timeline.and_then(|kind| kind.for_environment(&cmd_env))
{
self.command_sender.send_recording_command(cmd);
}
}
let mut cmd_palette_provider = crate::command_palette::CommandPaletteProviderImpl {
recording: store_context.as_ref().map(|ctx| ctx.recording()),
redap_servers: &self.state.redap_servers,
cmd_env,
};
if let Some(cmd) = self.cmd_palette.show(ui.ctx(), &mut cmd_palette_provider) {
match cmd {
CommandPaletteAction::UiCommand(cmd) => {
self.command_sender.send_ui(cmd);
}
CommandPaletteAction::RecordingCommand(cmd) => {
use re_ui::RecordingCommandSender as _;
self.command_sender.send_recording_command(cmd);
}
CommandPaletteAction::RedapServerCommand(cmd) => {
use re_ui::RedapServerCommandSender as _;
self.command_sender.send_redap_server_command(cmd);
}
CommandPaletteAction::SelectEntityPath(entity_path) => {
self.command_sender
.send_system(SystemCommand::set_selection(Item::from(
entity_path.clone(),
)));
self.command_sender
.send_system(SystemCommand::SetFocus(entity_path.into()));
}
CommandPaletteAction::SelectComponentPath(component_path) => {
let item = Item::from(component_path);
self.command_sender
.send_system(SystemCommand::set_selection(item.clone()));
self.command_sender
.send_system(SystemCommand::SetFocus(item.into()));
}
CommandPaletteAction::SelectRedapServer(origin) => {
self.select_and_navigate_to(&Item::RedapServer(origin));
}
CommandPaletteAction::SelectRedapEntry {
origin, entry_id, ..
} => {
self.select_and_navigate_to(&Item::RedapEntry {
origin,
kind: re_viewer_context::RedapEntryKind::Entry(entry_id),
});
}
CommandPaletteAction::TableCommand(cmd) => {
use re_ui::TableCommandSender as _;
self.command_sender.send_table_command(cmd);
}
CommandPaletteAction::OpenUrl(url) => {
match ViewerOpenUrl::parse_with_options(
url.as_str(),
&re_data_source::FromUriOptions {
accept_extensionless_http: true,
},
) {
Ok(url) => {
url.open(
ui,
&OpenUrlOptions {
recording_open_behavior:
RecordingOpenBehavior::OpenAndSelect,
show_loader: true,
},
&self.command_sender,
);
}
Err(err) => {
re_log::warn!("{err}");
}
}
}
}
}
let route = self.state.navigation.current().clone();
Self::handle_dropping_files(ui, &self.command_sender, &route);
self.run_pending_ui_commands(
ui,
&app_blueprint,
&storage_context,
store_context.as_ref(),
&route,
);
self.run_pending_recording_commands(
ui,
&app_blueprint,
&storage_context,
store_context.as_ref(),
);
}
self.run_pending_system_commands(&mut store_hub, ui);
self.update_history(&store_hub);
self.store_hub = Some(store_hub);
{
let screenshots: Vec<_> = ui.input(|i| {
i.raw
.events
.iter()
.filter_map(|event| {
if let egui::Event::Screenshot {
image, user_data, ..
} = event
{
Some((image.clone(), user_data.clone()))
} else {
None
}
})
.collect()
});
for (image, user_data) in screenshots {
self.process_screenshot_result(&image, &user_data);
}
}
}
#[cfg(target_arch = "wasm32")]
fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
Some(&mut *self)
}
}
#[cfg(not(target_arch = "wasm32"))]
fn save_profile_trace(view: &re_tracing::reexports::puffin::FrameView) -> anyhow::Result<()> {
let Some(path) = rfd::FileDialog::new()
.set_file_name("rerun.puffin")
.set_title("Save profile trace")
.add_filter("Puffin profile", &["puffin"])
.save_file()
else {
re_log::info!("Profile trace capture cancelled by user.");
return Ok(());
};
let file = std::fs::File::create(&path)?;
let mut writer = std::io::BufWriter::new(file);
view.write(&mut writer)?;
re_log::info!("Saved profile trace to {}", path.display());
Ok(())
}
impl MemUsageTreeCapture for App {
fn capture_mem_usage_tree(&self) -> MemUsageTree {
re_tracing::profile_function!();
let mut node = re_byte_size::MemUsageNode::default();
node.add("state", self.state.capture_mem_usage_tree());
node.add("rx_log", self.rx_log.capture_mem_usage_tree());
node.add("store_hub", self.store_hub.capture_mem_usage_tree());
node.add(
"store_subscribers",
re_chunk_store::ChunkStore::capture_all_subscribers_mem_usage_tree(),
);
let mut globals = re_byte_size::MemUsageNode::new();
globals.add(
"forgiving_parse_cache",
re_log_types::forgiving_parse_cache_bytes_used(),
);
globals.add("string_interner", re_string_interner::bytes_used() as u64);
node.add("globals", globals.into_tree());
node.into_tree()
}
}
#[cfg(target_arch = "wasm32")]
fn blueprint_loader(component_reflection: Arc<ComponentReflectionMap>) -> BlueprintPersistence {
noop_blueprint_loader(component_reflection)
}
fn noop_blueprint_loader(
component_reflection: Arc<ComponentReflectionMap>,
) -> BlueprintPersistence {
BlueprintPersistence {
loader: None,
saver: None,
validator: Some(Box::new(move |blueprint| {
crate::blueprint::is_valid_blueprint(blueprint, &component_reflection)
})),
deleter: None,
}
}
#[cfg(not(target_arch = "wasm32"))]
fn blueprint_loader(component_reflection: Arc<ComponentReflectionMap>) -> BlueprintPersistence {
use re_entity_db::{EntityDb, StoreBundle};
use re_log_types::{ApplicationId, StoreKind};
fn load_blueprint_from_disk(
component_reflection: &ComponentReflectionMap,
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:?}");
if let Some(bundle) = crate::loading::load_blueprint_file(&blueprint_path) {
for store in bundle.entity_dbs() {
if store.store_kind() == StoreKind::Blueprint
&& !crate::blueprint::is_valid_blueprint(store, component_reflection)
{
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)
}
}
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({
let component_reflection = component_reflection.clone();
move |app_id| load_blueprint_from_disk(&component_reflection, app_id)
})),
saver: Some(Box::new(save_blueprint_to_disk)),
validator: Some(Box::new(move |blueprint| {
crate::blueprint::is_valid_blueprint(blueprint, &component_reflection)
})),
deleter: Some(Box::new(crate::saving::delete_blueprint)),
}
}