use std::sync::Arc;
#[cfg(not(target_arch = "wasm32"))]
use std::sync::OnceLock;
use egui::{FocusDirection, Key};
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, FileContents, 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_ui::{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, AsyncRuntimeHandle, AuthContext, CommandReceiver, CommandSender,
ComponentUiRegistry, EditRedapServerModalCommand, FallbackProviderRegistry, Item, NeedsRepaint,
Route, SystemCommand, SystemCommandSender as _, TimeControlCommand, ViewClass,
ViewClassRegistry, ViewClassRegistryError, command_channel,
};
use crate::AppState;
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;
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";
#[cfg(target_arch = "wasm32")]
struct PendingFilePromise {
recommended_store_id: Option<StoreId>,
force_store_info: bool,
promise: poll_promise::Promise<Vec<re_data_source::FileContents>>,
}
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>,
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,
#[cfg(not(target_arch = "wasm32"))]
internal_catalog_origin: Arc<OnceLock<re_uri::Origin>>,
}
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>,
tokio_runtime: AsyncRuntimeHandle,
) -> Self {
Self::with_commands(
main_thread_token,
build_info,
app_env,
startup_options,
creation_context,
connection_registry,
tokio_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>,
tokio_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 = 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,
}
})));
});
tokio_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 app_env.is_test() {
state.app_options = AppOptions::test();
}
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 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);
}
}
});
}),
);
{
let command_sender = command_sender.clone();
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(cmd) = UICommand::listen_for_kb_shortcut(ctx) {
command_sender.send_ui(cmd);
}
}),
);
}
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()
} else {
blueprint_loader()
},
&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),
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: tokio_runtime,
#[cfg(not(target_arch = "wasm32"))]
internal_catalog_origin: Arc::new(OnceLock::new()),
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn set_profiler(&mut self, profiler: re_tracing::Profiler) {
self.profiler = profiler;
}
#[cfg(not(target_arch = "wasm32"))]
pub fn internal_catalog_origin(&self) -> Arc<OnceLock<re_uri::Origin>> {
self.internal_catalog_origin.clone()
}
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()
}
pub fn open_url_or_file(&self, url: &str) {
match ViewerOpenUrl::parse_with_options(
url,
&re_data_source::FromUriOptions {
accept_extensionless_http: true,
..Default::default()
},
) {
Ok(url) => {
url.open(
&self.egui_ctx,
&OpenUrlOptions {
follow: false,
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 crate::web_history::{HistoryEntry, HistoryExt as _, history};
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) = history().ok_or_log_js_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 {
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 = file
.path
.clone()
.map(|p| ApplicationId::from(p.display().to_string()))
.unwrap_or_else(|| ApplicationId::from(file.name.clone()));
let recording_id = RecordingId::random();
force_store_info = true;
StoreId::recording(application_id, recording_id)
});
if let Some(bytes) = file.bytes {
command_sender.send_system(SystemCommand::LoadDataSource(
LogDataSource::FileContents(
FileSource::DragAndDrop {
recommended_store_id: Some(active_store_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(
LogDataSource::FilePath {
file_source: FileSource::DragAndDrop {
recommended_store_id: Some(active_store_id.clone()),
force_store_info,
},
path,
follow: false,
},
));
}
}
}
#[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(not(target_arch = "wasm32"))]
{
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();
}
}
#[cfg(target_arch = "wasm32")]
{
re_log::error!(
"Saving screenshots to a path is not supported on web. Attempted to save to: {file_path:?}"
);
}
}
}
} 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.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::FileContents(
FileSource::FileDialog {
recommended_store_id: recommended_store_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 = 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()); }
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);
}
if let Some(cmd) = self
.cmd_palette
.show(ui, &crate::open_url_description::command_palette_parse_url)
{
match cmd {
re_ui::CommandPaletteAction::UiCommand(cmd) => {
self.command_sender.send_ui(cmd);
}
re_ui::CommandPaletteAction::OpenUrl(url_desc) => {
match ViewerOpenUrl::parse_with_options(
&url_desc.url,
&re_data_source::FromUriOptions {
accept_extensionless_http: true,
..Default::default()
},
) {
Ok(url) => {
url.open(
ui,
&OpenUrlOptions {
follow: false,
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_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() -> BlueprintPersistence {
noop_blueprint_loader()
}
fn noop_blueprint_loader() -> BlueprintPersistence {
BlueprintPersistence {
loader: None,
saver: None,
validator: Some(Box::new(crate::blueprint::is_valid_blueprint)),
deleter: None,
}
}
#[cfg(not(target_arch = "wasm32"))]
fn blueprint_loader() -> BlueprintPersistence {
use re_entity_db::{EntityDb, StoreBundle};
use re_log_types::{ApplicationId, StoreKind};
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:?}");
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)
{
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(load_blueprint_from_disk)),
saver: Some(Box::new(save_blueprint_to_disk)),
validator: Some(Box::new(crate::blueprint::is_valid_blueprint)),
deleter: Some(Box::new(crate::saving::delete_blueprint)),
}
}