From fc1bb665fee373d7527884e878f34a8d5f1cd643 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 2 Jun 2023 09:26:44 +0200 Subject: [PATCH 01/26] Break up app.rs into a top_panel.rs, rerun_menu.rs and, loading.rs --- crates/re_viewer/src/app.rs | 907 ++--------------------------- crates/re_viewer/src/lib.rs | 3 + crates/re_viewer/src/loading.rs | 59 ++ crates/re_viewer/src/rerun_menu.rs | 424 ++++++++++++++ crates/re_viewer/src/top_panel.rs | 282 +++++++++ 5 files changed, 831 insertions(+), 844 deletions(-) create mode 100644 crates/re_viewer/src/loading.rs create mode 100644 crates/re_viewer/src/rerun_menu.rs create mode 100644 crates/re_viewer/src/top_panel.rs diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index ec06f6cb1f53..02916cad6fcf 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -1,14 +1,12 @@ use std::{any::Any, hash::Hash}; use ahash::HashMap; -use egui::NumExt as _; use itertools::Itertools as _; use poll_promise::Promise; use web_time::Instant; use re_arrow_store::{DataStoreConfig, DataStoreStats}; use re_data_store::store_db::StoreDb; -use re_format::format_number; use re_log_types::{ApplicationId, LogMsg, StoreId, StoreKind}; use re_renderer::WgpuResourcePoolStatistics; use re_smart_channel::Receiver; @@ -21,7 +19,6 @@ use re_viewport::ViewportState; use crate::{ui::Blueprint, viewer_analytics::ViewerAnalytics, StoreHub}; -#[cfg(not(target_arch = "wasm32"))] use re_log_types::TimeRangeF; const WATERMARK: bool = false; // Nice for recording media material @@ -83,21 +80,21 @@ pub struct App { build_info: re_build_info::BuildInfo, startup_options: StartupOptions, ram_limit_warner: re_memory::RamLimitWarner, - re_ui: re_ui::ReUi, - screenshotter: crate::screenshotter::Screenshotter, + pub(crate) re_ui: re_ui::ReUi, + pub(crate) screenshotter: crate::screenshotter::Screenshotter, /// Listens to the local text log stream text_log_rx: std::sync::mpsc::Receiver, component_ui_registry: ComponentUiRegistry, - rx: Receiver, + pub(crate) rx: Receiver, /// Where the recordings and blueprints are stored. - store_hub: crate::StoreHub, + pub(crate) store_hub: crate::StoreHub, /// What is serialized - state: AppState, + pub(crate) state: AppState, /// Pending background tasks, using `poll_promise`. pending_promises: HashMap>>, @@ -108,13 +105,13 @@ pub struct App { memory_panel: crate::memory_panel::MemoryPanel, memory_panel_open: bool, - latest_queue_interest: web_time::Instant, + pub(crate) latest_queue_interest: web_time::Instant, /// Measures how long a frame takes to paint - frame_time_history: egui::util::History, + pub(crate) frame_time_history: egui::util::History, /// Commands to run at the end of the frame. - pending_commands: Vec, + pub(crate) pending_commands: Vec, cmd_palette: re_ui::CommandPalette, analytics: ViewerAnalytics, @@ -214,6 +211,10 @@ impl App { self.state.profiler = profiler; } + pub fn build_info(&self) -> &re_build_info::BuildInfo { + &self.build_info + } + /// Adds a new space view class to the viewer. pub fn add_space_view_class( &mut self, @@ -270,9 +271,8 @@ impl App { }) } - /// Returns whether a promise with the given name is currently running. - pub fn promise_exists(&mut self, name: impl AsRef) -> bool { - self.pending_promises.contains_key(name.as_ref()) + pub fn is_file_save_in_progress(&self) -> bool { + self.pending_promises.contains_key(FILE_SAVER_PROMISE) } fn check_keyboard_shortcuts(&mut self, egui_ctx: &egui::Context) { @@ -281,8 +281,8 @@ impl App { } } - #[cfg(not(target_arch = "wasm32"))] - fn loop_selection(&self) -> Option<(re_data_store::Timeline, TimeRangeF)> { + /// Currently selected section of time, if any. + pub fn loop_selection(&self) -> Option<(re_data_store::Timeline, TimeRangeF)> { self.state.selected_rec_id.as_ref().and_then(|rec_id| { self.state .recording_configs @@ -459,7 +459,8 @@ impl App { } } - fn selected_app_id(&self) -> ApplicationId { + /// The app ID of active blueprint. + pub fn selected_app_id(&self) -> ApplicationId { self.store_db() .and_then(|store_db| { store_db @@ -645,7 +646,7 @@ impl eframe::App for App { warning_panel(&self.re_ui, ui, frame); - top_panel(&blueprint, ui, frame, self, &gpu_resource_stats); + crate::top_panel::top_panel(&blueprint, ui, frame, self, &gpu_resource_stats); self.memory_panel_ui( ui, @@ -1016,8 +1017,8 @@ impl App { egui_ctx.set_style((*style).clone()); } - /// Do we have an open `StoreDb` that is non-empty? - fn store_db_is_nonempty(&self) -> bool { + /// Do we have an open [`StoreDb`] that is non-empty? + pub(crate) fn store_db_is_nonempty(&self) -> bool { self.store_db() .map_or(false, |store_db| !store_db.is_empty()) } @@ -1060,7 +1061,7 @@ impl App { if let Some(file) = egui_ctx.input(|i| i.raw.dropped_files.first().cloned()) { if let Some(bytes) = &file.bytes { let mut bytes: &[u8] = &(*bytes)[..]; - if let Some(rrd) = load_file_contents(&file.name, &mut bytes) { + if let Some(rrd) = crate::loading::load_file_contents(&file.name, &mut bytes) { self.on_rrd_loaded(rrd); #[allow(clippy::needless_return)] // false positive on wasm32 @@ -1070,7 +1071,7 @@ impl App { #[cfg(not(target_arch = "wasm32"))] if let Some(path) = &file.path { - if let Some(rrd) = load_file_path(path) { + if let Some(rrd) = crate::loading::load_file_path(path) { self.on_rrd_loaded(rrd); } } @@ -1113,33 +1114,24 @@ fn preview_files_being_dropped(egui_ctx: &egui::Context) { // ------------------------------------------------------------------------------------ -#[derive(Copy, Clone, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)] -enum PanelSelection { - #[default] - Viewport, -} - #[derive(Default, serde::Deserialize, serde::Serialize)] #[serde(default)] -struct AppState { +pub(crate) struct AppState { /// Global options for the whole viewer. - app_options: AppOptions, + pub(crate) app_options: AppOptions, /// Things that need caching. #[serde(skip)] cache: Caches, #[serde(skip)] - selected_rec_id: Option, + pub(crate) selected_rec_id: Option, #[serde(skip)] - selected_blueprint_by_app: HashMap, + pub(crate) selected_blueprint_by_app: HashMap, /// Configuration for the current recording (found in [`StoreDb`]). recording_configs: HashMap, - /// Which view panel is currently being shown - panel_selection: PanelSelection, - selection_panel: crate::selection_panel::SelectionPanel, time_panel: re_time_panel::TimePanel, @@ -1174,7 +1166,6 @@ impl AppState { selected_rec_id: _, selected_blueprint_by_app: _, recording_configs, - panel_selection, selection_panel, time_panel, #[cfg(not(target_arch = "wasm32"))] @@ -1211,10 +1202,8 @@ impl AppState { egui::CentralPanel::default() .frame(central_panel_frame) - .show_inside(ui, |ui| match *panel_selection { - PanelSelection::Viewport => { - blueprint.blueprint_panel_and_viewport(viewport_state, &mut ctx, ui); - } + .show_inside(ui, |ui| { + blueprint.blueprint_panel_and_viewport(viewport_state, &mut ctx, ui); }); { @@ -1267,407 +1256,6 @@ fn warning_panel(re_ui: &re_ui::ReUi, ui: &mut egui::Ui, frame: &mut eframe::Fra } } -fn top_panel( - blueprint: &Blueprint, - ui: &mut egui::Ui, - frame: &mut eframe::Frame, - app: &mut App, - gpu_resource_stats: &WgpuResourcePoolStatistics, -) { - re_tracing::profile_function!(); - - let native_pixels_per_point = frame.info().native_pixels_per_point; - let fullscreen = { - #[cfg(target_arch = "wasm32")] - { - false - } - #[cfg(not(target_arch = "wasm32"))] - { - frame.info().window_info.fullscreen - } - }; - let style_like_web = app.screenshotter.is_screenshotting(); - let top_bar_style = - app.re_ui - .top_bar_style(native_pixels_per_point, fullscreen, style_like_web); - - egui::TopBottomPanel::top("top_bar") - .frame(app.re_ui.top_panel_frame()) - .exact_height(top_bar_style.height) - .show_inside(ui, |ui| { - let _response = egui::menu::bar(ui, |ui| { - ui.set_height(top_bar_style.height); - ui.add_space(top_bar_style.indent); - - top_bar_ui(blueprint, ui, frame, app, gpu_resource_stats); - }) - .response; - - #[cfg(not(target_arch = "wasm32"))] - if !re_ui::NATIVE_WINDOW_BAR { - let title_bar_response = _response.interact(egui::Sense::click()); - if title_bar_response.double_clicked() { - frame.set_maximized(!frame.info().window_info.maximized); - } else if title_bar_response.is_pointer_button_down_on() { - frame.drag_window(); - } - } - }); -} - -fn rerun_menu_button_ui(ui: &mut egui::Ui, frame: &mut eframe::Frame, app: &mut App) { - // let desired_icon_height = ui.max_rect().height() - 2.0 * ui.spacing_mut().button_padding.y; - let desired_icon_height = ui.max_rect().height() - 4.0; // TODO(emilk): figure out this fudge - let desired_icon_height = desired_icon_height.at_most(28.0); // figma size 2023-02-03 - - let icon_image = app.re_ui.icon_image(&re_ui::icons::RERUN_MENU); - let image_size = icon_image.size_vec2() * (desired_icon_height / icon_image.size_vec2().y); - let texture_id = icon_image.texture_id(ui.ctx()); - - ui.menu_image_button(texture_id, image_size, |ui| { - ui.set_min_width(220.0); - let spacing = 12.0; - - ui.menu_button("About", |ui| about_rerun_ui(ui, &app.build_info)); - - main_view_selector_ui(ui, app); - - ui.add_space(spacing); - - Command::ToggleCommandPalette.menu_button_ui(ui, &mut app.pending_commands); - - ui.add_space(spacing); - - #[cfg(not(target_arch = "wasm32"))] - { - Command::Open.menu_button_ui(ui, &mut app.pending_commands); - - save_buttons_ui(ui, app); - - ui.add_space(spacing); - - // On the web the browser controls the zoom - let zoom_factor = app.state.app_options.zoom_factor; - ui.weak(format!("Zoom {:.0}%", zoom_factor * 100.0)) - .on_hover_text("The zoom factor applied on top of the OS scaling factor."); - Command::ZoomIn.menu_button_ui(ui, &mut app.pending_commands); - Command::ZoomOut.menu_button_ui(ui, &mut app.pending_commands); - ui.add_enabled_ui(zoom_factor != 1.0, |ui| { - Command::ZoomReset.menu_button_ui(ui, &mut app.pending_commands) - }); - - Command::ToggleFullscreen.menu_button_ui(ui, &mut app.pending_commands); - - ui.add_space(spacing); - } - - { - Command::ResetViewer.menu_button_ui(ui, &mut app.pending_commands); - - #[cfg(not(target_arch = "wasm32"))] - Command::OpenProfiler.menu_button_ui(ui, &mut app.pending_commands); - - Command::ToggleMemoryPanel.menu_button_ui(ui, &mut app.pending_commands); - } - - ui.add_space(spacing); - - ui.menu_button("Recordings", |ui| { - recordings_menu(ui, app); - }); - - ui.menu_button("Blueprints", |ui| { - blueprints_menu(ui, app); - }); - - ui.menu_button("Options", |ui| { - options_menu_ui(ui, frame, &mut app.state.app_options); - }); - - ui.add_space(spacing); - ui.hyperlink_to( - "Help", - "https://www.rerun.io/docs/getting-started/viewer-walkthrough", - ); - - #[cfg(not(target_arch = "wasm32"))] - { - ui.add_space(spacing); - Command::Quit.menu_button_ui(ui, &mut app.pending_commands); - } - }); -} - -fn about_rerun_ui(ui: &mut egui::Ui, build_info: &re_build_info::BuildInfo) { - let re_build_info::BuildInfo { - crate_name, - version, - rustc_version, - llvm_version, - git_hash, - git_branch: _, - is_in_rerun_workspace: _, - target_triple, - datetime, - } = *build_info; - - ui.style_mut().wrap = Some(false); - - let rustc_version = if rustc_version.is_empty() { - "unknown" - } else { - rustc_version - }; - - let llvm_version = if llvm_version.is_empty() { - "unknown" - } else { - llvm_version - }; - - let short_git_hash = &git_hash[..std::cmp::min(git_hash.len(), 7)]; - - ui.label(format!( - "{crate_name} {version} ({short_git_hash})\n\ - {target_triple}\n\ - rustc {rustc_version}\n\ - LLVM {llvm_version}\n\ - Built {datetime}", - )); - - ui.add_space(12.0); - ui.hyperlink_to("www.rerun.io", "https://www.rerun.io/"); -} - -fn top_bar_ui( - blueprint: &Blueprint, - ui: &mut egui::Ui, - frame: &mut eframe::Frame, - app: &mut App, - gpu_resource_stats: &WgpuResourcePoolStatistics, -) { - rerun_menu_button_ui(ui, frame, app); - - if app.state.app_options.show_metrics { - ui.separator(); - frame_time_label_ui(ui, app); - memory_use_label_ui(ui, gpu_resource_stats); - input_latency_label_ui(ui, app); - } - - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if re_ui::CUSTOM_WINDOW_DECORATIONS && !cfg!(target_arch = "wasm32") { - ui.add_space(8.0); - #[cfg(not(target_arch = "wasm32"))] - re_ui::native_window_buttons_ui(frame, ui); - ui.separator(); - } else { - // Make the first button the same distance form the side as from the top, - // no matter how high the top bar is. - let extra_margin = (ui.available_height() - 24.0) / 2.0; - ui.add_space(extra_margin); - } - - let mut selection_panel_expanded = blueprint.selection_panel_expanded; - if app - .re_ui - .medium_icon_toggle_button( - ui, - &re_ui::icons::RIGHT_PANEL_TOGGLE, - &mut selection_panel_expanded, - ) - .on_hover_text(format!( - "Toggle Selection View{}", - Command::ToggleSelectionPanel.format_shortcut_tooltip_suffix(ui.ctx()) - )) - .clicked() - { - app.pending_commands.push(Command::ToggleSelectionPanel); - } - - let mut time_panel_expanded = blueprint.time_panel_expanded; - if app - .re_ui - .medium_icon_toggle_button( - ui, - &re_ui::icons::BOTTOM_PANEL_TOGGLE, - &mut time_panel_expanded, - ) - .on_hover_text(format!( - "Toggle Timeline View{}", - Command::ToggleTimePanel.format_shortcut_tooltip_suffix(ui.ctx()) - )) - .clicked() - { - app.pending_commands.push(Command::ToggleTimePanel); - } - - let mut blueprint_panel_expanded = blueprint.blueprint_panel_expanded; - if app - .re_ui - .medium_icon_toggle_button( - ui, - &re_ui::icons::LEFT_PANEL_TOGGLE, - &mut blueprint_panel_expanded, - ) - .on_hover_text(format!( - "Toggle Blueprint View{}", - Command::ToggleBlueprintPanel.format_shortcut_tooltip_suffix(ui.ctx()) - )) - .clicked() - { - app.pending_commands.push(Command::ToggleBlueprintPanel); - } - - if cfg!(debug_assertions) && app.state.app_options.show_metrics { - ui.vertical_centered(|ui| { - ui.style_mut().wrap = Some(false); - ui.add_space(6.0); // TODO(emilk): in egui, add a proper way of centering a single widget in a UI. - egui::warn_if_debug_build(ui); - }); - } - }); -} - -fn frame_time_label_ui(ui: &mut egui::Ui, app: &mut App) { - if let Some(frame_time) = app.frame_time_history.average() { - let ms = frame_time * 1e3; - - let visuals = ui.visuals(); - let color = if ms < 15.0 { - visuals.weak_text_color() - } else { - visuals.warn_fg_color - }; - - // we use monospace so the width doesn't fluctuate as the numbers change. - let text = format!("{ms:.1} ms"); - ui.label(egui::RichText::new(text).monospace().color(color)) - .on_hover_text("CPU time used by Rerun Viewer each frame. Lower is better."); - } -} - -fn memory_use_label_ui(ui: &mut egui::Ui, gpu_resource_stats: &WgpuResourcePoolStatistics) { - const CODE: &str = "use re_memory::AccountingAllocator;\n\ - #[global_allocator]\n\ - static GLOBAL: AccountingAllocator =\n \ - AccountingAllocator::new(std::alloc::System);"; - - fn click_to_copy( - ui: &mut egui::Ui, - text: impl Into, - add_contents_on_hover: impl FnOnce(&mut egui::Ui), - ) { - #[allow(clippy::blocks_in_if_conditions)] - let text = text.into(); - if ui - .add( - egui::Label::new( - egui::RichText::new(text) - .monospace() - .color(ui.visuals().weak_text_color()), - ) - .sense(egui::Sense::click()), - ) - .on_hover_ui(|ui| add_contents_on_hover(ui)) - .clicked() - { - ui.ctx().output_mut(|o| o.copied_text = CODE.to_owned()); - } - } - - let mem = re_memory::MemoryUse::capture(); - - if let Some(count) = re_memory::accounting_allocator::global_allocs() { - // we use monospace so the width doesn't fluctuate as the numbers change. - - let bytes_used_text = re_format::format_bytes(count.size as _); - ui.label( - egui::RichText::new(&bytes_used_text) - .monospace() - .color(ui.visuals().weak_text_color()), - ) - .on_hover_text(format!( - "Rerun Viewer is using {} of RAM in {} separate allocations,\n\ - plus {} of GPU memory in {} textures and {} buffers.", - bytes_used_text, - format_number(count.count), - re_format::format_bytes(gpu_resource_stats.total_bytes() as _), - format_number(gpu_resource_stats.num_textures), - format_number(gpu_resource_stats.num_buffers), - )); - } else if let Some(rss) = mem.resident { - let bytes_used_text = re_format::format_bytes(rss as _); - click_to_copy(ui, &bytes_used_text, |ui| { - ui.label(format!( - "Rerun Viewer is using {} of Resident memory (RSS),\n\ - plus {} of GPU memory in {} textures and {} buffers.", - bytes_used_text, - re_format::format_bytes(gpu_resource_stats.total_bytes() as _), - format_number(gpu_resource_stats.num_textures), - format_number(gpu_resource_stats.num_buffers), - )); - ui.label( - "To get more accurate memory reportings, consider configuring your Rerun \n\ - viewer to use an AccountingAllocator by adding the following to your \n\ - code's main entrypoint:", - ); - ui.code(CODE); - ui.label("(click to copy to clipboard)"); - }); - } else { - click_to_copy(ui, "N/A MiB", |ui| { - ui.label( - "The Rerun viewer was not configured to run with an AccountingAllocator,\n\ - consider adding the following to your code's main entrypoint:", - ); - ui.code(CODE); - ui.label("(click to copy to clipboard)"); - }); - } -} - -fn input_latency_label_ui(ui: &mut egui::Ui, app: &mut App) { - // TODO(emilk): it would be nice to know if the network stream is still open - let is_latency_interesting = app.rx.source().is_network(); - - let queue_len = app.rx.len(); - - // empty queue == unreliable latency - let latency_sec = app.rx.latency_ns() as f32 / 1e9; - if queue_len > 0 - && (!is_latency_interesting || app.state.app_options.warn_latency < latency_sec) - { - // we use this to avoid flicker - app.latest_queue_interest = web_time::Instant::now(); - } - - if app.latest_queue_interest.elapsed().as_secs_f32() < 1.0 { - ui.separator(); - if is_latency_interesting { - let text = format!( - "Latency: {:.2}s, queue: {}", - latency_sec, - format_number(queue_len), - ); - let hover_text = - "When more data is arriving over network than the Rerun Viewer can index, a queue starts building up, leading to latency and increased RAM use.\n\ - This latency does NOT include network latency."; - - if latency_sec < app.state.app_options.warn_latency { - ui.weak(text).on_hover_text(hover_text); - } else { - ui.label(app.re_ui.warning_text(text)) - .on_hover_text(hover_text); - } - } else { - ui.weak(format!("Queue: {}", format_number(queue_len))) - .on_hover_text("Number of messages in the inbound queue"); - } - } -} - // ---------------------------------------------------------------------------- const FILE_SAVER_PROMISE: &str = "file_saver"; @@ -1675,8 +1263,7 @@ const FILE_SAVER_PROMISE: &str = "file_saver"; fn file_saver_progress_ui(egui_ctx: &egui::Context, app: &mut App) { use std::path::PathBuf; - let file_save_in_progress = app.promise_exists(FILE_SAVER_PROMISE); - if file_save_in_progress { + if app.is_file_save_in_progress() { // There's already a file save running in the background. if let Some(res) = app.poll_promise::>(FILE_SAVER_PROMISE) { @@ -1709,54 +1296,41 @@ fn file_saver_progress_ui(egui_ctx: &egui::Context, app: &mut App) { } } -// TODO(emilk): support saving data on web -#[cfg(not(target_arch = "wasm32"))] -fn save_buttons_ui(ui: &mut egui::Ui, app: &mut App) { - let file_save_in_progress = app.promise_exists(FILE_SAVER_PROMISE); +fn recording_config_entry<'cfgs>( + configs: &'cfgs mut HashMap, + id: StoreId, + data_source: &'_ re_smart_channel::SmartChannelSource, + store_db: &'_ StoreDb, +) -> &'cfgs mut RecordingConfig { + configs + .entry(id) + .or_insert_with(|| new_recording_confg(data_source, store_db)) +} - let save_button = Command::Save.menu_button(ui.ctx()); - let save_selection_button = Command::SaveSelection.menu_button(ui.ctx()); +fn new_recording_confg( + data_source: &'_ re_smart_channel::SmartChannelSource, + store_db: &'_ StoreDb, +) -> RecordingConfig { + let play_state = match data_source { + // Play files from the start by default - it feels nice and alive./ + // RrdHttpStream downloads the whole file before decoding it, so we treat it the same as a file. + re_smart_channel::SmartChannelSource::Files { .. } + | re_smart_channel::SmartChannelSource::RrdHttpStream { .. } + | re_smart_channel::SmartChannelSource::RrdWebEventListener => PlayState::Playing, - if file_save_in_progress { - ui.add_enabled_ui(false, |ui| { - ui.horizontal(|ui| { - ui.add(save_button); - ui.spinner(); - }); - ui.horizontal(|ui| { - ui.add(save_selection_button); - ui.spinner(); - }); - }); - } else { - ui.add_enabled_ui(app.store_db_is_nonempty(), |ui| { - if ui - .add(save_button) - .on_hover_text("Save all data to a Rerun data file (.rrd)") - .clicked() - { - ui.close_menu(); - app.pending_commands.push(Command::Save); - } + // Live data - follow it! + re_smart_channel::SmartChannelSource::Sdk + | re_smart_channel::SmartChannelSource::WsClient { .. } + | re_smart_channel::SmartChannelSource::TcpServer { .. } => PlayState::Following, + }; - // We need to know the loop selection _before_ we can even display the - // button, as this will determine whether its grayed out or not! - // TODO(cmc): In practice the loop (green) selection is always there - // at the moment so... - let loop_selection = app.loop_selection(); - - if ui - .add_enabled(loop_selection.is_some(), save_selection_button) - .on_hover_text( - "Save data for the current loop selection to a Rerun data file (.rrd)", - ) - .clicked() - { - ui.close_menu(); - app.pending_commands.push(Command::SaveSelection); - } - }); - } + let mut rec_cfg = RecordingConfig::default(); + + rec_cfg + .time_ctrl + .set_play_state(store_db.times_per_timeline(), play_state); + + rec_cfg } #[cfg(not(target_arch = "wasm32"))] @@ -1765,7 +1339,7 @@ fn open(app: &mut App) { .add_filter("rerun data file", &["rrd"]) .pick_file() { - if let Some(store_db) = load_file_path(&path) { + if let Some(store_db) = crate::loading::load_file_path(&path) { app.on_rrd_loaded(store_db); } } @@ -1804,266 +1378,6 @@ fn save(app: &mut App, loop_selection: Option<(re_data_store::Timeline, TimeRang } } -fn main_view_selector_ui(ui: &mut egui::Ui, app: &mut App) { - if app.store_db_is_nonempty() { - ui.horizontal(|ui| { - ui.label("Main view:"); - if ui - .selectable_value( - &mut app.state.panel_selection, - PanelSelection::Viewport, - "Viewport", - ) - .clicked() - { - ui.close_menu(); - } - }); - } -} - -fn recordings_menu(ui: &mut egui::Ui, app: &mut App) { - let store_dbs = app - .store_hub - .recordings() - .sorted_by_key(|store_db| store_db.store_info().map(|ri| ri.started)) - .collect_vec(); - - if store_dbs.is_empty() { - ui.weak("(empty)"); - return; - } - - ui.style_mut().wrap = Some(false); - for store_db in &store_dbs { - let info = if let Some(store_info) = store_db.store_info() { - format!( - "{} - {}", - store_info.application_id, - store_info.started.format() - ) - } else { - "".to_owned() - }; - if ui - .radio( - app.state.selected_rec_id.as_ref() == Some(store_db.store_id()), - info, - ) - .clicked() - { - app.state.selected_rec_id = Some(store_db.store_id().clone()); - } - } -} - -fn blueprints_menu(ui: &mut egui::Ui, app: &mut App) { - let app_id = app.selected_app_id(); - let blueprint_dbs = app - .store_hub - .blueprints() - .sorted_by_key(|store_db| store_db.store_info().map(|ri| ri.started)) - .filter(|log| { - log.store_info() - .map_or(false, |ri| ri.application_id == app_id) - }) - .collect_vec(); - - if blueprint_dbs.is_empty() { - ui.weak("(empty)"); - return; - } - - ui.style_mut().wrap = Some(false); - for store_db in blueprint_dbs - .iter() - .filter(|log| log.store_kind() == StoreKind::Blueprint) - { - let info = if let Some(store_info) = store_db.store_info() { - if store_info.is_app_default_blueprint() { - format!("{} - Default Blueprint", store_info.application_id,) - } else { - format!( - "{} - {}", - store_info.application_id, - store_info.started.format() - ) - } - } else { - "".to_owned() - }; - if ui - .radio( - app.state.selected_blueprint_by_app.get(&app_id) == Some(store_db.store_id()), - info, - ) - .clicked() - { - app.state - .selected_blueprint_by_app - .insert(app_id.clone(), store_db.store_id().clone()); - } - } -} - -fn options_menu_ui(ui: &mut egui::Ui, _frame: &mut eframe::Frame, options: &mut AppOptions) { - ui.style_mut().wrap = Some(false); - - if ui - .checkbox(&mut options.show_metrics, "Show performance metrics") - .on_hover_text("Show metrics for milliseconds/frame and RAM usage in the top bar.") - .clicked() - { - ui.close_menu(); - } - - #[cfg(not(target_arch = "wasm32"))] - { - if ui - .checkbox(&mut options.experimental_space_view_screenshots, "(experimental) Space View screenshots") - .on_hover_text("Allow taking screenshots of 2D & 3D space views via their context menu. Does not contain labels.") - .clicked() - { - ui.close_menu(); - } - } - - #[cfg(debug_assertions)] - { - ui.separator(); - ui.label("Debug:"); - - egui_debug_options_ui(ui); - ui.separator(); - debug_menu_options_ui(ui, options, _frame); - } -} - -#[cfg(debug_assertions)] -fn egui_debug_options_ui(ui: &mut egui::Ui) { - let mut debug = ui.style().debug; - let mut any_clicked = false; - - any_clicked |= ui - .checkbox(&mut debug.debug_on_hover, "Ui debug on hover") - .on_hover_text("However over widgets to see their rectangles") - .changed(); - any_clicked |= ui - .checkbox(&mut debug.show_expand_width, "Show expand width") - .on_hover_text("Show which widgets make their parent wider") - .changed(); - any_clicked |= ui - .checkbox(&mut debug.show_expand_height, "Show expand height") - .on_hover_text("Show which widgets make their parent higher") - .changed(); - any_clicked |= ui.checkbox(&mut debug.show_resize, "Show resize").changed(); - any_clicked |= ui - .checkbox( - &mut debug.show_interactive_widgets, - "Show interactive widgets", - ) - .on_hover_text("Show an overlay on all interactive widgets.") - .changed(); - - // This option currently causes the viewer to hang. - // any_clicked |= ui - // .checkbox(&mut debug.show_blocking_widget, "Show blocking widgets") - // .on_hover_text("Show what widget blocks the interaction of another widget.") - // .changed(); - - if any_clicked { - let mut style = (*ui.ctx().style()).clone(); - style.debug = debug; - ui.ctx().set_style(style); - } -} - -#[cfg(debug_assertions)] -fn debug_menu_options_ui(ui: &mut egui::Ui, options: &mut AppOptions, _frame: &mut eframe::Frame) { - #[cfg(not(target_arch = "wasm32"))] - { - if ui.button("Mobile size").clicked() { - // frame.set_window_size(egui::vec2(375.0, 812.0)); // iPhone 12 mini - _frame.set_window_size(egui::vec2(375.0, 667.0)); // iPhone SE 2nd gen - _frame.set_fullscreen(false); - ui.close_menu(); - } - ui.separator(); - } - - if ui.button("Log info").clicked() { - re_log::info!("Logging some info"); - } - - ui.checkbox( - &mut options.show_picking_debug_overlay, - "Picking Debug Overlay", - ) - .on_hover_text("Show a debug overlay that renders the picking layer information using the `debug_overlay.wgsl` shader."); - - ui.menu_button("Crash", |ui| { - #[allow(clippy::manual_assert)] - if ui.button("panic!").clicked() { - panic!("Intentional panic"); - } - - if ui.button("panic! during unwind").clicked() { - struct PanicOnDrop {} - - impl Drop for PanicOnDrop { - fn drop(&mut self) { - panic!("Second intentional panic in Drop::drop"); - } - } - - let _this_will_panic_when_dropped = PanicOnDrop {}; - panic!("First intentional panic"); - } - - if ui.button("SEGFAULT").clicked() { - // Taken from https://github.com/EmbarkStudios/crash-handling/blob/main/sadness-generator/src/lib.rs - - /// This is the fixed address used to generate a segfault. It's possible that - /// this address can be mapped and writable by the your process in which case a - /// crash may not occur - #[cfg(target_pointer_width = "64")] - pub const SEGFAULT_ADDRESS: u64 = u32::MAX as u64 + 0x42; - #[cfg(target_pointer_width = "32")] - pub const SEGFAULT_ADDRESS: u32 = 0x42; - - let bad_ptr: *mut u8 = SEGFAULT_ADDRESS as _; - #[allow(unsafe_code)] - // SAFETY: this is not safe. We are _trying_ to crash. - unsafe { - std::ptr::write_volatile(bad_ptr, 1); - } - } - - if ui.button("Stack overflow").clicked() { - // Taken from https://github.com/EmbarkStudios/crash-handling/blob/main/sadness-generator/src/lib.rs - fn recurse(data: u64) -> u64 { - let mut buff = [0u8; 256]; - buff[..9].copy_from_slice(b"junk data"); - - let mut result = data; - for c in buff { - result += c as u64; - } - - if result == 0 { - result - } else { - recurse(result) + 1 - } - } - - recurse(42); - } - }); -} - -// --- - /// Returns a closure that, when run, will save the contents of the current database /// to disk, at the specified `path`. /// @@ -2126,98 +1440,3 @@ fn save_database_to_file( .context("Message encode") }) } - -#[cfg(not(target_arch = "wasm32"))] -#[must_use] -fn load_file_path(path: &std::path::Path) -> Option { - fn load_file_path_impl(path: &std::path::Path) -> anyhow::Result { - re_tracing::profile_function!(); - use anyhow::Context as _; - let file = std::fs::File::open(path).context("Failed to open file")?; - StoreHub::from_rrd(file) - } - - re_log::info!("Loading {path:?}…"); - - match load_file_path_impl(path) { - Ok(mut rrd) => { - re_log::info!("Loaded {path:?}"); - for store_db in rrd.store_dbs_mut() { - store_db.data_source = Some(re_smart_channel::SmartChannelSource::Files { - paths: vec![path.into()], - }); - } - Some(rrd) - } - Err(err) => { - let msg = format!("Failed loading {path:?}: {}", re_error::format(&err)); - re_log::error!("{msg}"); - rfd::MessageDialog::new() - .set_level(rfd::MessageLevel::Error) - .set_description(&msg) - .show(); - None - } - } -} - -#[must_use] -fn load_file_contents(name: &str, read: impl std::io::Read) -> Option { - match StoreHub::from_rrd(read) { - Ok(mut rrd) => { - re_log::info!("Loaded {name:?}"); - for store_db in rrd.store_dbs_mut() { - store_db.data_source = Some(re_smart_channel::SmartChannelSource::Files { - paths: vec![name.into()], - }); - } - Some(rrd) - } - Err(err) => { - let msg = format!("Failed loading {name:?}: {}", re_error::format(&err)); - re_log::error!("{msg}"); - rfd::MessageDialog::new() - .set_level(rfd::MessageLevel::Error) - .set_description(&msg) - .show(); - None - } - } -} - -fn recording_config_entry<'cfgs>( - configs: &'cfgs mut HashMap, - id: StoreId, - data_source: &'_ re_smart_channel::SmartChannelSource, - store_db: &'_ StoreDb, -) -> &'cfgs mut RecordingConfig { - configs - .entry(id) - .or_insert_with(|| new_recording_confg(data_source, store_db)) -} - -fn new_recording_confg( - data_source: &'_ re_smart_channel::SmartChannelSource, - store_db: &'_ StoreDb, -) -> RecordingConfig { - let play_state = match data_source { - // Play files from the start by default - it feels nice and alive./ - // RrdHttpStream downloads the whole file before decoding it, so we treat it the same as a file. - re_smart_channel::SmartChannelSource::Files { .. } - | re_smart_channel::SmartChannelSource::RrdHttpStream { .. } - | re_smart_channel::SmartChannelSource::RrdWebEventListener => PlayState::Playing, - - // Live data - follow it! - re_smart_channel::SmartChannelSource::Sdk - | re_smart_channel::SmartChannelSource::WsClient { .. } - | re_smart_channel::SmartChannelSource::TcpServer { .. } => PlayState::Following, - }; - - let mut rec_cfg = RecordingConfig::default(); - - rec_cfg - .time_ctrl - .set_play_state(store_db.times_per_timeline(), play_state); - - rec_cfg -} diff --git a/crates/re_viewer/src/lib.rs b/crates/re_viewer/src/lib.rs index a110fd4fd988..22de010c7f1f 100644 --- a/crates/re_viewer/src/lib.rs +++ b/crates/re_viewer/src/lib.rs @@ -6,11 +6,14 @@ mod app; pub mod blueprint_components; pub mod env_vars; +mod loading; #[cfg(not(target_arch = "wasm32"))] mod profiler; mod remote_viewer_app; +mod rerun_menu; mod screenshotter; mod store_hub; +mod top_panel; mod ui; mod viewer_analytics; diff --git a/crates/re_viewer/src/loading.rs b/crates/re_viewer/src/loading.rs new file mode 100644 index 000000000000..d213456c7cd7 --- /dev/null +++ b/crates/re_viewer/src/loading.rs @@ -0,0 +1,59 @@ +use crate::StoreHub; + +#[cfg(not(target_arch = "wasm32"))] +#[must_use] +pub fn load_file_path(path: &std::path::Path) -> Option { + fn load_file_path_impl(path: &std::path::Path) -> anyhow::Result { + re_tracing::profile_function!(); + use anyhow::Context as _; + let file = std::fs::File::open(path).context("Failed to open file")?; + StoreHub::from_rrd(file) + } + + re_log::info!("Loading {path:?}…"); + + match load_file_path_impl(path) { + Ok(mut rrd) => { + re_log::info!("Loaded {path:?}"); + for store_db in rrd.store_dbs_mut() { + store_db.data_source = Some(re_smart_channel::SmartChannelSource::Files { + paths: vec![path.into()], + }); + } + Some(rrd) + } + Err(err) => { + let msg = format!("Failed loading {path:?}: {}", re_error::format(&err)); + re_log::error!("{msg}"); + rfd::MessageDialog::new() + .set_level(rfd::MessageLevel::Error) + .set_description(&msg) + .show(); + None + } + } +} + +#[must_use] +pub fn load_file_contents(name: &str, read: impl std::io::Read) -> Option { + match StoreHub::from_rrd(read) { + Ok(mut rrd) => { + re_log::info!("Loaded {name:?}"); + for store_db in rrd.store_dbs_mut() { + store_db.data_source = Some(re_smart_channel::SmartChannelSource::Files { + paths: vec![name.into()], + }); + } + Some(rrd) + } + Err(err) => { + let msg = format!("Failed loading {name:?}: {}", re_error::format(&err)); + re_log::error!("{msg}"); + rfd::MessageDialog::new() + .set_level(rfd::MessageLevel::Error) + .set_description(&msg) + .show(); + None + } + } +} diff --git a/crates/re_viewer/src/rerun_menu.rs b/crates/re_viewer/src/rerun_menu.rs new file mode 100644 index 000000000000..c8b37373bf4d --- /dev/null +++ b/crates/re_viewer/src/rerun_menu.rs @@ -0,0 +1,424 @@ +//! The main Rerun drop-down menu found in the top panel. + +use egui::NumExt as _; +use itertools::Itertools as _; + +use re_log_types::StoreKind; +use re_ui::Command; +use re_viewer_context::AppOptions; + +use crate::App; + +pub fn rerun_menu_button_ui(ui: &mut egui::Ui, frame: &mut eframe::Frame, app: &mut App) { + // let desired_icon_height = ui.max_rect().height() - 2.0 * ui.spacing_mut().button_padding.y; + let desired_icon_height = ui.max_rect().height() - 4.0; // TODO(emilk): figure out this fudge + let desired_icon_height = desired_icon_height.at_most(28.0); // figma size 2023-02-03 + + let icon_image = app.re_ui.icon_image(&re_ui::icons::RERUN_MENU); + let image_size = icon_image.size_vec2() * (desired_icon_height / icon_image.size_vec2().y); + let texture_id = icon_image.texture_id(ui.ctx()); + + ui.menu_image_button(texture_id, image_size, |ui| { + ui.set_min_width(220.0); + let spacing = 12.0; + + ui.menu_button("About", |ui| about_rerun_ui(ui, app.build_info())); + + ui.add_space(spacing); + + Command::ToggleCommandPalette.menu_button_ui(ui, &mut app.pending_commands); + + ui.add_space(spacing); + + #[cfg(not(target_arch = "wasm32"))] + { + Command::Open.menu_button_ui(ui, &mut app.pending_commands); + + save_buttons_ui(ui, app); + + ui.add_space(spacing); + + // On the web the browser controls the zoom + let zoom_factor = app.state.app_options.zoom_factor; + ui.weak(format!("Zoom {:.0}%", zoom_factor * 100.0)) + .on_hover_text("The zoom factor applied on top of the OS scaling factor."); + Command::ZoomIn.menu_button_ui(ui, &mut app.pending_commands); + Command::ZoomOut.menu_button_ui(ui, &mut app.pending_commands); + ui.add_enabled_ui(zoom_factor != 1.0, |ui| { + Command::ZoomReset.menu_button_ui(ui, &mut app.pending_commands) + }); + + Command::ToggleFullscreen.menu_button_ui(ui, &mut app.pending_commands); + + ui.add_space(spacing); + } + + { + Command::ResetViewer.menu_button_ui(ui, &mut app.pending_commands); + + #[cfg(not(target_arch = "wasm32"))] + Command::OpenProfiler.menu_button_ui(ui, &mut app.pending_commands); + + Command::ToggleMemoryPanel.menu_button_ui(ui, &mut app.pending_commands); + } + + ui.add_space(spacing); + + ui.menu_button("Recordings", |ui| { + recordings_menu(ui, app); + }); + + ui.menu_button("Blueprints", |ui| { + blueprints_menu(ui, app); + }); + + ui.menu_button("Options", |ui| { + options_menu_ui(ui, frame, &mut app.state.app_options); + }); + + ui.add_space(spacing); + ui.hyperlink_to( + "Help", + "https://www.rerun.io/docs/getting-started/viewer-walkthrough", + ); + + #[cfg(not(target_arch = "wasm32"))] + { + ui.add_space(spacing); + Command::Quit.menu_button_ui(ui, &mut app.pending_commands); + } + }); +} + +fn about_rerun_ui(ui: &mut egui::Ui, build_info: &re_build_info::BuildInfo) { + let re_build_info::BuildInfo { + crate_name, + version, + rustc_version, + llvm_version, + git_hash, + git_branch: _, + is_in_rerun_workspace: _, + target_triple, + datetime, + } = *build_info; + + ui.style_mut().wrap = Some(false); + + let rustc_version = if rustc_version.is_empty() { + "unknown" + } else { + rustc_version + }; + + let llvm_version = if llvm_version.is_empty() { + "unknown" + } else { + llvm_version + }; + + let short_git_hash = &git_hash[..std::cmp::min(git_hash.len(), 7)]; + + ui.label(format!( + "{crate_name} {version} ({short_git_hash})\n\ + {target_triple}\n\ + rustc {rustc_version}\n\ + LLVM {llvm_version}\n\ + Built {datetime}", + )); + + ui.add_space(12.0); + ui.hyperlink_to("www.rerun.io", "https://www.rerun.io/"); +} + +fn recordings_menu(ui: &mut egui::Ui, app: &mut App) { + let store_dbs = app + .store_hub + .recordings() + .sorted_by_key(|store_db| store_db.store_info().map(|ri| ri.started)) + .collect_vec(); + + if store_dbs.is_empty() { + ui.weak("(empty)"); + return; + } + + ui.style_mut().wrap = Some(false); + for store_db in &store_dbs { + let info = if let Some(store_info) = store_db.store_info() { + format!( + "{} - {}", + store_info.application_id, + store_info.started.format() + ) + } else { + "".to_owned() + }; + if ui + .radio( + app.state.selected_rec_id.as_ref() == Some(store_db.store_id()), + info, + ) + .clicked() + { + app.state.selected_rec_id = Some(store_db.store_id().clone()); + } + } +} + +fn blueprints_menu(ui: &mut egui::Ui, app: &mut App) { + let app_id = app.selected_app_id(); + let blueprint_dbs = app + .store_hub + .blueprints() + .sorted_by_key(|store_db| store_db.store_info().map(|ri| ri.started)) + .filter(|log| { + log.store_info() + .map_or(false, |ri| ri.application_id == app_id) + }) + .collect_vec(); + + if blueprint_dbs.is_empty() { + ui.weak("(empty)"); + return; + } + + ui.style_mut().wrap = Some(false); + for store_db in blueprint_dbs + .iter() + .filter(|log| log.store_kind() == StoreKind::Blueprint) + { + let info = if let Some(store_info) = store_db.store_info() { + if store_info.is_app_default_blueprint() { + format!("{} - Default Blueprint", store_info.application_id,) + } else { + format!( + "{} - {}", + store_info.application_id, + store_info.started.format() + ) + } + } else { + "".to_owned() + }; + if ui + .radio( + app.state.selected_blueprint_by_app.get(&app_id) == Some(store_db.store_id()), + info, + ) + .clicked() + { + app.state + .selected_blueprint_by_app + .insert(app_id.clone(), store_db.store_id().clone()); + } + } +} + +fn options_menu_ui(ui: &mut egui::Ui, _frame: &mut eframe::Frame, options: &mut AppOptions) { + ui.style_mut().wrap = Some(false); + + if ui + .checkbox(&mut options.show_metrics, "Show performance metrics") + .on_hover_text("Show metrics for milliseconds/frame and RAM usage in the top bar.") + .clicked() + { + ui.close_menu(); + } + + #[cfg(not(target_arch = "wasm32"))] + { + if ui + .checkbox(&mut options.experimental_space_view_screenshots, "(experimental) Space View screenshots") + .on_hover_text("Allow taking screenshots of 2D & 3D space views via their context menu. Does not contain labels.") + .clicked() + { + ui.close_menu(); + } + } + + #[cfg(debug_assertions)] + { + ui.separator(); + ui.label("Debug:"); + + egui_debug_options_ui(ui); + ui.separator(); + debug_menu_options_ui(ui, options, _frame); + } +} + +// TODO(emilk): support saving data on web +#[cfg(not(target_arch = "wasm32"))] +fn save_buttons_ui(ui: &mut egui::Ui, app: &mut App) { + let file_save_in_progress = app.is_file_save_in_progress(); + + let save_button = Command::Save.menu_button(ui.ctx()); + let save_selection_button = Command::SaveSelection.menu_button(ui.ctx()); + + if file_save_in_progress { + ui.add_enabled_ui(false, |ui| { + ui.horizontal(|ui| { + ui.add(save_button); + ui.spinner(); + }); + ui.horizontal(|ui| { + ui.add(save_selection_button); + ui.spinner(); + }); + }); + } else { + ui.add_enabled_ui(app.store_db_is_nonempty(), |ui| { + if ui + .add(save_button) + .on_hover_text("Save all data to a Rerun data file (.rrd)") + .clicked() + { + ui.close_menu(); + app.pending_commands.push(Command::Save); + } + + // We need to know the loop selection _before_ we can even display the + // button, as this will determine whether its grayed out or not! + // TODO(cmc): In practice the loop (green) selection is always there + // at the moment so... + let loop_selection = app.loop_selection(); + + if ui + .add_enabled(loop_selection.is_some(), save_selection_button) + .on_hover_text( + "Save data for the current loop selection to a Rerun data file (.rrd)", + ) + .clicked() + { + ui.close_menu(); + app.pending_commands.push(Command::SaveSelection); + } + }); + } +} + +#[cfg(debug_assertions)] +fn egui_debug_options_ui(ui: &mut egui::Ui) { + let mut debug = ui.style().debug; + let mut any_clicked = false; + + any_clicked |= ui + .checkbox(&mut debug.debug_on_hover, "Ui debug on hover") + .on_hover_text("However over widgets to see their rectangles") + .changed(); + any_clicked |= ui + .checkbox(&mut debug.show_expand_width, "Show expand width") + .on_hover_text("Show which widgets make their parent wider") + .changed(); + any_clicked |= ui + .checkbox(&mut debug.show_expand_height, "Show expand height") + .on_hover_text("Show which widgets make their parent higher") + .changed(); + any_clicked |= ui.checkbox(&mut debug.show_resize, "Show resize").changed(); + any_clicked |= ui + .checkbox( + &mut debug.show_interactive_widgets, + "Show interactive widgets", + ) + .on_hover_text("Show an overlay on all interactive widgets.") + .changed(); + + // This option currently causes the viewer to hang. + // any_clicked |= ui + // .checkbox(&mut debug.show_blocking_widget, "Show blocking widgets") + // .on_hover_text("Show what widget blocks the interaction of another widget.") + // .changed(); + + if any_clicked { + let mut style = (*ui.ctx().style()).clone(); + style.debug = debug; + ui.ctx().set_style(style); + } +} + +#[cfg(debug_assertions)] +fn debug_menu_options_ui(ui: &mut egui::Ui, options: &mut AppOptions, _frame: &mut eframe::Frame) { + #[cfg(not(target_arch = "wasm32"))] + { + if ui.button("Mobile size").clicked() { + // frame.set_window_size(egui::vec2(375.0, 812.0)); // iPhone 12 mini + _frame.set_window_size(egui::vec2(375.0, 667.0)); // iPhone SE 2nd gen + _frame.set_fullscreen(false); + ui.close_menu(); + } + ui.separator(); + } + + if ui.button("Log info").clicked() { + re_log::info!("Logging some info"); + } + + ui.checkbox( + &mut options.show_picking_debug_overlay, + "Picking Debug Overlay", + ) + .on_hover_text("Show a debug overlay that renders the picking layer information using the `debug_overlay.wgsl` shader."); + + ui.menu_button("Crash", |ui| { + #[allow(clippy::manual_assert)] + if ui.button("panic!").clicked() { + panic!("Intentional panic"); + } + + if ui.button("panic! during unwind").clicked() { + struct PanicOnDrop {} + + impl Drop for PanicOnDrop { + fn drop(&mut self) { + panic!("Second intentional panic in Drop::drop"); + } + } + + let _this_will_panic_when_dropped = PanicOnDrop {}; + panic!("First intentional panic"); + } + + if ui.button("SEGFAULT").clicked() { + // Taken from https://github.com/EmbarkStudios/crash-handling/blob/main/sadness-generator/src/lib.rs + + /// This is the fixed address used to generate a segfault. It's possible that + /// this address can be mapped and writable by the your process in which case a + /// crash may not occur + #[cfg(target_pointer_width = "64")] + pub const SEGFAULT_ADDRESS: u64 = u32::MAX as u64 + 0x42; + #[cfg(target_pointer_width = "32")] + pub const SEGFAULT_ADDRESS: u32 = 0x42; + + let bad_ptr: *mut u8 = SEGFAULT_ADDRESS as _; + #[allow(unsafe_code)] + // SAFETY: this is not safe. We are _trying_ to crash. + unsafe { + std::ptr::write_volatile(bad_ptr, 1); + } + } + + if ui.button("Stack overflow").clicked() { + // Taken from https://github.com/EmbarkStudios/crash-handling/blob/main/sadness-generator/src/lib.rs + fn recurse(data: u64) -> u64 { + let mut buff = [0u8; 256]; + buff[..9].copy_from_slice(b"junk data"); + + let mut result = data; + for c in buff { + result += c as u64; + } + + if result == 0 { + result + } else { + recurse(result) + 1 + } + } + + recurse(42); + } + }); +} + +// --- diff --git a/crates/re_viewer/src/top_panel.rs b/crates/re_viewer/src/top_panel.rs new file mode 100644 index 000000000000..5bb956fe60ef --- /dev/null +++ b/crates/re_viewer/src/top_panel.rs @@ -0,0 +1,282 @@ +use re_format::format_number; +use re_renderer::WgpuResourcePoolStatistics; +use re_ui::Command; + +use crate::{ui::Blueprint, App}; + +pub fn top_panel( + blueprint: &Blueprint, + ui: &mut egui::Ui, + frame: &mut eframe::Frame, + app: &mut App, + gpu_resource_stats: &WgpuResourcePoolStatistics, +) { + re_tracing::profile_function!(); + + let native_pixels_per_point = frame.info().native_pixels_per_point; + let fullscreen = { + #[cfg(target_arch = "wasm32")] + { + false + } + #[cfg(not(target_arch = "wasm32"))] + { + frame.info().window_info.fullscreen + } + }; + let style_like_web = app.screenshotter.is_screenshotting(); + let top_bar_style = + app.re_ui + .top_bar_style(native_pixels_per_point, fullscreen, style_like_web); + + egui::TopBottomPanel::top("top_bar") + .frame(app.re_ui.top_panel_frame()) + .exact_height(top_bar_style.height) + .show_inside(ui, |ui| { + let _response = egui::menu::bar(ui, |ui| { + ui.set_height(top_bar_style.height); + ui.add_space(top_bar_style.indent); + + top_bar_ui(blueprint, ui, frame, app, gpu_resource_stats); + }) + .response; + + #[cfg(not(target_arch = "wasm32"))] + if !re_ui::NATIVE_WINDOW_BAR { + let title_bar_response = _response.interact(egui::Sense::click()); + if title_bar_response.double_clicked() { + frame.set_maximized(!frame.info().window_info.maximized); + } else if title_bar_response.is_pointer_button_down_on() { + frame.drag_window(); + } + } + }); +} + +fn top_bar_ui( + blueprint: &Blueprint, + ui: &mut egui::Ui, + frame: &mut eframe::Frame, + app: &mut App, + gpu_resource_stats: &WgpuResourcePoolStatistics, +) { + crate::rerun_menu::rerun_menu_button_ui(ui, frame, app); + + if app.state.app_options.show_metrics { + ui.separator(); + frame_time_label_ui(ui, app); + memory_use_label_ui(ui, gpu_resource_stats); + input_latency_label_ui(ui, app); + } + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if re_ui::CUSTOM_WINDOW_DECORATIONS && !cfg!(target_arch = "wasm32") { + ui.add_space(8.0); + #[cfg(not(target_arch = "wasm32"))] + re_ui::native_window_buttons_ui(frame, ui); + ui.separator(); + } else { + // Make the first button the same distance form the side as from the top, + // no matter how high the top bar is. + let extra_margin = (ui.available_height() - 24.0) / 2.0; + ui.add_space(extra_margin); + } + + let mut selection_panel_expanded = blueprint.selection_panel_expanded; + if app + .re_ui + .medium_icon_toggle_button( + ui, + &re_ui::icons::RIGHT_PANEL_TOGGLE, + &mut selection_panel_expanded, + ) + .on_hover_text(format!( + "Toggle Selection View{}", + Command::ToggleSelectionPanel.format_shortcut_tooltip_suffix(ui.ctx()) + )) + .clicked() + { + app.pending_commands.push(Command::ToggleSelectionPanel); + } + + let mut time_panel_expanded = blueprint.time_panel_expanded; + if app + .re_ui + .medium_icon_toggle_button( + ui, + &re_ui::icons::BOTTOM_PANEL_TOGGLE, + &mut time_panel_expanded, + ) + .on_hover_text(format!( + "Toggle Timeline View{}", + Command::ToggleTimePanel.format_shortcut_tooltip_suffix(ui.ctx()) + )) + .clicked() + { + app.pending_commands.push(Command::ToggleTimePanel); + } + + let mut blueprint_panel_expanded = blueprint.blueprint_panel_expanded; + if app + .re_ui + .medium_icon_toggle_button( + ui, + &re_ui::icons::LEFT_PANEL_TOGGLE, + &mut blueprint_panel_expanded, + ) + .on_hover_text(format!( + "Toggle Blueprint View{}", + Command::ToggleBlueprintPanel.format_shortcut_tooltip_suffix(ui.ctx()) + )) + .clicked() + { + app.pending_commands.push(Command::ToggleBlueprintPanel); + } + + if cfg!(debug_assertions) && app.state.app_options.show_metrics { + ui.vertical_centered(|ui| { + ui.style_mut().wrap = Some(false); + ui.add_space(6.0); // TODO(emilk): in egui, add a proper way of centering a single widget in a UI. + egui::warn_if_debug_build(ui); + }); + } + }); +} + +fn frame_time_label_ui(ui: &mut egui::Ui, app: &mut App) { + if let Some(frame_time) = app.frame_time_history.average() { + let ms = frame_time * 1e3; + + let visuals = ui.visuals(); + let color = if ms < 15.0 { + visuals.weak_text_color() + } else { + visuals.warn_fg_color + }; + + // we use monospace so the width doesn't fluctuate as the numbers change. + let text = format!("{ms:.1} ms"); + ui.label(egui::RichText::new(text).monospace().color(color)) + .on_hover_text("CPU time used by Rerun Viewer each frame. Lower is better."); + } +} + +fn memory_use_label_ui(ui: &mut egui::Ui, gpu_resource_stats: &WgpuResourcePoolStatistics) { + const CODE: &str = "use re_memory::AccountingAllocator;\n\ + #[global_allocator]\n\ + static GLOBAL: AccountingAllocator =\n \ + AccountingAllocator::new(std::alloc::System);"; + + fn click_to_copy( + ui: &mut egui::Ui, + text: impl Into, + add_contents_on_hover: impl FnOnce(&mut egui::Ui), + ) { + #[allow(clippy::blocks_in_if_conditions)] + let text = text.into(); + if ui + .add( + egui::Label::new( + egui::RichText::new(text) + .monospace() + .color(ui.visuals().weak_text_color()), + ) + .sense(egui::Sense::click()), + ) + .on_hover_ui(|ui| add_contents_on_hover(ui)) + .clicked() + { + ui.ctx().output_mut(|o| o.copied_text = CODE.to_owned()); + } + } + + let mem = re_memory::MemoryUse::capture(); + + if let Some(count) = re_memory::accounting_allocator::global_allocs() { + // we use monospace so the width doesn't fluctuate as the numbers change. + + let bytes_used_text = re_format::format_bytes(count.size as _); + ui.label( + egui::RichText::new(&bytes_used_text) + .monospace() + .color(ui.visuals().weak_text_color()), + ) + .on_hover_text(format!( + "Rerun Viewer is using {} of RAM in {} separate allocations,\n\ + plus {} of GPU memory in {} textures and {} buffers.", + bytes_used_text, + format_number(count.count), + re_format::format_bytes(gpu_resource_stats.total_bytes() as _), + format_number(gpu_resource_stats.num_textures), + format_number(gpu_resource_stats.num_buffers), + )); + } else if let Some(rss) = mem.resident { + let bytes_used_text = re_format::format_bytes(rss as _); + click_to_copy(ui, &bytes_used_text, |ui| { + ui.label(format!( + "Rerun Viewer is using {} of Resident memory (RSS),\n\ + plus {} of GPU memory in {} textures and {} buffers.", + bytes_used_text, + re_format::format_bytes(gpu_resource_stats.total_bytes() as _), + format_number(gpu_resource_stats.num_textures), + format_number(gpu_resource_stats.num_buffers), + )); + ui.label( + "To get more accurate memory reportings, consider configuring your Rerun \n\ + viewer to use an AccountingAllocator by adding the following to your \n\ + code's main entrypoint:", + ); + ui.code(CODE); + ui.label("(click to copy to clipboard)"); + }); + } else { + click_to_copy(ui, "N/A MiB", |ui| { + ui.label( + "The Rerun viewer was not configured to run with an AccountingAllocator,\n\ + consider adding the following to your code's main entrypoint:", + ); + ui.code(CODE); + ui.label("(click to copy to clipboard)"); + }); + } +} + +fn input_latency_label_ui(ui: &mut egui::Ui, app: &mut App) { + // TODO(emilk): it would be nice to know if the network stream is still open + let is_latency_interesting = app.rx.source().is_network(); + + let queue_len = app.rx.len(); + + // empty queue == unreliable latency + let latency_sec = app.rx.latency_ns() as f32 / 1e9; + if queue_len > 0 + && (!is_latency_interesting || app.state.app_options.warn_latency < latency_sec) + { + // we use this to avoid flicker + app.latest_queue_interest = web_time::Instant::now(); + } + + if app.latest_queue_interest.elapsed().as_secs_f32() < 1.0 { + ui.separator(); + if is_latency_interesting { + let text = format!( + "Latency: {:.2}s, queue: {}", + latency_sec, + format_number(queue_len), + ); + let hover_text = + "When more data is arriving over network than the Rerun Viewer can index, a queue starts building up, leading to latency and increased RAM use.\n\ + This latency does NOT include network latency."; + + if latency_sec < app.state.app_options.warn_latency { + ui.weak(text).on_hover_text(hover_text); + } else { + ui.label(app.re_ui.warning_text(text)) + .on_hover_text(hover_text); + } + } else { + ui.weak(format!("Queue: {}", format_number(queue_len))) + .on_hover_text("Number of messages in the inbound queue"); + } + } +} From 0fd68e815c6f1171f0d7727f701f6f450b665427 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 2 Jun 2023 09:51:10 +0200 Subject: [PATCH 02/26] make re_ui private again --- crates/re_viewer/src/app.rs | 6 +++++- crates/re_viewer/src/rerun_menu.rs | 2 +- crates/re_viewer/src/top_panel.rs | 12 ++++++------ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index 02916cad6fcf..1a0f03d74153 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -80,7 +80,7 @@ pub struct App { build_info: re_build_info::BuildInfo, startup_options: StartupOptions, ram_limit_warner: re_memory::RamLimitWarner, - pub(crate) re_ui: re_ui::ReUi, + re_ui: re_ui::ReUi, pub(crate) screenshotter: crate::screenshotter::Screenshotter, /// Listens to the local text log stream @@ -215,6 +215,10 @@ impl App { &self.build_info } + pub fn re_ui(&self) -> &re_ui::ReUi { + &self.re_ui + } + /// Adds a new space view class to the viewer. pub fn add_space_view_class( &mut self, diff --git a/crates/re_viewer/src/rerun_menu.rs b/crates/re_viewer/src/rerun_menu.rs index c8b37373bf4d..34486aa186cc 100644 --- a/crates/re_viewer/src/rerun_menu.rs +++ b/crates/re_viewer/src/rerun_menu.rs @@ -14,7 +14,7 @@ pub fn rerun_menu_button_ui(ui: &mut egui::Ui, frame: &mut eframe::Frame, app: & let desired_icon_height = ui.max_rect().height() - 4.0; // TODO(emilk): figure out this fudge let desired_icon_height = desired_icon_height.at_most(28.0); // figma size 2023-02-03 - let icon_image = app.re_ui.icon_image(&re_ui::icons::RERUN_MENU); + let icon_image = app.re_ui().icon_image(&re_ui::icons::RERUN_MENU); let image_size = icon_image.size_vec2() * (desired_icon_height / icon_image.size_vec2().y); let texture_id = icon_image.texture_id(ui.ctx()); diff --git a/crates/re_viewer/src/top_panel.rs b/crates/re_viewer/src/top_panel.rs index 5bb956fe60ef..96c436f027bd 100644 --- a/crates/re_viewer/src/top_panel.rs +++ b/crates/re_viewer/src/top_panel.rs @@ -26,11 +26,11 @@ pub fn top_panel( }; let style_like_web = app.screenshotter.is_screenshotting(); let top_bar_style = - app.re_ui + app.re_ui() .top_bar_style(native_pixels_per_point, fullscreen, style_like_web); egui::TopBottomPanel::top("top_bar") - .frame(app.re_ui.top_panel_frame()) + .frame(app.re_ui().top_panel_frame()) .exact_height(top_bar_style.height) .show_inside(ui, |ui| { let _response = egui::menu::bar(ui, |ui| { @@ -84,7 +84,7 @@ fn top_bar_ui( let mut selection_panel_expanded = blueprint.selection_panel_expanded; if app - .re_ui + .re_ui() .medium_icon_toggle_button( ui, &re_ui::icons::RIGHT_PANEL_TOGGLE, @@ -101,7 +101,7 @@ fn top_bar_ui( let mut time_panel_expanded = blueprint.time_panel_expanded; if app - .re_ui + .re_ui() .medium_icon_toggle_button( ui, &re_ui::icons::BOTTOM_PANEL_TOGGLE, @@ -118,7 +118,7 @@ fn top_bar_ui( let mut blueprint_panel_expanded = blueprint.blueprint_panel_expanded; if app - .re_ui + .re_ui() .medium_icon_toggle_button( ui, &re_ui::icons::LEFT_PANEL_TOGGLE, @@ -271,7 +271,7 @@ fn input_latency_label_ui(ui: &mut egui::Ui, app: &mut App) { if latency_sec < app.state.app_options.warn_latency { ui.weak(text).on_hover_text(hover_text); } else { - ui.label(app.re_ui.warning_text(text)) + ui.label(app.re_ui().warning_text(text)) .on_hover_text(hover_text); } } else { From 563f6ffad553ad8c08a0195f59a4af515deb52e8 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 2 Jun 2023 12:21:41 +0200 Subject: [PATCH 03/26] inline once-off function --- crates/re_viewer/src/app.rs | 6 ------ crates/re_viewer/src/rerun_menu.rs | 5 ++++- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index 1a0f03d74153..890478a6a589 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -1021,12 +1021,6 @@ impl App { egui_ctx.set_style((*style).clone()); } - /// Do we have an open [`StoreDb`] that is non-empty? - pub(crate) fn store_db_is_nonempty(&self) -> bool { - self.store_db() - .map_or(false, |store_db| !store_db.is_empty()) - } - /// Get access to the currently shown [`StoreDb`], if any. pub fn store_db(&self) -> Option<&StoreDb> { self.state diff --git a/crates/re_viewer/src/rerun_menu.rs b/crates/re_viewer/src/rerun_menu.rs index 34486aa186cc..c73039129d8b 100644 --- a/crates/re_viewer/src/rerun_menu.rs +++ b/crates/re_viewer/src/rerun_menu.rs @@ -268,7 +268,10 @@ fn save_buttons_ui(ui: &mut egui::Ui, app: &mut App) { }); }); } else { - ui.add_enabled_ui(app.store_db_is_nonempty(), |ui| { + let store_db_is_nonempty = app + .store_db() + .map_or(false, |store_db| !store_db.is_empty()); + ui.add_enabled_ui(store_db_is_nonempty, |ui| { if ui .add(save_button) .on_hover_text("Save all data to a Rerun data file (.rrd)") From 43ea299d4d07c7d132b9a31e68513dd0efacdd06 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 2 Jun 2023 12:25:59 +0200 Subject: [PATCH 04/26] Make app_options private --- crates/re_viewer/src/app.rs | 10 +++++++++- crates/re_viewer/src/rerun_menu.rs | 4 ++-- crates/re_viewer/src/top_panel.rs | 8 ++++---- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index 890478a6a589..646ac63f392b 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -1116,7 +1116,7 @@ fn preview_files_being_dropped(egui_ctx: &egui::Context) { #[serde(default)] pub(crate) struct AppState { /// Global options for the whole viewer. - pub(crate) app_options: AppOptions, + app_options: AppOptions, /// Things that need caching. #[serde(skip)] @@ -1144,6 +1144,14 @@ pub(crate) struct AppState { } impl AppState { + pub fn app_options(&self) -> &AppOptions { + &self.app_options + } + + pub fn app_options_mut(&mut self) -> &mut AppOptions { + &mut self.app_options + } + #[allow(clippy::too_many_arguments)] fn show( &mut self, diff --git a/crates/re_viewer/src/rerun_menu.rs b/crates/re_viewer/src/rerun_menu.rs index c73039129d8b..1d0a2349ccfc 100644 --- a/crates/re_viewer/src/rerun_menu.rs +++ b/crates/re_viewer/src/rerun_menu.rs @@ -39,7 +39,7 @@ pub fn rerun_menu_button_ui(ui: &mut egui::Ui, frame: &mut eframe::Frame, app: & ui.add_space(spacing); // On the web the browser controls the zoom - let zoom_factor = app.state.app_options.zoom_factor; + let zoom_factor = app.state.app_options().zoom_factor; ui.weak(format!("Zoom {:.0}%", zoom_factor * 100.0)) .on_hover_text("The zoom factor applied on top of the OS scaling factor."); Command::ZoomIn.menu_button_ui(ui, &mut app.pending_commands); @@ -73,7 +73,7 @@ pub fn rerun_menu_button_ui(ui: &mut egui::Ui, frame: &mut eframe::Frame, app: & }); ui.menu_button("Options", |ui| { - options_menu_ui(ui, frame, &mut app.state.app_options); + options_menu_ui(ui, frame, app.state.app_options_mut()); }); ui.add_space(spacing); diff --git a/crates/re_viewer/src/top_panel.rs b/crates/re_viewer/src/top_panel.rs index 96c436f027bd..d51beb73829d 100644 --- a/crates/re_viewer/src/top_panel.rs +++ b/crates/re_viewer/src/top_panel.rs @@ -62,7 +62,7 @@ fn top_bar_ui( ) { crate::rerun_menu::rerun_menu_button_ui(ui, frame, app); - if app.state.app_options.show_metrics { + if app.state.app_options().show_metrics { ui.separator(); frame_time_label_ui(ui, app); memory_use_label_ui(ui, gpu_resource_stats); @@ -133,7 +133,7 @@ fn top_bar_ui( app.pending_commands.push(Command::ToggleBlueprintPanel); } - if cfg!(debug_assertions) && app.state.app_options.show_metrics { + if cfg!(debug_assertions) && app.state.app_options().show_metrics { ui.vertical_centered(|ui| { ui.style_mut().wrap = Some(false); ui.add_space(6.0); // TODO(emilk): in egui, add a proper way of centering a single widget in a UI. @@ -250,7 +250,7 @@ fn input_latency_label_ui(ui: &mut egui::Ui, app: &mut App) { // empty queue == unreliable latency let latency_sec = app.rx.latency_ns() as f32 / 1e9; if queue_len > 0 - && (!is_latency_interesting || app.state.app_options.warn_latency < latency_sec) + && (!is_latency_interesting || app.state.app_options().warn_latency < latency_sec) { // we use this to avoid flicker app.latest_queue_interest = web_time::Instant::now(); @@ -268,7 +268,7 @@ fn input_latency_label_ui(ui: &mut egui::Ui, app: &mut App) { "When more data is arriving over network than the Rerun Viewer can index, a queue starts building up, leading to latency and increased RAM use.\n\ This latency does NOT include network latency."; - if latency_sec < app.state.app_options.warn_latency { + if latency_sec < app.state.app_options().warn_latency { ui.weak(text).on_hover_text(hover_text); } else { ui.label(app.re_ui().warning_text(text)) From 665d96a77d43c4163e6e80090d3d11ec69def8c1 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 2 Jun 2023 14:42:59 +0200 Subject: [PATCH 05/26] Less pub(crate) --- crates/re_viewer/src/app.rs | 6 +++++- crates/re_viewer/src/top_panel.rs | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index 646ac63f392b..e1f2fe01f2ce 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -81,7 +81,7 @@ pub struct App { startup_options: StartupOptions, ram_limit_warner: re_memory::RamLimitWarner, re_ui: re_ui::ReUi, - pub(crate) screenshotter: crate::screenshotter::Screenshotter, + screenshotter: crate::screenshotter::Screenshotter, /// Listens to the local text log stream text_log_rx: std::sync::mpsc::Receiver, @@ -219,6 +219,10 @@ impl App { &self.re_ui } + pub fn is_screenshotting(&self) -> bool { + self.screenshotter.is_screenshotting() + } + /// Adds a new space view class to the viewer. pub fn add_space_view_class( &mut self, diff --git a/crates/re_viewer/src/top_panel.rs b/crates/re_viewer/src/top_panel.rs index d51beb73829d..ef6918d69863 100644 --- a/crates/re_viewer/src/top_panel.rs +++ b/crates/re_viewer/src/top_panel.rs @@ -24,7 +24,7 @@ pub fn top_panel( frame.info().window_info.fullscreen } }; - let style_like_web = app.screenshotter.is_screenshotting(); + let style_like_web = app.is_screenshotting(); let top_bar_style = app.re_ui() .top_bar_style(native_pixels_per_point, fullscreen, style_like_web); From de92245d938974a3c428142a95d7cdd11a660b1d Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 2 Jun 2023 14:48:34 +0200 Subject: [PATCH 06/26] Less pub(crate) --- crates/re_viewer/src/app.rs | 14 +++++++++++++- crates/re_viewer/src/rerun_menu.rs | 4 ++-- crates/re_viewer/src/top_panel.rs | 18 +++++++++--------- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index e1f2fe01f2ce..dfd3c00d359d 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -88,7 +88,7 @@ pub struct App { component_ui_registry: ComponentUiRegistry, - pub(crate) rx: Receiver, + rx: Receiver, /// Where the recordings and blueprints are stored. pub(crate) store_hub: crate::StoreHub, @@ -219,10 +219,22 @@ impl App { &self.re_ui } + 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 msg_receiver(&self) -> &Receiver { + &self.rx + } + /// Adds a new space view class to the viewer. pub fn add_space_view_class( &mut self, diff --git a/crates/re_viewer/src/rerun_menu.rs b/crates/re_viewer/src/rerun_menu.rs index 1d0a2349ccfc..32467ceb79a2 100644 --- a/crates/re_viewer/src/rerun_menu.rs +++ b/crates/re_viewer/src/rerun_menu.rs @@ -39,7 +39,7 @@ pub fn rerun_menu_button_ui(ui: &mut egui::Ui, frame: &mut eframe::Frame, app: & ui.add_space(spacing); // On the web the browser controls the zoom - let zoom_factor = app.state.app_options().zoom_factor; + let zoom_factor = app.app_options().zoom_factor; ui.weak(format!("Zoom {:.0}%", zoom_factor * 100.0)) .on_hover_text("The zoom factor applied on top of the OS scaling factor."); Command::ZoomIn.menu_button_ui(ui, &mut app.pending_commands); @@ -73,7 +73,7 @@ pub fn rerun_menu_button_ui(ui: &mut egui::Ui, frame: &mut eframe::Frame, app: & }); ui.menu_button("Options", |ui| { - options_menu_ui(ui, frame, app.state.app_options_mut()); + options_menu_ui(ui, frame, app.app_options_mut()); }); ui.add_space(spacing); diff --git a/crates/re_viewer/src/top_panel.rs b/crates/re_viewer/src/top_panel.rs index ef6918d69863..60e4238c5ad0 100644 --- a/crates/re_viewer/src/top_panel.rs +++ b/crates/re_viewer/src/top_panel.rs @@ -62,7 +62,7 @@ fn top_bar_ui( ) { crate::rerun_menu::rerun_menu_button_ui(ui, frame, app); - if app.state.app_options().show_metrics { + if app.app_options().show_metrics { ui.separator(); frame_time_label_ui(ui, app); memory_use_label_ui(ui, gpu_resource_stats); @@ -133,7 +133,7 @@ fn top_bar_ui( app.pending_commands.push(Command::ToggleBlueprintPanel); } - if cfg!(debug_assertions) && app.state.app_options().show_metrics { + if cfg!(debug_assertions) && app.app_options().show_metrics { ui.vertical_centered(|ui| { ui.style_mut().wrap = Some(false); ui.add_space(6.0); // TODO(emilk): in egui, add a proper way of centering a single widget in a UI. @@ -242,16 +242,16 @@ fn memory_use_label_ui(ui: &mut egui::Ui, gpu_resource_stats: &WgpuResourcePoolS } fn input_latency_label_ui(ui: &mut egui::Ui, app: &mut App) { + let rx = app.msg_receiver(); + // TODO(emilk): it would be nice to know if the network stream is still open - let is_latency_interesting = app.rx.source().is_network(); + let is_latency_interesting = rx.source().is_network(); - let queue_len = app.rx.len(); + let queue_len = rx.len(); // empty queue == unreliable latency - let latency_sec = app.rx.latency_ns() as f32 / 1e9; - if queue_len > 0 - && (!is_latency_interesting || app.state.app_options().warn_latency < latency_sec) - { + let latency_sec = rx.latency_ns() as f32 / 1e9; + if queue_len > 0 && (!is_latency_interesting || app.app_options().warn_latency < latency_sec) { // we use this to avoid flicker app.latest_queue_interest = web_time::Instant::now(); } @@ -268,7 +268,7 @@ fn input_latency_label_ui(ui: &mut egui::Ui, app: &mut App) { "When more data is arriving over network than the Rerun Viewer can index, a queue starts building up, leading to latency and increased RAM use.\n\ This latency does NOT include network latency."; - if latency_sec < app.state.app_options().warn_latency { + if latency_sec < app.app_options().warn_latency { ui.weak(text).on_hover_text(hover_text); } else { ui.label(app.re_ui().warning_text(text)) From 846c49f51d1aaabfa832a7fdba01cc85feef5d3b Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 5 Jun 2023 11:43:27 +0200 Subject: [PATCH 07/26] Remove unnecessary mut --- crates/re_viewer/src/app.rs | 4 ++-- crates/re_viewer_context/src/viewer_context.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index dfd3c00d359d..9b384d901d23 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -1183,7 +1183,7 @@ impl AppState { re_tracing::profile_function!(); let Self { - app_options: options, + app_options, cache, selected_rec_id: _, selected_blueprint_by_app: _, @@ -1203,7 +1203,7 @@ impl AppState { ); let mut ctx = ViewerContext { - app_options: options, + app_options, cache, space_view_class_registry, component_ui_registry, diff --git a/crates/re_viewer_context/src/viewer_context.rs b/crates/re_viewer_context/src/viewer_context.rs index 50e73d1a93d0..7433a56a6cfb 100644 --- a/crates/re_viewer_context/src/viewer_context.rs +++ b/crates/re_viewer_context/src/viewer_context.rs @@ -8,7 +8,7 @@ use crate::{ /// Common things needed by many parts of the viewer. pub struct ViewerContext<'a> { /// Global options for the whole viewer. - pub app_options: &'a mut AppOptions, + pub app_options: &'a AppOptions, /// Things that need caching and are shared across the whole viewer. /// From eff09ae9b1c031c886a30701221a4889e9558aad Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 5 Jun 2023 11:49:31 +0200 Subject: [PATCH 08/26] Move ui files to ui mod --- crates/re_viewer/src/app.rs | 2 +- crates/re_viewer/src/lib.rs | 2 -- crates/re_viewer/src/ui/mod.rs | 6 +++++- crates/re_viewer/src/{ => ui}/rerun_menu.rs | 0 crates/re_viewer/src/{ => ui}/top_panel.rs | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) rename crates/re_viewer/src/{ => ui}/rerun_menu.rs (100%) rename crates/re_viewer/src/{ => ui}/top_panel.rs (99%) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index 9b384d901d23..d54a5c330aca 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -666,7 +666,7 @@ impl eframe::App for App { warning_panel(&self.re_ui, ui, frame); - crate::top_panel::top_panel(&blueprint, ui, frame, self, &gpu_resource_stats); + crate::ui::top_panel(&blueprint, ui, frame, self, &gpu_resource_stats); self.memory_panel_ui( ui, diff --git a/crates/re_viewer/src/lib.rs b/crates/re_viewer/src/lib.rs index 22de010c7f1f..8b3d06122b61 100644 --- a/crates/re_viewer/src/lib.rs +++ b/crates/re_viewer/src/lib.rs @@ -10,10 +10,8 @@ mod loading; #[cfg(not(target_arch = "wasm32"))] mod profiler; mod remote_viewer_app; -mod rerun_menu; mod screenshotter; mod store_hub; -mod top_panel; mod ui; mod viewer_analytics; diff --git a/crates/re_viewer/src/ui/mod.rs b/crates/re_viewer/src/ui/mod.rs index 60dfdd50bd1a..17f1375be39e 100644 --- a/crates/re_viewer/src/ui/mod.rs +++ b/crates/re_viewer/src/ui/mod.rs @@ -1,11 +1,15 @@ mod blueprint; mod blueprint_load; mod blueprint_sync; +mod rerun_menu; mod selection_history_ui; +mod top_panel; pub(crate) mod memory_panel; pub(crate) mod selection_panel; // ---- -pub(crate) use self::blueprint::Blueprint; +pub(crate) use { + self::blueprint::Blueprint, self::rerun_menu::rerun_menu_button_ui, self::top_panel::top_panel, +}; diff --git a/crates/re_viewer/src/rerun_menu.rs b/crates/re_viewer/src/ui/rerun_menu.rs similarity index 100% rename from crates/re_viewer/src/rerun_menu.rs rename to crates/re_viewer/src/ui/rerun_menu.rs diff --git a/crates/re_viewer/src/top_panel.rs b/crates/re_viewer/src/ui/top_panel.rs similarity index 99% rename from crates/re_viewer/src/top_panel.rs rename to crates/re_viewer/src/ui/top_panel.rs index 60e4238c5ad0..eb15793763e9 100644 --- a/crates/re_viewer/src/top_panel.rs +++ b/crates/re_viewer/src/ui/top_panel.rs @@ -60,7 +60,7 @@ fn top_bar_ui( app: &mut App, gpu_resource_stats: &WgpuResourcePoolStatistics, ) { - crate::rerun_menu::rerun_menu_button_ui(ui, frame, app); + crate::ui::rerun_menu_button_ui(ui, frame, app); if app.app_options().show_metrics { ui.separator(); From b275c1d8b01563b39ca1910eaa2a17a6c1ca9c4f Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 5 Jun 2023 12:38:33 +0200 Subject: [PATCH 09/26] Break out the wait_screen_ui --- crates/re_viewer/src/app.rs | 58 +---------------------- crates/re_viewer/src/ui/mod.rs | 2 + crates/re_viewer/src/ui/wait_screen_ui.rs | 58 +++++++++++++++++++++++ 3 files changed, 62 insertions(+), 56 deletions(-) create mode 100644 crates/re_viewer/src/ui/wait_screen_ui.rs diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index d54a5c330aca..6ecfe5d27740 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -705,7 +705,7 @@ impl eframe::App for App { render_ctx.begin_frame(); if store_db.is_empty() { - wait_screen_ui(ui, &self.rx); + crate::ui::wait_screen_ui(ui, &self.rx); } else { self.state.show( &mut blueprint, @@ -722,7 +722,7 @@ impl eframe::App for App { } } } else { - wait_screen_ui(ui, &self.rx); + crate::ui::wait_screen_ui(ui, &self.rx); } }); @@ -796,60 +796,6 @@ fn paint_native_window_frame(egui_ctx: &egui::Context) { ); } -fn wait_screen_ui(ui: &mut egui::Ui, rx: &Receiver) { - ui.centered_and_justified(|ui| { - fn ready_and_waiting(ui: &mut egui::Ui, txt: &str) { - let style = ui.style(); - let mut layout_job = egui::text::LayoutJob::default(); - layout_job.append( - "Ready", - 0.0, - egui::TextFormat::simple( - egui::TextStyle::Heading.resolve(style), - style.visuals.strong_text_color(), - ), - ); - layout_job.append( - &format!("\n\n{txt}"), - 0.0, - egui::TextFormat::simple( - egui::TextStyle::Body.resolve(style), - style.visuals.text_color(), - ), - ); - layout_job.halign = egui::Align::Center; - ui.label(layout_job); - } - - match rx.source() { - re_smart_channel::SmartChannelSource::Files { paths } => { - ui.strong(format!( - "Loading {}…", - paths - .iter() - .format_with(", ", |path, f| f(&format_args!("{}", path.display()))) - )); - } - re_smart_channel::SmartChannelSource::RrdHttpStream { url } => { - ui.strong(format!("Loading {url}…")); - } - re_smart_channel::SmartChannelSource::RrdWebEventListener => { - ready_and_waiting(ui, "Waiting for logging data…"); - } - re_smart_channel::SmartChannelSource::Sdk => { - ready_and_waiting(ui, "Waiting for logging data from SDK"); - } - re_smart_channel::SmartChannelSource::WsClient { ws_server_url } => { - // TODO(emilk): it would be even better to know whether or not we are connected, or are attempting to connect - ready_and_waiting(ui, &format!("Waiting for data from {ws_server_url}")); - } - re_smart_channel::SmartChannelSource::TcpServer { port } => { - ready_and_waiting(ui, &format!("Listening on port {port}")); - } - }; - }); -} - impl App { /// Show recent text log messages to the user as toast notifications. fn show_text_logs_as_notifications(&mut self) { diff --git a/crates/re_viewer/src/ui/mod.rs b/crates/re_viewer/src/ui/mod.rs index 17f1375be39e..bd8d0d8b9aa3 100644 --- a/crates/re_viewer/src/ui/mod.rs +++ b/crates/re_viewer/src/ui/mod.rs @@ -4,6 +4,7 @@ mod blueprint_sync; mod rerun_menu; mod selection_history_ui; mod top_panel; +mod wait_screen_ui; pub(crate) mod memory_panel; pub(crate) mod selection_panel; @@ -12,4 +13,5 @@ pub(crate) mod selection_panel; pub(crate) use { self::blueprint::Blueprint, self::rerun_menu::rerun_menu_button_ui, self::top_panel::top_panel, + self::wait_screen_ui::wait_screen_ui, }; diff --git a/crates/re_viewer/src/ui/wait_screen_ui.rs b/crates/re_viewer/src/ui/wait_screen_ui.rs new file mode 100644 index 000000000000..5e3d3b6c25f8 --- /dev/null +++ b/crates/re_viewer/src/ui/wait_screen_ui.rs @@ -0,0 +1,58 @@ +use re_log_types::LogMsg; +use re_smart_channel::Receiver; + +use itertools::Itertools as _; + +pub fn wait_screen_ui(ui: &mut egui::Ui, rx: &Receiver) { + ui.centered_and_justified(|ui| { + fn ready_and_waiting(ui: &mut egui::Ui, txt: &str) { + let style = ui.style(); + let mut layout_job = egui::text::LayoutJob::default(); + layout_job.append( + "Ready", + 0.0, + egui::TextFormat::simple( + egui::TextStyle::Heading.resolve(style), + style.visuals.strong_text_color(), + ), + ); + layout_job.append( + &format!("\n\n{txt}"), + 0.0, + egui::TextFormat::simple( + egui::TextStyle::Body.resolve(style), + style.visuals.text_color(), + ), + ); + layout_job.halign = egui::Align::Center; + ui.label(layout_job); + } + + match rx.source() { + re_smart_channel::SmartChannelSource::Files { paths } => { + ui.strong(format!( + "Loading {}…", + paths + .iter() + .format_with(", ", |path, f| f(&format_args!("{}", path.display()))) + )); + } + re_smart_channel::SmartChannelSource::RrdHttpStream { url } => { + ui.strong(format!("Loading {url}…")); + } + re_smart_channel::SmartChannelSource::RrdWebEventListener => { + ready_and_waiting(ui, "Waiting for logging data…"); + } + re_smart_channel::SmartChannelSource::Sdk => { + ready_and_waiting(ui, "Waiting for logging data from SDK"); + } + re_smart_channel::SmartChannelSource::WsClient { ws_server_url } => { + // TODO(emilk): it would be even better to know whether or not we are connected, or are attempting to connect + ready_and_waiting(ui, &format!("Waiting for data from {ws_server_url}")); + } + re_smart_channel::SmartChannelSource::TcpServer { port } => { + ready_and_waiting(ui, &format!("Listening on port {port}")); + } + }; + }); +} From 328219dc84884d6f77d627a0327b417e7967aa7f Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 5 Jun 2023 12:43:24 +0200 Subject: [PATCH 10/26] Remove outdated workaround for old egui bug --- crates/re_viewer/src/app.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index 6ecfe5d27740..b699048a3b8a 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -1199,12 +1199,8 @@ fn warning_panel(re_ui: &re_ui::ReUi, ui: &mut egui::Ui, frame: &mut eframe::Fra // We have not yet optimized the UI experience for mobile. Show a warning banner // with a link to the tracking issue. - // Although this banner is applicable to IOS / Android generically without limit to web - // There is a small issue in egui where Windows native currently reports as android. - // TODO(jleibs): Remove the is_web gate once https://github.com/emilk/egui/pull/2832 has landed. - if frame.is_web() - && (ui.ctx().os() == egui::os::OperatingSystem::IOS - || ui.ctx().os() == egui::os::OperatingSystem::Android) + if ui.ctx().os() == egui::os::OperatingSystem::IOS + || ui.ctx().os() == egui::os::OperatingSystem::Android { let frame = egui::Frame { fill: ui.visuals().panel_fill, From 6373bbd61f03b31facb17dcca06a1a1b56a22001 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 5 Jun 2023 12:44:57 +0200 Subject: [PATCH 11/26] Move mobile_warning_ui to own file --- crates/re_viewer/src/app.rs | 27 +------------------- crates/re_viewer/src/ui/mobile_warning_ui.rs | 24 +++++++++++++++++ crates/re_viewer/src/ui/mod.rs | 4 ++- 3 files changed, 28 insertions(+), 27 deletions(-) create mode 100644 crates/re_viewer/src/ui/mobile_warning_ui.rs diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index b699048a3b8a..babaeae8c6a7 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -664,7 +664,7 @@ impl eframe::App for App { .show(egui_ctx, |ui| { paint_background_fill(ui); - warning_panel(&self.re_ui, ui, frame); + crate::ui::mobile_warning_ui(&self.re_ui, ui); crate::ui::top_panel(&blueprint, ui, frame, self, &gpu_resource_stats); @@ -1195,31 +1195,6 @@ impl AppState { } } -fn warning_panel(re_ui: &re_ui::ReUi, ui: &mut egui::Ui, frame: &mut eframe::Frame) { - // We have not yet optimized the UI experience for mobile. Show a warning banner - // with a link to the tracking issue. - - if ui.ctx().os() == egui::os::OperatingSystem::IOS - || ui.ctx().os() == egui::os::OperatingSystem::Android - { - let frame = egui::Frame { - fill: ui.visuals().panel_fill, - ..re_ui.bottom_panel_frame() - }; - - egui::TopBottomPanel::bottom("warning_panel") - .resizable(false) - .frame(frame) - .show_inside(ui, |ui| { - ui.centered_and_justified(|ui| { - let text = - re_ui.warning_text("Mobile OSes are not yet supported. Click for details."); - ui.hyperlink_to(text, "https://github.com/rerun-io/rerun/issues/1672"); - }); - }); - } -} - // ---------------------------------------------------------------------------- const FILE_SAVER_PROMISE: &str = "file_saver"; diff --git a/crates/re_viewer/src/ui/mobile_warning_ui.rs b/crates/re_viewer/src/ui/mobile_warning_ui.rs new file mode 100644 index 000000000000..b02b4dfad375 --- /dev/null +++ b/crates/re_viewer/src/ui/mobile_warning_ui.rs @@ -0,0 +1,24 @@ +pub fn mobile_warning_ui(re_ui: &re_ui::ReUi, ui: &mut egui::Ui) { + // We have not yet optimized the UI experience for mobile. Show a warning banner + // with a link to the tracking issue. + + if ui.ctx().os() == egui::os::OperatingSystem::IOS + || ui.ctx().os() == egui::os::OperatingSystem::Android + { + let frame = egui::Frame { + fill: ui.visuals().panel_fill, + ..re_ui.bottom_panel_frame() + }; + + egui::TopBottomPanel::bottom("warning_panel") + .resizable(false) + .frame(frame) + .show_inside(ui, |ui| { + ui.centered_and_justified(|ui| { + let text = + re_ui.warning_text("Mobile OSes are not yet supported. Click for details."); + ui.hyperlink_to(text, "https://github.com/rerun-io/rerun/issues/1672"); + }); + }); + } +} diff --git a/crates/re_viewer/src/ui/mod.rs b/crates/re_viewer/src/ui/mod.rs index bd8d0d8b9aa3..2e5a491a9a6f 100644 --- a/crates/re_viewer/src/ui/mod.rs +++ b/crates/re_viewer/src/ui/mod.rs @@ -1,6 +1,7 @@ mod blueprint; mod blueprint_load; mod blueprint_sync; +mod mobile_warning_ui; mod rerun_menu; mod selection_history_ui; mod top_panel; @@ -12,6 +13,7 @@ pub(crate) mod selection_panel; // ---- pub(crate) use { - self::blueprint::Blueprint, self::rerun_menu::rerun_menu_button_ui, self::top_panel::top_panel, + self::blueprint::Blueprint, self::mobile_warning_ui::mobile_warning_ui, + self::rerun_menu::rerun_menu_button_ui, self::top_panel::top_panel, self::wait_screen_ui::wait_screen_ui, }; From ec0bed788009731cf984336e5bbbc3e4d14dcb3c Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 5 Jun 2023 12:55:13 +0200 Subject: [PATCH 12/26] Move AppState to its own file --- crates/re_viewer/src/app.rs | 197 +++--------------------------- crates/re_viewer/src/app_state.rs | 181 +++++++++++++++++++++++++++ crates/re_viewer/src/lib.rs | 9 +- 3 files changed, 204 insertions(+), 183 deletions(-) create mode 100644 crates/re_viewer/src/app_state.rs diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index babaeae8c6a7..f9cdf1ebafe7 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -12,17 +12,14 @@ use re_renderer::WgpuResourcePoolStatistics; use re_smart_channel::Receiver; use re_ui::{toasts, Command}; use re_viewer_context::{ - AppOptions, Caches, ComponentUiRegistry, PlayState, RecordingConfig, SpaceViewClass, - SpaceViewClassRegistry, SpaceViewClassRegistryError, ViewerContext, + AppOptions, ComponentUiRegistry, PlayState, SpaceViewClass, SpaceViewClassRegistry, + SpaceViewClassRegistryError, }; -use re_viewport::ViewportState; -use crate::{ui::Blueprint, viewer_analytics::ViewerAnalytics, StoreHub}; +use crate::{ui::Blueprint, viewer_analytics::ViewerAnalytics, AppState, StoreHub}; use re_log_types::TimeRangeF; -const WATERMARK: bool = false; // Nice for recording media material - // ---------------------------------------------------------------------------- #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -394,15 +391,15 @@ impl App { } #[cfg(not(target_arch = "wasm32"))] Command::ZoomIn => { - self.state.app_options.zoom_factor += 0.1; + self.app_options_mut().zoom_factor += 0.1; } #[cfg(not(target_arch = "wasm32"))] Command::ZoomOut => { - self.state.app_options.zoom_factor -= 0.1; + self.app_options_mut().zoom_factor -= 0.1; } #[cfg(not(target_arch = "wasm32"))] Command::ZoomReset => { - self.state.app_options.zoom_factor = 1.0; + self.app_options_mut().zoom_factor = 1.0; } Command::SelectionPrevious => { @@ -569,15 +566,15 @@ impl eframe::App for App { } else { // Ensure zoom factor is sane and in 10% steps at all times before applying it. { - let mut zoom_factor = self.state.app_options.zoom_factor; + let mut zoom_factor = self.app_options().zoom_factor; zoom_factor = zoom_factor.clamp(MIN_ZOOM_FACTOR, MAX_ZOOM_FACTOR); zoom_factor = (zoom_factor * 10.).round() / 10.; - self.state.app_options.zoom_factor = zoom_factor; + self.app_options_mut().zoom_factor = zoom_factor; } // Apply zoom factor on top of natively reported pixel per point. let pixels_per_point = frame.info().native_pixels_per_point.unwrap_or(1.0) - * self.state.app_options.zoom_factor; + * self.app_options().zoom_factor; egui_ctx.set_pixels_per_point(pixels_per_point); } @@ -684,14 +681,14 @@ impl eframe::App for App { .as_ref() .and_then(|rec_id| self.store_hub.recording(rec_id)) { - recording_config_entry( - &mut self.state.recording_configs, - store_db.store_id().clone(), - self.rx.source(), - store_db, - ) - .selection_state - .on_frame_start(|item| blueprint.is_item_valid(item)); + self.state + .recording_config_entry( + store_db.store_id().clone(), + self.rx.source(), + store_db, + ) + .selection_state + .on_frame_start(|item| blueprint.is_item_valid(item)); // TODO(andreas): store the re_renderer somewhere else. let egui_renderer = { @@ -1072,129 +1069,6 @@ fn preview_files_being_dropped(egui_ctx: &egui::Context) { } } -// ------------------------------------------------------------------------------------ - -#[derive(Default, serde::Deserialize, serde::Serialize)] -#[serde(default)] -pub(crate) struct AppState { - /// Global options for the whole viewer. - app_options: AppOptions, - - /// Things that need caching. - #[serde(skip)] - cache: Caches, - - #[serde(skip)] - pub(crate) selected_rec_id: Option, - #[serde(skip)] - pub(crate) selected_blueprint_by_app: HashMap, - - /// Configuration for the current recording (found in [`StoreDb`]). - recording_configs: HashMap, - - selection_panel: crate::selection_panel::SelectionPanel, - time_panel: re_time_panel::TimePanel, - - #[cfg(not(target_arch = "wasm32"))] - #[serde(skip)] - profiler: crate::Profiler, - - // TODO(jleibs): This is sort of a weird place to put this but makes more - // sense than the blueprint - #[serde(skip)] - viewport_state: ViewportState, -} - -impl AppState { - pub fn app_options(&self) -> &AppOptions { - &self.app_options - } - - pub fn app_options_mut(&mut self) -> &mut AppOptions { - &mut self.app_options - } - - #[allow(clippy::too_many_arguments)] - fn show( - &mut self, - blueprint: &mut Blueprint, - ui: &mut egui::Ui, - render_ctx: &mut re_renderer::RenderContext, - store_db: &StoreDb, - re_ui: &re_ui::ReUi, - component_ui_registry: &ComponentUiRegistry, - space_view_class_registry: &SpaceViewClassRegistry, - rx: &Receiver, - ) { - re_tracing::profile_function!(); - - let Self { - app_options, - cache, - selected_rec_id: _, - selected_blueprint_by_app: _, - recording_configs, - selection_panel, - time_panel, - #[cfg(not(target_arch = "wasm32"))] - profiler: _, - viewport_state, - } = self; - - let rec_cfg = recording_config_entry( - recording_configs, - store_db.store_id().clone(), - rx.source(), - store_db, - ); - - let mut ctx = ViewerContext { - app_options, - cache, - space_view_class_registry, - component_ui_registry, - store_db, - rec_cfg, - re_ui, - render_ctx, - }; - - time_panel.show_panel(&mut ctx, ui, blueprint.time_panel_expanded); - selection_panel.show_panel(viewport_state, &mut ctx, ui, blueprint); - - let central_panel_frame = egui::Frame { - fill: ui.style().visuals.panel_fill, - inner_margin: egui::Margin::same(0.0), - ..Default::default() - }; - - egui::CentralPanel::default() - .frame(central_panel_frame) - .show_inside(ui, |ui| { - blueprint.blueprint_panel_and_viewport(viewport_state, &mut ctx, ui); - }); - - { - // We move the time at the very end of the frame, - // so we have one frame to see the first data before we move the time. - let dt = ui.ctx().input(|i| i.stable_dt); - let more_data_is_coming = rx.is_connected(); - let needs_repaint = ctx.rec_cfg.time_ctrl.update( - store_db.times_per_timeline(), - dt, - more_data_is_coming, - ); - if needs_repaint == re_viewer_context::NeedsRepaint::Yes { - ui.ctx().request_repaint(); - } - } - - if WATERMARK { - re_ui.paint_watermark(); - } - } -} - // ---------------------------------------------------------------------------- const FILE_SAVER_PROMISE: &str = "file_saver"; @@ -1235,43 +1109,6 @@ fn file_saver_progress_ui(egui_ctx: &egui::Context, app: &mut App) { } } -fn recording_config_entry<'cfgs>( - configs: &'cfgs mut HashMap, - id: StoreId, - data_source: &'_ re_smart_channel::SmartChannelSource, - store_db: &'_ StoreDb, -) -> &'cfgs mut RecordingConfig { - configs - .entry(id) - .or_insert_with(|| new_recording_confg(data_source, store_db)) -} - -fn new_recording_confg( - data_source: &'_ re_smart_channel::SmartChannelSource, - store_db: &'_ StoreDb, -) -> RecordingConfig { - let play_state = match data_source { - // Play files from the start by default - it feels nice and alive./ - // RrdHttpStream downloads the whole file before decoding it, so we treat it the same as a file. - re_smart_channel::SmartChannelSource::Files { .. } - | re_smart_channel::SmartChannelSource::RrdHttpStream { .. } - | re_smart_channel::SmartChannelSource::RrdWebEventListener => PlayState::Playing, - - // Live data - follow it! - re_smart_channel::SmartChannelSource::Sdk - | re_smart_channel::SmartChannelSource::WsClient { .. } - | re_smart_channel::SmartChannelSource::TcpServer { .. } => PlayState::Following, - }; - - let mut rec_cfg = RecordingConfig::default(); - - rec_cfg - .time_ctrl - .set_play_state(store_db.times_per_timeline(), play_state); - - rec_cfg -} - #[cfg(not(target_arch = "wasm32"))] fn open(app: &mut App) { if let Some(path) = rfd::FileDialog::new() diff --git a/crates/re_viewer/src/app_state.rs b/crates/re_viewer/src/app_state.rs new file mode 100644 index 000000000000..e6e973348ad2 --- /dev/null +++ b/crates/re_viewer/src/app_state.rs @@ -0,0 +1,181 @@ +use ahash::HashMap; + +use re_data_store::StoreDb; +use re_log_types::{ApplicationId, LogMsg, StoreId}; +use re_smart_channel::Receiver; +use re_viewer_context::{ + AppOptions, Caches, ComponentUiRegistry, PlayState, RecordingConfig, SpaceViewClassRegistry, + ViewerContext, +}; +use re_viewport::ViewportState; + +use crate::ui::Blueprint; + +const WATERMARK: bool = false; // Nice for recording media material + +#[derive(Default, serde::Deserialize, serde::Serialize)] +#[serde(default)] +pub struct AppState { + /// Global options for the whole viewer. + app_options: AppOptions, + + /// Things that need caching. + #[serde(skip)] + pub(crate) cache: Caches, + + #[serde(skip)] + pub(crate) selected_rec_id: Option, + #[serde(skip)] + pub(crate) selected_blueprint_by_app: HashMap, + + /// Configuration for the current recording (found in [`StoreDb`]). + pub(crate) recording_configs: HashMap, + + selection_panel: crate::selection_panel::SelectionPanel, + time_panel: re_time_panel::TimePanel, + + #[cfg(not(target_arch = "wasm32"))] + #[serde(skip)] + pub(crate) profiler: crate::Profiler, // TODO(emilk): move to `App`? + + // TODO(jleibs): This is sort of a weird place to put this but makes more + // sense than the blueprint + #[serde(skip)] + viewport_state: ViewportState, +} + +impl AppState { + pub fn app_options(&self) -> &AppOptions { + &self.app_options + } + + pub fn app_options_mut(&mut self) -> &mut AppOptions { + &mut self.app_options + } + + #[allow(clippy::too_many_arguments)] + pub fn show( + &mut self, + blueprint: &mut Blueprint, + ui: &mut egui::Ui, + render_ctx: &mut re_renderer::RenderContext, + store_db: &StoreDb, + re_ui: &re_ui::ReUi, + component_ui_registry: &ComponentUiRegistry, + space_view_class_registry: &SpaceViewClassRegistry, + rx: &Receiver, + ) { + re_tracing::profile_function!(); + + let Self { + app_options, + cache, + selected_rec_id: _, + selected_blueprint_by_app: _, + recording_configs, + selection_panel, + time_panel, + #[cfg(not(target_arch = "wasm32"))] + profiler: _, + viewport_state, + } = self; + + let rec_cfg = recording_config_entry( + recording_configs, + store_db.store_id().clone(), + rx.source(), + store_db, + ); + + let mut ctx = ViewerContext { + app_options, + cache, + space_view_class_registry, + component_ui_registry, + store_db, + rec_cfg, + re_ui, + render_ctx, + }; + + time_panel.show_panel(&mut ctx, ui, blueprint.time_panel_expanded); + selection_panel.show_panel(viewport_state, &mut ctx, ui, blueprint); + + let central_panel_frame = egui::Frame { + fill: ui.style().visuals.panel_fill, + inner_margin: egui::Margin::same(0.0), + ..Default::default() + }; + + egui::CentralPanel::default() + .frame(central_panel_frame) + .show_inside(ui, |ui| { + blueprint.blueprint_panel_and_viewport(viewport_state, &mut ctx, ui); + }); + + { + // We move the time at the very end of the frame, + // so we have one frame to see the first data before we move the time. + let dt = ui.ctx().input(|i| i.stable_dt); + let more_data_is_coming = rx.is_connected(); + let needs_repaint = ctx.rec_cfg.time_ctrl.update( + store_db.times_per_timeline(), + dt, + more_data_is_coming, + ); + if needs_repaint == re_viewer_context::NeedsRepaint::Yes { + ui.ctx().request_repaint(); + } + } + + if WATERMARK { + re_ui.paint_watermark(); + } + } + + pub fn recording_config_entry( + &mut self, + id: StoreId, + data_source: &'_ re_smart_channel::SmartChannelSource, + store_db: &'_ StoreDb, + ) -> &mut RecordingConfig { + recording_config_entry(&mut self.recording_configs, id, data_source, store_db) + } +} + +fn recording_config_entry<'cfgs>( + configs: &'cfgs mut HashMap, + id: StoreId, + data_source: &'_ re_smart_channel::SmartChannelSource, + store_db: &'_ StoreDb, +) -> &'cfgs mut RecordingConfig { + fn new_recording_confg( + data_source: &'_ re_smart_channel::SmartChannelSource, + store_db: &'_ StoreDb, + ) -> RecordingConfig { + let play_state = match data_source { + // Play files from the start by default - it feels nice and alive./ + // RrdHttpStream downloads the whole file before decoding it, so we treat it the same as a file. + re_smart_channel::SmartChannelSource::Files { .. } + | re_smart_channel::SmartChannelSource::RrdHttpStream { .. } + | re_smart_channel::SmartChannelSource::RrdWebEventListener => PlayState::Playing, + + // Live data - follow it! + re_smart_channel::SmartChannelSource::Sdk + | re_smart_channel::SmartChannelSource::WsClient { .. } + | re_smart_channel::SmartChannelSource::TcpServer { .. } => PlayState::Following, + }; + + let mut rec_cfg = RecordingConfig::default(); + + rec_cfg + .time_ctrl + .set_play_state(store_db.times_per_timeline(), play_state); + + rec_cfg + } + + configs + .entry(id) + .or_insert_with(|| new_recording_confg(data_source, store_db)) +} diff --git a/crates/re_viewer/src/lib.rs b/crates/re_viewer/src/lib.rs index 8b3d06122b61..84fc0fda5997 100644 --- a/crates/re_viewer/src/lib.rs +++ b/crates/re_viewer/src/lib.rs @@ -4,6 +4,7 @@ //! including all 2D and 3D visualization code. mod app; +mod app_state; pub mod blueprint_components; pub mod env_vars; mod loading; @@ -15,8 +16,10 @@ mod store_hub; mod ui; mod viewer_analytics; -use re_log_types::PythonVersion; -pub(crate) use ui::{memory_panel, selection_panel}; +pub(crate) use { + app_state::AppState, + ui::{memory_panel, selection_panel}, +}; pub use app::{App, StartupOptions}; pub use remote_viewer_app::RemoteViewerApp; @@ -62,7 +65,7 @@ pub fn build_info() -> re_build_info::BuildInfo { #[derive(Clone, Debug, PartialEq, Eq)] pub enum AppEnvironment { /// Created from the Rerun Python SDK. - PythonSdk(PythonVersion), + PythonSdk(re_log_types::PythonVersion), /// Created from the Rerun Rust SDK. RustSdk { From a7c7f28c0bc5d2f69e8fcf817b06aff615428187 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 5 Jun 2023 13:13:22 +0200 Subject: [PATCH 13/26] Create helper BackgroundTasks --- crates/re_viewer/src/app.rs | 82 ++++-------------------- crates/re_viewer/src/background_tasks.rs | 79 +++++++++++++++++++++++ crates/re_viewer/src/lib.rs | 1 + crates/re_viewer/src/ui/rerun_menu.rs | 2 +- 4 files changed, 93 insertions(+), 71 deletions(-) create mode 100644 crates/re_viewer/src/background_tasks.rs diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index f9cdf1ebafe7..9aa01485f8f9 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -1,8 +1,4 @@ -use std::{any::Any, hash::Hash}; - -use ahash::HashMap; use itertools::Itertools as _; -use poll_promise::Promise; use web_time::Instant; use re_arrow_store::{DataStoreConfig, DataStoreStats}; @@ -16,7 +12,10 @@ use re_viewer_context::{ SpaceViewClassRegistryError, }; -use crate::{ui::Blueprint, viewer_analytics::ViewerAnalytics, AppState, StoreHub}; +use crate::{ + background_tasks::BackgroundTasks, ui::Blueprint, viewer_analytics::ViewerAnalytics, AppState, + StoreHub, +}; use re_log_types::TimeRangeF; @@ -93,8 +92,8 @@ pub struct App { /// What is serialized pub(crate) state: AppState, - /// Pending background tasks, using `poll_promise`. - pending_promises: HashMap>>, + /// Pending background tasks, e.g. files being saved. + pub(crate) background_tasks: BackgroundTasks, /// Toast notifications. toasts: toasts::Toasts, @@ -185,7 +184,7 @@ impl App { rx, store_hub: Default::default(), state, - pending_promises: Default::default(), + background_tasks: Default::default(), toasts: toasts::Toasts::new(), memory_panel: Default::default(), memory_panel_open: false, @@ -240,58 +239,6 @@ impl App { self.space_view_class_registry.add(space_view_class) } - /// Creates a promise with the specified name that will run `f` on a background - /// thread using the `poll_promise` crate. - /// - /// Names can only be re-used once the promise with that name has finished running, - /// otherwise an other is returned. - // TODO(cmc): offer `spawn_async_promise` once we open save_file to the web - #[cfg(not(target_arch = "wasm32"))] - pub fn spawn_threaded_promise( - &mut self, - name: impl Into, - f: F, - ) -> anyhow::Result<()> - where - F: FnOnce() -> T + Send + 'static, - T: Send + 'static, - { - let name = name.into(); - - if self.pending_promises.contains_key(&name) { - anyhow::bail!("there's already a promise {name:?} running!"); - } - - let f = move || Box::new(f()) as Box; // erase it - let promise = Promise::spawn_thread(&name, f); - - self.pending_promises.insert(name, promise); - - Ok(()) - } - - /// Polls the promise with the given name. - /// - /// Returns `Some` it it's ready, or `None` otherwise. - /// - /// Panics if `T` does not match the actual return value of the promise. - pub fn poll_promise(&mut self, name: impl AsRef) -> Option { - self.pending_promises - .remove(name.as_ref()) - .and_then(|promise| match promise.try_take() { - Ok(any) => Some(*any.downcast::().unwrap()), - Err(promise) => { - self.pending_promises - .insert(name.as_ref().to_owned(), promise); - None - } - }) - } - - pub fn is_file_save_in_progress(&self) -> bool { - self.pending_promises.contains_key(FILE_SAVER_PROMISE) - } - fn check_keyboard_shortcuts(&mut self, egui_ctx: &egui::Context) { if let Some(cmd) = Command::listen_for_kb_shortcut(egui_ctx) { self.pending_commands.push(cmd); @@ -643,7 +590,7 @@ impl eframe::App for App { self.cleanup(); - file_saver_progress_ui(egui_ctx, self); // toasts for background file saver + file_saver_progress_ui(egui_ctx, &mut self.background_tasks); // toasts for background file saver let mut main_panel_frame = egui::Frame::default(); if re_ui::CUSTOM_WINDOW_DECORATIONS { @@ -1071,17 +1018,12 @@ fn preview_files_being_dropped(egui_ctx: &egui::Context) { // ---------------------------------------------------------------------------- -const FILE_SAVER_PROMISE: &str = "file_saver"; - -fn file_saver_progress_ui(egui_ctx: &egui::Context, app: &mut App) { - use std::path::PathBuf; - - if app.is_file_save_in_progress() { +fn file_saver_progress_ui(egui_ctx: &egui::Context, background_tasks: &mut BackgroundTasks) { + if background_tasks.is_file_save_in_progress() { // There's already a file save running in the background. - if let Some(res) = app.poll_promise::>(FILE_SAVER_PROMISE) { + if let Some(res) = background_tasks.poll_file_saver_promise() { // File save promise has returned. - match res { Ok(path) => { re_log::info!("File saved to {path:?}."); // this will also show a notification the user @@ -1147,7 +1089,7 @@ fn save(app: &mut App, loop_selection: Option<(re_data_store::Timeline, TimeRang return; } }; - if let Err(err) = app.spawn_threaded_promise(FILE_SAVER_PROMISE, f) { + if let Err(err) = app.background_tasks.spawn_file_saver(f) { // NOTE: Can only happen if saving through the command palette. re_log::error!("File saving failed: {err}"); } diff --git a/crates/re_viewer/src/background_tasks.rs b/crates/re_viewer/src/background_tasks.rs new file mode 100644 index 000000000000..bee0fcf524cf --- /dev/null +++ b/crates/re_viewer/src/background_tasks.rs @@ -0,0 +1,79 @@ +use std::{any::Any, path::PathBuf}; + +use ahash::HashMap; +use poll_promise::Promise; + +const FILE_SAVER_PROMISE: &str = "file_saver"; + +/// Pending background tasks, e.g. files being saved. +#[derive(Default)] +pub struct BackgroundTasks { + /// Pending background tasks, using `poll_promise`. + promises: HashMap>>, +} + +impl BackgroundTasks { + /// Creates a promise with the specified name that will run `f` on a background + /// thread using the `poll_promise` crate. + /// + /// Names can only be re-used once the promise with that name has finished running, + /// otherwise an other is returned. + // TODO(cmc): offer `spawn_async_promise` once we open save_file to the web + #[cfg(not(target_arch = "wasm32"))] + pub fn spawn_threaded_promise( + &mut self, + name: impl Into, + f: F, + ) -> anyhow::Result<()> + where + F: FnOnce() -> T + Send + 'static, + T: Send + 'static, + { + let name = name.into(); + + if self.promises.contains_key(&name) { + anyhow::bail!("there's already a promise {name:?} running!"); + } + + let f = move || Box::new(f()) as Box; // erase it + let promise = Promise::spawn_thread(&name, f); + + self.promises.insert(name, promise); + + Ok(()) + } + + #[cfg(not(target_arch = "wasm32"))] + pub fn spawn_file_saver(&mut self, f: F) -> anyhow::Result<()> + where + F: FnOnce() -> T + Send + 'static, + T: Send + 'static, + { + self.spawn_threaded_promise(FILE_SAVER_PROMISE, f) + } + + /// Polls the promise with the given name. + /// + /// Returns `Some` it it's ready, or `None` otherwise. + /// + /// Panics if `T` does not match the actual return value of the promise. + pub fn poll_promise(&mut self, name: impl AsRef) -> Option { + self.promises + .remove(name.as_ref()) + .and_then(|promise| match promise.try_take() { + Ok(any) => Some(*any.downcast::().unwrap()), + Err(promise) => { + self.promises.insert(name.as_ref().to_owned(), promise); + None + } + }) + } + + pub fn poll_file_saver_promise(&mut self) -> Option> { + self.poll_promise(FILE_SAVER_PROMISE) + } + + pub fn is_file_save_in_progress(&self) -> bool { + self.promises.contains_key(FILE_SAVER_PROMISE) + } +} diff --git a/crates/re_viewer/src/lib.rs b/crates/re_viewer/src/lib.rs index 84fc0fda5997..662b9b0c24b2 100644 --- a/crates/re_viewer/src/lib.rs +++ b/crates/re_viewer/src/lib.rs @@ -5,6 +5,7 @@ mod app; mod app_state; +mod background_tasks; pub mod blueprint_components; pub mod env_vars; mod loading; diff --git a/crates/re_viewer/src/ui/rerun_menu.rs b/crates/re_viewer/src/ui/rerun_menu.rs index 32467ceb79a2..281ce97830f0 100644 --- a/crates/re_viewer/src/ui/rerun_menu.rs +++ b/crates/re_viewer/src/ui/rerun_menu.rs @@ -251,7 +251,7 @@ fn options_menu_ui(ui: &mut egui::Ui, _frame: &mut eframe::Frame, options: &mut // TODO(emilk): support saving data on web #[cfg(not(target_arch = "wasm32"))] fn save_buttons_ui(ui: &mut egui::Ui, app: &mut App) { - let file_save_in_progress = app.is_file_save_in_progress(); + let file_save_in_progress = app.background_tasks.is_file_save_in_progress(); let save_button = Command::Save.menu_button(ui.ctx()); let save_selection_button = Command::SaveSelection.menu_button(ui.ctx()); From 328a62953b0410af596de29dcd190b717c55013b Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 5 Jun 2023 13:18:11 +0200 Subject: [PATCH 14/26] Move Profiler from AppState to App --- crates/re_viewer/src/app.rs | 9 +++++++-- crates/re_viewer/src/app_state.rs | 6 ------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index 9aa01485f8f9..b4ab6bcce9a4 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -79,6 +79,9 @@ pub struct App { re_ui: re_ui::ReUi, screenshotter: crate::screenshotter::Screenshotter, + #[cfg(not(target_arch = "wasm32"))] + profiler: crate::Profiler, + /// Listens to the local text log stream text_log_rx: std::sync::mpsc::Receiver, @@ -179,6 +182,8 @@ impl App { re_ui, screenshotter, + profiler: Default::default(), + text_log_rx, component_ui_registry: re_data_ui::create_component_ui_registry(), rx, @@ -204,7 +209,7 @@ impl App { #[cfg(not(target_arch = "wasm32"))] pub fn set_profiler(&mut self, profiler: crate::Profiler) { - self.state.profiler = profiler; + self.profiler = profiler; } pub fn build_info(&self) -> &re_build_info::BuildInfo { @@ -306,7 +311,7 @@ impl App { #[cfg(not(target_arch = "wasm32"))] Command::OpenProfiler => { - self.state.profiler.start(); + self.profiler.start(); } Command::ToggleMemoryPanel => { diff --git a/crates/re_viewer/src/app_state.rs b/crates/re_viewer/src/app_state.rs index e6e973348ad2..1278cf77bf29 100644 --- a/crates/re_viewer/src/app_state.rs +++ b/crates/re_viewer/src/app_state.rs @@ -34,10 +34,6 @@ pub struct AppState { selection_panel: crate::selection_panel::SelectionPanel, time_panel: re_time_panel::TimePanel, - #[cfg(not(target_arch = "wasm32"))] - #[serde(skip)] - pub(crate) profiler: crate::Profiler, // TODO(emilk): move to `App`? - // TODO(jleibs): This is sort of a weird place to put this but makes more // sense than the blueprint #[serde(skip)] @@ -75,8 +71,6 @@ impl AppState { recording_configs, selection_panel, time_panel, - #[cfg(not(target_arch = "wasm32"))] - profiler: _, viewport_state, } = self; From ae20339f5a20d884048c6179452b4fb0b76bd4b2 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 5 Jun 2023 13:22:25 +0200 Subject: [PATCH 15/26] Small refactor of `open` function --- crates/re_viewer/src/app.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index b4ab6bcce9a4..af99f7d3dda7 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -298,7 +298,9 @@ impl App { } #[cfg(not(target_arch = "wasm32"))] Command::Open => { - open(self); + if let Some(rrd) = open_rrd_dialog() { + self.on_rrd_loaded(rrd); + } } #[cfg(not(target_arch = "wasm32"))] Command::Quit => { @@ -1057,14 +1059,14 @@ fn file_saver_progress_ui(egui_ctx: &egui::Context, background_tasks: &mut Backg } #[cfg(not(target_arch = "wasm32"))] -fn open(app: &mut App) { +fn open_rrd_dialog() -> Option { if let Some(path) = rfd::FileDialog::new() .add_filter("rerun data file", &["rrd"]) .pick_file() { - if let Some(store_db) = crate::loading::load_file_path(&path) { - app.on_rrd_loaded(store_db); - } + crate::loading::load_file_path(&path) + } else { + None } } From a675f7f0b175111f47140ddc1ead3afce87ef9ee Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 5 Jun 2023 13:51:51 +0200 Subject: [PATCH 16/26] Fix web build --- crates/re_viewer/src/app.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index af99f7d3dda7..afca26f4918a 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -182,6 +182,7 @@ impl App { re_ui, screenshotter, + #[cfg(not(target_arch = "wasm32"))] profiler: Default::default(), text_log_rx, From 8f9081c019dce8283baebf11835c0ba733b80d2a Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 5 Jun 2023 15:05:22 +0200 Subject: [PATCH 17/26] Break out some code from App::update --- crates/re_viewer/src/app.rs | 202 ++++++++++++++++++++---------------- 1 file changed, 114 insertions(+), 88 deletions(-) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index afca26f4918a..a7d9cc973075 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -482,6 +482,99 @@ impl App { let blueprint_db = self.store_hub.blueprint_entry(this_frame_blueprint_id); Blueprint::from_db(egui_ctx, blueprint_db) } + + /// Top-level ui function. + /// + /// Shows the viewer ui. + #[allow(clippy::too_many_arguments)] + fn ui( + &mut self, + egui_ctx: &egui::Context, + frame: &mut eframe::Frame, + blueprint: &mut Blueprint, + gpu_resource_stats: &WgpuResourcePoolStatistics, + blueprint_config: &DataStoreConfig, + store_stats: &DataStoreStats, + blueprint_stats: &DataStoreStats, + ) { + let store_config = self + .store_db() + .map(|store_db| store_db.entity_db.data_store.config().clone()) + .unwrap_or_default(); + + let mut main_panel_frame = egui::Frame::default(); + if re_ui::CUSTOM_WINDOW_DECORATIONS { + // Add some margin so that we can later paint an outline around it all. + 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(&self.re_ui, ui); + + crate::ui::top_panel(&*blueprint, ui, frame, self, gpu_resource_stats); + + self.memory_panel_ui( + ui, + gpu_resource_stats, + &store_config, + blueprint_config, + store_stats, + blueprint_stats, + ); + + // NOTE: cannot call `.store_db()` due to borrowck shenanigans + if let Some(store_db) = self + .state + .selected_rec_id + .as_ref() + .and_then(|rec_id| self.store_hub.recording(rec_id)) + { + self.state + .recording_config_entry( + store_db.store_id().clone(), + self.rx.source(), + store_db, + ) + .selection_state + .on_frame_start(|item| blueprint.is_item_valid(item)); + + // TODO(andreas): store the re_renderer somewhere else. + let egui_renderer = { + let render_state = frame.wgpu_render_state().unwrap(); + &mut render_state.renderer.write() + }; + if let Some(render_ctx) = egui_renderer + .paint_callback_resources + .get_mut::() + { + render_ctx.begin_frame(); + + if store_db.is_empty() { + crate::ui::wait_screen_ui(ui, &self.rx); + } else { + self.state.show( + blueprint, + ui, + render_ctx, + store_db, + &self.re_ui, + &self.component_ui_registry, + &self.space_view_class_registry, + &self.rx, + ); + + render_ctx.before_submit(); + } + } + } else { + crate::ui::wait_screen_ui(ui, &self.rx); + } + }); + } } impl eframe::App for App { @@ -561,29 +654,18 @@ impl eframe::App for App { StoreId::from_string(StoreKind::Blueprint, self.selected_app_id().0) }); - let store_config = self - .store_db() - .map(|store_db| store_db.entity_db.data_store.config().clone()) - .unwrap_or_default(); - let store_stats = self .store_db() .map(|store_db| DataStoreStats::from_store(&store_db.entity_db.data_store)) .unwrap_or_default(); - let blueprint_config = self - .store_hub - .blueprint_mut(&active_blueprint_id) - .map(|bp_db| bp_db.entity_db.data_store.config().clone()) - .unwrap_or_default(); - let blueprint_stats = self .store_hub .blueprint_mut(&active_blueprint_id) .map(|bp_db| DataStoreStats::from_store(&bp_db.entity_db.data_store)) .unwrap_or_default(); - // do first, before doing too many allocations + // do early, before doing too many allocations self.memory_panel .update(&gpu_resource_stats, &store_stats, &blueprint_stats); @@ -600,83 +682,26 @@ impl eframe::App for App { file_saver_progress_ui(egui_ctx, &mut self.background_tasks); // toasts for background file saver - let mut main_panel_frame = egui::Frame::default(); - if re_ui::CUSTOM_WINDOW_DECORATIONS { - // Add some margin so that we can later paint an outline around it all. - main_panel_frame.inner_margin = 1.0.into(); - } - let blueprint_snapshot = self.load_or_create_blueprint(&active_blueprint_id, egui_ctx); // Make a mutable copy we can edit. let mut blueprint = blueprint_snapshot.clone(); - egui::CentralPanel::default() - .frame(main_panel_frame) - .show(egui_ctx, |ui| { - paint_background_fill(ui); - - crate::ui::mobile_warning_ui(&self.re_ui, ui); - - crate::ui::top_panel(&blueprint, ui, frame, self, &gpu_resource_stats); - - self.memory_panel_ui( - ui, - &gpu_resource_stats, - &store_config, - &blueprint_config, - &store_stats, - &blueprint_stats, - ); - - // NOTE: cannot call `.store_db()` due to borrowck shenanigans - if let Some(store_db) = self - .state - .selected_rec_id - .as_ref() - .and_then(|rec_id| self.store_hub.recording(rec_id)) - { - self.state - .recording_config_entry( - store_db.store_id().clone(), - self.rx.source(), - store_db, - ) - .selection_state - .on_frame_start(|item| blueprint.is_item_valid(item)); - - // TODO(andreas): store the re_renderer somewhere else. - let egui_renderer = { - let render_state = frame.wgpu_render_state().unwrap(); - &mut render_state.renderer.write() - }; - if let Some(render_ctx) = egui_renderer - .paint_callback_resources - .get_mut::() - { - render_ctx.begin_frame(); - - if store_db.is_empty() { - crate::ui::wait_screen_ui(ui, &self.rx); - } else { - self.state.show( - &mut blueprint, - ui, - render_ctx, - store_db, - &self.re_ui, - &self.component_ui_registry, - &self.space_view_class_registry, - &self.rx, - ); + let blueprint_config = self + .store_hub + .blueprint_mut(&active_blueprint_id) + .map(|bp_db| bp_db.entity_db.data_store.config().clone()) + .unwrap_or_default(); - render_ctx.before_submit(); - } - } - } else { - crate::ui::wait_screen_ui(ui, &self.rx); - } - }); + self.ui( + egui_ctx, + frame, + &mut blueprint, + &gpu_resource_stats, + &blueprint_config, + &store_stats, + &blueprint_stats, + ); if re_ui::CUSTOM_WINDOW_DECORATIONS { // Paint the main window frame on top of everything else @@ -695,11 +720,6 @@ impl eframe::App for App { self.run_pending_commands(&mut blueprint, egui_ctx, frame); - self.frame_time_history.add( - egui_ctx.input(|i| i.time), - frame_start.elapsed().as_secs_f32(), - ); - // If there was a real active blueprint that came from the store, save the changes back. if let Some(blueprint_db) = self.store_hub.blueprint_mut(&active_blueprint_id) { blueprint.sync_changes_to_store(&blueprint_snapshot, blueprint_db); @@ -709,6 +729,12 @@ impl eframe::App for App { // keep it around for borrow-checker reasons. re_log::warn_once!("Blueprint unexpectedly missing from store."); } + + // Frame time measurer - must be last + self.frame_time_history.add( + egui_ctx.input(|i| i.time), + frame_start.elapsed().as_secs_f32(), + ); } #[cfg(not(target_arch = "wasm32"))] From 69a65e51635273831c9c609054f54a889c8c0354 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 5 Jun 2023 15:07:21 +0200 Subject: [PATCH 18/26] Move loop_selection --- crates/re_viewer/src/app.rs | 18 +----------------- crates/re_viewer/src/app_state.rs | 17 ++++++++++++++++- crates/re_viewer/src/ui/rerun_menu.rs | 2 +- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index a7d9cc973075..c3bd3a644b8e 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -251,22 +251,6 @@ impl App { } } - /// Currently selected section of time, if any. - pub fn loop_selection(&self) -> Option<(re_data_store::Timeline, TimeRangeF)> { - self.state.selected_rec_id.as_ref().and_then(|rec_id| { - self.state - .recording_configs - .get(rec_id) - // is there an active loop selection? - .and_then(|rec_cfg| { - rec_cfg - .time_ctrl - .loop_selection() - .map(|q| (*rec_cfg.time_ctrl.timeline(), q)) - }) - }) - } - fn run_pending_commands( &mut self, blueprint: &mut Blueprint, @@ -295,7 +279,7 @@ impl App { } #[cfg(not(target_arch = "wasm32"))] Command::SaveSelection => { - save(self, self.loop_selection()); + save(self, self.state.loop_selection()); } #[cfg(not(target_arch = "wasm32"))] Command::Open => { diff --git a/crates/re_viewer/src/app_state.rs b/crates/re_viewer/src/app_state.rs index 1278cf77bf29..60f83f80cdc3 100644 --- a/crates/re_viewer/src/app_state.rs +++ b/crates/re_viewer/src/app_state.rs @@ -1,7 +1,7 @@ use ahash::HashMap; use re_data_store::StoreDb; -use re_log_types::{ApplicationId, LogMsg, StoreId}; +use re_log_types::{ApplicationId, LogMsg, StoreId, TimeRangeF}; use re_smart_channel::Receiver; use re_viewer_context::{ AppOptions, Caches, ComponentUiRegistry, PlayState, RecordingConfig, SpaceViewClassRegistry, @@ -49,6 +49,21 @@ impl AppState { &mut self.app_options } + /// Currently selected section of time, if any. + pub fn loop_selection(&self) -> Option<(re_data_store::Timeline, TimeRangeF)> { + self.selected_rec_id.as_ref().and_then(|rec_id| { + self.recording_configs + .get(rec_id) + // is there an active loop selection? + .and_then(|rec_cfg| { + rec_cfg + .time_ctrl + .loop_selection() + .map(|q| (*rec_cfg.time_ctrl.timeline(), q)) + }) + }) + } + #[allow(clippy::too_many_arguments)] pub fn show( &mut self, diff --git a/crates/re_viewer/src/ui/rerun_menu.rs b/crates/re_viewer/src/ui/rerun_menu.rs index 281ce97830f0..391178757199 100644 --- a/crates/re_viewer/src/ui/rerun_menu.rs +++ b/crates/re_viewer/src/ui/rerun_menu.rs @@ -285,7 +285,7 @@ fn save_buttons_ui(ui: &mut egui::Ui, app: &mut App) { // button, as this will determine whether its grayed out or not! // TODO(cmc): In practice the loop (green) selection is always there // at the moment so... - let loop_selection = app.loop_selection(); + let loop_selection = app.state.loop_selection(); if ui .add_enabled(loop_selection.is_some(), save_selection_button) From a62690084c67a07af95fe3cfb5445b86ac7f5fb6 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 5 Jun 2023 15:14:11 +0200 Subject: [PATCH 19/26] Move fn cleanup --- crates/re_viewer/src/app.rs | 27 ++------------------------- crates/re_viewer/src/app_state.rs | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index c3bd3a644b8e..67f68ab78bdc 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -662,7 +662,8 @@ impl eframe::App for App { self.show_text_logs_as_notifications(); self.receive_messages(egui_ctx); - self.cleanup(); + self.store_hub.purge_empty(); + self.state.cleanup(&self.store_hub); file_saver_progress_ui(egui_ctx, &mut self.background_tasks); // toasts for background file saver @@ -849,30 +850,6 @@ impl App { } } - fn cleanup(&mut self) { - re_tracing::profile_function!(); - - self.store_hub.purge_empty(); - - if !self - .state - .selected_rec_id - .as_ref() - .map_or(false, |rec_id| self.store_hub.contains_recording(rec_id)) - { - // Pick any: - self.state.selected_rec_id = self - .store_hub - .recordings() - .next() - .map(|log| log.store_id().clone()); - } - - self.state - .recording_configs - .retain(|store_id, _| self.store_hub.contains_recording(store_id)); - } - fn purge_memory_if_needed(&mut self) { re_tracing::profile_function!(); diff --git a/crates/re_viewer/src/app_state.rs b/crates/re_viewer/src/app_state.rs index 60f83f80cdc3..e18cdea31569 100644 --- a/crates/re_viewer/src/app_state.rs +++ b/crates/re_viewer/src/app_state.rs @@ -150,6 +150,25 @@ impl AppState { ) -> &mut RecordingConfig { recording_config_entry(&mut self.recording_configs, id, data_source, store_db) } + + pub fn cleanup(&mut self, store_hub: &crate::StoreHub) { + re_tracing::profile_function!(); + + if !self + .selected_rec_id + .as_ref() + .map_or(false, |rec_id| store_hub.contains_recording(rec_id)) + { + // Pick any: + self.selected_rec_id = store_hub + .recordings() + .next() + .map(|log| log.store_id().clone()); + } + + self.recording_configs + .retain(|store_id, _| store_hub.contains_recording(store_id)); + } } fn recording_config_entry<'cfgs>( From d149cf22b952c881bcf88dfb0c56aa71a9f0e56e Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 5 Jun 2023 15:14:20 +0200 Subject: [PATCH 20/26] Less pub(crate) --- crates/re_viewer/src/app.rs | 14 +++++++------- crates/re_viewer/src/app_state.rs | 6 +++++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index 67f68ab78bdc..1c0bce86810a 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -345,8 +345,8 @@ impl App { let state = &mut self.state; if let Some(rec_cfg) = state .selected_rec_id - .as_ref() - .and_then(|rec_id| state.recording_configs.get_mut(rec_id)) + .clone() + .and_then(|rec_id| state.recording_config_mut(&rec_id)) { rec_cfg.selection_state.select_previous(); } @@ -355,8 +355,8 @@ impl App { let state = &mut self.state; if let Some(rec_cfg) = state .selected_rec_id - .as_ref() - .and_then(|rec_id| state.recording_configs.get_mut(rec_id)) + .clone() + .and_then(|rec_id| state.recording_config_mut(&rec_id)) { rec_cfg.selection_state.select_next(); } @@ -389,11 +389,11 @@ impl App { } fn run_time_control_command(&mut self, command: TimeControlCommand) { - let Some(rec_id) = &self.state.selected_rec_id else { return; }; - let Some(rec_cfg) = self.state.recording_configs.get_mut(rec_id) else { return; }; + let Some(rec_id) = self.state.selected_rec_id.clone() else { return; }; + let Some(rec_cfg) = self.state.recording_config_mut(&rec_id) else { return; }; let time_ctrl = &mut rec_cfg.time_ctrl; - let Some(store_db) = self.store_hub.recording(rec_id) else { return; }; + let Some(store_db) = self.store_hub.recording(&rec_id) else { return; }; let times_per_timeline = store_db.times_per_timeline(); match command { diff --git a/crates/re_viewer/src/app_state.rs b/crates/re_viewer/src/app_state.rs index e18cdea31569..d339e29cc3e0 100644 --- a/crates/re_viewer/src/app_state.rs +++ b/crates/re_viewer/src/app_state.rs @@ -29,7 +29,7 @@ pub struct AppState { pub(crate) selected_blueprint_by_app: HashMap, /// Configuration for the current recording (found in [`StoreDb`]). - pub(crate) recording_configs: HashMap, + recording_configs: HashMap, selection_panel: crate::selection_panel::SelectionPanel, time_panel: re_time_panel::TimePanel, @@ -142,6 +142,10 @@ impl AppState { } } + pub fn recording_config_mut(&mut self, rec_id: &StoreId) -> Option<&mut RecordingConfig> { + self.recording_configs.get_mut(rec_id) + } + pub fn recording_config_entry( &mut self, id: StoreId, From 1b3d239ae52539d37a0de5e43b75940f45fe5031 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 5 Jun 2023 15:26:37 +0200 Subject: [PATCH 21/26] Fix web build --- crates/re_viewer/src/app.rs | 9 +++++---- crates/re_viewer/src/app_state.rs | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index 1c0bce86810a..189a3b426663 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -17,8 +17,6 @@ use crate::{ StoreHub, }; -use re_log_types::TimeRangeF; - // ---------------------------------------------------------------------------- #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -1059,7 +1057,10 @@ fn open_rrd_dialog() -> Option { } #[cfg(not(target_arch = "wasm32"))] -fn save(app: &mut App, loop_selection: Option<(re_data_store::Timeline, TimeRangeF)>) { +fn save( + app: &mut App, + loop_selection: Option<(re_data_store::Timeline, re_log_types::TimeRangeF)>, +) { let Some(store_db) = app.store_db() else { // NOTE: Can only happen if saving through the command palette. re_log::error!("No data to save!"); @@ -1100,7 +1101,7 @@ fn save(app: &mut App, loop_selection: Option<(re_data_store::Timeline, TimeRang fn save_database_to_file( store_db: &StoreDb, path: std::path::PathBuf, - time_selection: Option<(re_data_store::Timeline, TimeRangeF)>, + time_selection: Option<(re_data_store::Timeline, re_log_types::TimeRangeF)>, ) -> anyhow::Result anyhow::Result> { use re_arrow_store::TimeRange; diff --git a/crates/re_viewer/src/app_state.rs b/crates/re_viewer/src/app_state.rs index d339e29cc3e0..31a603a67703 100644 --- a/crates/re_viewer/src/app_state.rs +++ b/crates/re_viewer/src/app_state.rs @@ -50,6 +50,7 @@ impl AppState { } /// Currently selected section of time, if any. + #[cfg_attr(target_arch = "wasm32", allow(dead_code))] pub fn loop_selection(&self) -> Option<(re_data_store::Timeline, TimeRangeF)> { self.selected_rec_id.as_ref().and_then(|rec_id| { self.recording_configs From e1e2b105de41a66c7b03d22bed0c3630eb1c00f5 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 5 Jun 2023 15:30:46 +0200 Subject: [PATCH 22/26] Less pub(crate) --- crates/re_viewer/src/app.rs | 29 ++++++++++++--------------- crates/re_viewer/src/app_state.rs | 12 ++++++++++- crates/re_viewer/src/ui/rerun_menu.rs | 4 ++-- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index 189a3b426663..f4a7e0282d1a 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -342,8 +342,7 @@ impl App { Command::SelectionPrevious => { let state = &mut self.state; if let Some(rec_cfg) = state - .selected_rec_id - .clone() + .recording_id() .and_then(|rec_id| state.recording_config_mut(&rec_id)) { rec_cfg.selection_state.select_previous(); @@ -352,8 +351,7 @@ impl App { Command::SelectionNext => { let state = &mut self.state; if let Some(rec_cfg) = state - .selected_rec_id - .clone() + .recording_id() .and_then(|rec_id| state.recording_config_mut(&rec_id)) { rec_cfg.selection_state.select_next(); @@ -387,7 +385,7 @@ impl App { } fn run_time_control_command(&mut self, command: TimeControlCommand) { - let Some(rec_id) = self.state.selected_rec_id.clone() else { return; }; + let Some(rec_id) = self.state.recording_id() else { return; }; let Some(rec_cfg) = self.state.recording_config_mut(&rec_id) else { return; }; let time_ctrl = &mut rec_cfg.time_ctrl; @@ -511,9 +509,8 @@ impl App { // NOTE: cannot call `.store_db()` due to borrowck shenanigans if let Some(store_db) = self .state - .selected_rec_id - .as_ref() - .and_then(|rec_id| self.store_hub.recording(rec_id)) + .recording_id() + .and_then(|rec_id| self.store_hub.recording(&rec_id)) { self.state .recording_config_entry( @@ -809,7 +806,7 @@ impl App { match msg.info.store_id.kind { StoreKind::Recording => { re_log::debug!("Opening a new recording: {:?}", msg.info); - self.state.selected_rec_id = Some(store_id.clone()); + self.state.set_recording_id(store_id.clone()); } StoreKind::Blueprint => { @@ -909,10 +906,11 @@ impl App { /// Reset the viewer to how it looked the first time you ran it. fn reset(&mut self, egui_ctx: &egui::Context) { - let selected_rec_id = self.state.selected_rec_id.clone(); - + let selected_rec_id = self.state.recording_id(); self.state = Default::default(); - self.state.selected_rec_id = selected_rec_id; + if let Some(selected_rec_id) = selected_rec_id { + self.state.set_recording_id(selected_rec_id); + } // Keep the style: let style = egui_ctx.style(); @@ -923,14 +921,13 @@ impl App { /// Get access to the currently shown [`StoreDb`], if any. pub fn store_db(&self) -> Option<&StoreDb> { self.state - .selected_rec_id - .as_ref() - .and_then(|rec_id| self.store_hub.recording(rec_id)) + .recording_id() + .and_then(|rec_id| self.store_hub.recording(&rec_id)) } fn on_rrd_loaded(&mut self, store_hub: StoreHub) { if let Some(store_db) = store_hub.recordings().next() { - self.state.selected_rec_id = Some(store_db.store_id().clone()); + self.state.set_recording_id(store_db.store_id().clone()); self.analytics.on_open_recording(store_db); } diff --git a/crates/re_viewer/src/app_state.rs b/crates/re_viewer/src/app_state.rs index 31a603a67703..e6c1a2d80ed3 100644 --- a/crates/re_viewer/src/app_state.rs +++ b/crates/re_viewer/src/app_state.rs @@ -24,7 +24,7 @@ pub struct AppState { pub(crate) cache: Caches, #[serde(skip)] - pub(crate) selected_rec_id: Option, + selected_rec_id: Option, #[serde(skip)] pub(crate) selected_blueprint_by_app: HashMap, @@ -41,6 +41,16 @@ pub struct AppState { } impl AppState { + /// The selected/visible recording id, if any. + pub fn recording_id(&self) -> Option { + self.selected_rec_id.clone() + } + + /// The selected/visible recording id, if any. + pub fn set_recording_id(&mut self, recording_id: StoreId) { + self.selected_rec_id = Some(recording_id); + } + pub fn app_options(&self) -> &AppOptions { &self.app_options } diff --git a/crates/re_viewer/src/ui/rerun_menu.rs b/crates/re_viewer/src/ui/rerun_menu.rs index 391178757199..80d52afc649e 100644 --- a/crates/re_viewer/src/ui/rerun_menu.rs +++ b/crates/re_viewer/src/ui/rerun_menu.rs @@ -156,12 +156,12 @@ fn recordings_menu(ui: &mut egui::Ui, app: &mut App) { }; if ui .radio( - app.state.selected_rec_id.as_ref() == Some(store_db.store_id()), + app.state.recording_id().as_ref() == Some(store_db.store_id()), info, ) .clicked() { - app.state.selected_rec_id = Some(store_db.store_id().clone()); + app.state.set_recording_id(store_db.store_id().clone()); } } } From de8cafb86904a0e4bf742efe6f9de4a5c3bab1f0 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 5 Jun 2023 15:32:12 +0200 Subject: [PATCH 23/26] Better naming --- crates/re_viewer/src/app.rs | 12 ++++++------ crates/re_viewer/src/ui/rerun_menu.rs | 2 +- examples/rust/extend_viewer_ui/src/main.rs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index f4a7e0282d1a..1666ea7b61e7 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -413,7 +413,7 @@ impl App { /// The app ID of active blueprint. pub fn selected_app_id(&self) -> ApplicationId { - self.store_db() + self.recording_db() .and_then(|store_db| { store_db .store_info() @@ -478,7 +478,7 @@ impl App { blueprint_stats: &DataStoreStats, ) { let store_config = self - .store_db() + .recording_db() .map(|store_db| store_db.entity_db.data_store.config().clone()) .unwrap_or_default(); @@ -634,7 +634,7 @@ impl eframe::App for App { }); let store_stats = self - .store_db() + .recording_db() .map(|store_db| DataStoreStats::from_store(&store_db.entity_db.data_store)) .unwrap_or_default(); @@ -918,8 +918,8 @@ impl App { egui_ctx.set_style((*style).clone()); } - /// Get access to the currently shown [`StoreDb`], if any. - pub fn store_db(&self) -> Option<&StoreDb> { + /// Get access to the [`StoreDb`] of the currently shown recording, if any. + pub fn recording_db(&self) -> Option<&StoreDb> { self.state .recording_id() .and_then(|rec_id| self.store_hub.recording(&rec_id)) @@ -1058,7 +1058,7 @@ fn save( app: &mut App, loop_selection: Option<(re_data_store::Timeline, re_log_types::TimeRangeF)>, ) { - let Some(store_db) = app.store_db() else { + let Some(store_db) = app.recording_db() else { // NOTE: Can only happen if saving through the command palette. re_log::error!("No data to save!"); return; diff --git a/crates/re_viewer/src/ui/rerun_menu.rs b/crates/re_viewer/src/ui/rerun_menu.rs index 80d52afc649e..8dbbd55f7be8 100644 --- a/crates/re_viewer/src/ui/rerun_menu.rs +++ b/crates/re_viewer/src/ui/rerun_menu.rs @@ -269,7 +269,7 @@ fn save_buttons_ui(ui: &mut egui::Ui, app: &mut App) { }); } else { let store_db_is_nonempty = app - .store_db() + .recording_db() .map_or(false, |store_db| !store_db.is_empty()); ui.add_enabled_ui(store_db_is_nonempty, |ui| { if ui diff --git a/examples/rust/extend_viewer_ui/src/main.rs b/examples/rust/extend_viewer_ui/src/main.rs index b6a32180d655..52bb2973c29e 100644 --- a/examples/rust/extend_viewer_ui/src/main.rs +++ b/examples/rust/extend_viewer_ui/src/main.rs @@ -102,7 +102,7 @@ impl MyApp { }); ui.separator(); - if let Some(store_db) = self.rerun_app.store_db() { + if let Some(store_db) = self.rerun_app.recording_db() { store_db_ui(ui, store_db); } else { ui.label("No log database loaded yet."); From 310c21409ee55afa90d5916280ed099b5488104c Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 5 Jun 2023 15:41:30 +0200 Subject: [PATCH 24/26] Small cleanup (use less App in interfaces) --- crates/re_viewer/src/app.rs | 4 ++++ crates/re_viewer/src/ui/rerun_menu.rs | 34 ++++++++++++++------------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index 1666ea7b61e7..0a98ec9608e8 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -235,6 +235,10 @@ impl App { &self.rx } + pub fn store_hub(&self) -> &crate::StoreHub { + &self.store_hub + } + /// Adds a new space view class to the viewer. pub fn add_space_view_class( &mut self, diff --git a/crates/re_viewer/src/ui/rerun_menu.rs b/crates/re_viewer/src/ui/rerun_menu.rs index 8dbbd55f7be8..16ca0b50a439 100644 --- a/crates/re_viewer/src/ui/rerun_menu.rs +++ b/crates/re_viewer/src/ui/rerun_menu.rs @@ -3,11 +3,11 @@ use egui::NumExt as _; use itertools::Itertools as _; -use re_log_types::StoreKind; +use re_log_types::{ApplicationId, StoreKind}; use re_ui::Command; use re_viewer_context::AppOptions; -use crate::App; +use crate::{app_state::AppState, App, StoreHub}; pub fn rerun_menu_button_ui(ui: &mut egui::Ui, frame: &mut eframe::Frame, app: &mut App) { // let desired_icon_height = ui.max_rect().height() - 2.0 * ui.spacing_mut().button_padding.y; @@ -65,11 +65,11 @@ pub fn rerun_menu_button_ui(ui: &mut egui::Ui, frame: &mut eframe::Frame, app: & ui.add_space(spacing); ui.menu_button("Recordings", |ui| { - recordings_menu(ui, app); + recordings_menu(ui, &app.store_hub, &mut app.state); }); ui.menu_button("Blueprints", |ui| { - blueprints_menu(ui, app); + blueprints_menu(ui, &app.selected_app_id(), &app.store_hub, &mut app.state); }); ui.menu_button("Options", |ui| { @@ -131,9 +131,8 @@ fn about_rerun_ui(ui: &mut egui::Ui, build_info: &re_build_info::BuildInfo) { ui.hyperlink_to("www.rerun.io", "https://www.rerun.io/"); } -fn recordings_menu(ui: &mut egui::Ui, app: &mut App) { - let store_dbs = app - .store_hub +fn recordings_menu(ui: &mut egui::Ui, store_hub: &StoreHub, app_state: &mut AppState) { + let store_dbs = store_hub .recordings() .sorted_by_key(|store_db| store_db.store_info().map(|ri| ri.started)) .collect_vec(); @@ -156,25 +155,28 @@ fn recordings_menu(ui: &mut egui::Ui, app: &mut App) { }; if ui .radio( - app.state.recording_id().as_ref() == Some(store_db.store_id()), + app_state.recording_id().as_ref() == Some(store_db.store_id()), info, ) .clicked() { - app.state.set_recording_id(store_db.store_id().clone()); + app_state.set_recording_id(store_db.store_id().clone()); } } } -fn blueprints_menu(ui: &mut egui::Ui, app: &mut App) { - let app_id = app.selected_app_id(); - let blueprint_dbs = app - .store_hub +fn blueprints_menu( + ui: &mut egui::Ui, + app_id: &ApplicationId, + store_hub: &StoreHub, + app_state: &mut AppState, +) { + let blueprint_dbs = store_hub .blueprints() .sorted_by_key(|store_db| store_db.store_info().map(|ri| ri.started)) .filter(|log| { log.store_info() - .map_or(false, |ri| ri.application_id == app_id) + .map_or(false, |ri| &ri.application_id == app_id) }) .collect_vec(); @@ -203,12 +205,12 @@ fn blueprints_menu(ui: &mut egui::Ui, app: &mut App) { }; if ui .radio( - app.state.selected_blueprint_by_app.get(&app_id) == Some(store_db.store_id()), + app_state.selected_blueprint_by_app.get(app_id) == Some(store_db.store_id()), info, ) .clicked() { - app.state + app_state .selected_blueprint_by_app .insert(app_id.clone(), store_db.store_id().clone()); } From 16a9aedcb04f9dbf547bb0dfbe7cd6ee37e8cd8b Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 5 Jun 2023 17:32:49 +0200 Subject: [PATCH 25/26] Make `Caches` non-mut --- Cargo.lock | 1 + crates/re_data_ui/src/image.rs | 3 +- .../src/scene/parts/images.rs | 4 +-- crates/re_viewer_context/Cargo.toml | 1 + crates/re_viewer_context/src/caches.rs | 36 ++++++++++--------- .../re_viewer_context/src/viewer_context.rs | 2 +- crates/re_viewport/src/view_tensor/ui.rs | 4 +-- 7 files changed, 29 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a3983b9840b9..a11190250637 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4479,6 +4479,7 @@ dependencies = [ "macaw", "ndarray", "nohash-hasher", + "parking_lot 0.12.1", "re_arrow_store", "re_components", "re_data_store", diff --git a/crates/re_data_ui/src/image.rs b/crates/re_data_ui/src/image.rs index 77a53e1a760a..e093b4963e9a 100644 --- a/crates/re_data_ui/src/image.rs +++ b/crates/re_data_ui/src/image.rs @@ -26,7 +26,8 @@ impl EntityDataUi for Tensor { ) { re_tracing::profile_function!(); - match ctx.cache.entry::().entry(self.clone()) { + let decoded = ctx.cache.entry::().entry(self.clone()); + match decoded { Ok(decoded) => { let annotations = crate::annotations(ctx, query, entity_path); tensor_ui( diff --git a/crates/re_space_view_spatial/src/scene/parts/images.rs b/crates/re_space_view_spatial/src/scene/parts/images.rs index 5628099fd919..fa0f38e9fe8c 100644 --- a/crates/re_space_view_spatial/src/scene/parts/images.rs +++ b/crates/re_space_view_spatial/src/scene/parts/images.rs @@ -43,13 +43,13 @@ fn to_textured_rect( let Some([height, width, _]) = tensor.image_height_width_channels() else { return None; }; let debug_name = ent_path.to_string(); - let tensor_stats = ctx.cache.entry::().entry(tensor); + let tensor_stats = *ctx.cache.entry::().entry(tensor); match gpu_bridge::tensor_to_gpu( ctx.render_ctx, &debug_name, tensor, - tensor_stats, + &tensor_stats, annotations, ) { Ok(colormapped_texture) => { diff --git a/crates/re_viewer_context/Cargo.toml b/crates/re_viewer_context/Cargo.toml index 40076172a15b..55a9eb2fd654 100644 --- a/crates/re_viewer_context/Cargo.toml +++ b/crates/re_viewer_context/Cargo.toml @@ -39,6 +39,7 @@ lazy_static.workspace = true macaw.workspace = true ndarray.workspace = true nohash-hasher.workspace = true +parking_lot.workspace = true serde = "1" slotmap.workspace = true thiserror.workspace = true diff --git a/crates/re_viewer_context/src/caches.rs b/crates/re_viewer_context/src/caches.rs index 723316640305..4979730b3862 100644 --- a/crates/re_viewer_context/src/caches.rs +++ b/crates/re_viewer_context/src/caches.rs @@ -1,21 +1,25 @@ +use std::any::{Any, TypeId}; + use ahash::HashMap; -use std::any::Any; +use parking_lot::Mutex; /// Does memoization of different objects for the immediate mode UI. #[derive(Default)] -pub struct Caches(HashMap>); +pub struct Caches(Mutex>>); impl Caches { /// Call once per frame to potentially flush the cache(s). - pub fn begin_frame(&mut self) { - for cache in self.0.values_mut() { + pub fn begin_frame(&self) { + re_tracing::profile_function!(); + for cache in self.0.lock().values_mut() { cache.begin_frame(); } } /// Attempt to free up memory. - pub fn purge_memory(&mut self) { - for cache in self.0.values_mut() { + pub fn purge_memory(&self) { + re_tracing::profile_function!(); + for cache in self.0.lock().values_mut() { cache.purge_memory(); } } @@ -23,16 +27,16 @@ impl Caches { /// Retrieves a cache for reading and writing. /// /// Adds the cache lazily if it wasn't already there. - pub fn entry(&mut self) -> &mut T { - let cache = self - .0 - .entry(std::any::TypeId::of::()) - .or_insert(Box::::default()); - - cache - .as_any_mut() - .downcast_mut::() - .expect("Downcast failed, this indicates a bug in how `Caches` adds new cache types.") + pub fn entry(&self) -> parking_lot::MappedMutexGuard<'_, C> { + parking_lot::MutexGuard::map(self.0.lock(), |map| { + map.entry(TypeId::of::()) + .or_insert(Box::::default()) + .as_any_mut() + .downcast_mut::() + .expect( + "Downcast failed, this indicates a bug in how `Caches` adds new cache types.", + ) + }) } } diff --git a/crates/re_viewer_context/src/viewer_context.rs b/crates/re_viewer_context/src/viewer_context.rs index 7433a56a6cfb..2515285e1a0f 100644 --- a/crates/re_viewer_context/src/viewer_context.rs +++ b/crates/re_viewer_context/src/viewer_context.rs @@ -13,7 +13,7 @@ pub struct ViewerContext<'a> { /// Things that need caching and are shared across the whole viewer. /// /// Use this only for things that you expected be shared across different panels and/or space views. - pub cache: &'a mut Caches, + pub cache: &'a Caches, /// How to display components. pub component_ui_registry: &'a ComponentUiRegistry, diff --git a/crates/re_viewport/src/view_tensor/ui.rs b/crates/re_viewport/src/view_tensor/ui.rs index f9c9696922ac..e93078851d45 100644 --- a/crates/re_viewport/src/view_tensor/ui.rs +++ b/crates/re_viewport/src/view_tensor/ui.rs @@ -178,11 +178,11 @@ fn paint_tensor_slice( ) -> anyhow::Result<(egui::Response, egui::Painter, egui::Rect)> { re_tracing::profile_function!(); - let tensor_stats = ctx.cache.entry::().entry(tensor); + let tensor_stats = *ctx.cache.entry::().entry(tensor); let colormapped_texture = super::tensor_slice_to_gpu::colormapped_texture( ctx.render_ctx, tensor, - tensor_stats, + &tensor_stats, state, )?; let [width, height] = colormapped_texture.texture.width_height(); From c3bbc29fe476f32c77926fedea901620f9419135 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 5 Jun 2023 18:14:35 +0200 Subject: [PATCH 26/26] Merge `impl App {` blocks --- crates/re_viewer/src/app.rs | 416 ++++++++++++++++++------------------ 1 file changed, 207 insertions(+), 209 deletions(-) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index 0a98ec9608e8..b6271e06b8bb 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -117,15 +117,6 @@ pub struct App { space_view_class_registry: SpaceViewClassRegistry, } -/// Add built-in space views to the registry. -fn populate_space_view_class_registry_with_builtin( - space_view_class_registry: &mut SpaceViewClassRegistry, -) -> Result<(), SpaceViewClassRegistryError> { - space_view_class_registry.add(re_space_view_text::TextSpaceView::default())?; - space_view_class_registry.add(re_space_view_text_box::TextBoxSpaceView::default())?; - Ok(()) -} - impl App { /// Create a viewer that receives new log messages over time pub fn from_receiver( @@ -558,207 +549,7 @@ impl App { } }); } -} - -impl eframe::App for App { - fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] { - [0.0; 4] // transparent so we can get rounded corners when doing [`re_ui::CUSTOM_WINDOW_DECORATIONS`] - } - - fn save(&mut self, storage: &mut dyn eframe::Storage) { - if self.startup_options.persist_state { - eframe::set_value(storage, eframe::APP_KEY, &self.state); - } - } - - fn update(&mut self, egui_ctx: &egui::Context, frame: &mut eframe::Frame) { - let frame_start = Instant::now(); - - #[cfg(not(target_arch = "wasm32"))] - if let Some(resolution_in_points) = self.startup_options.resolution_in_points.take() { - frame.set_window_size(resolution_in_points.into()); - } - - #[cfg(not(target_arch = "wasm32"))] - if self.screenshotter.update(egui_ctx, frame).quit { - frame.close(); - return; - } - - if self.startup_options.memory_limit.limit.is_none() { - // we only warn about high memory usage if the user hasn't specified a limit - self.ram_limit_warner.update(); - } - - #[cfg(not(target_arch = "wasm32"))] - if self.screenshotter.is_screenshotting() { - // Make screenshots high-quality by pretending we have a high-dpi display, whether we do or not: - egui_ctx.set_pixels_per_point(2.0); - } else { - // Ensure zoom factor is sane and in 10% steps at all times before applying it. - { - let mut zoom_factor = self.app_options().zoom_factor; - zoom_factor = zoom_factor.clamp(MIN_ZOOM_FACTOR, MAX_ZOOM_FACTOR); - zoom_factor = (zoom_factor * 10.).round() / 10.; - self.app_options_mut().zoom_factor = zoom_factor; - } - - // Apply zoom factor on top of natively reported pixel per point. - let pixels_per_point = frame.info().native_pixels_per_point.unwrap_or(1.0) - * self.app_options().zoom_factor; - egui_ctx.set_pixels_per_point(pixels_per_point); - } - - // TODO(andreas): store the re_renderer somewhere else. - let gpu_resource_stats = { - let egui_renderer = { - let render_state = frame.wgpu_render_state().unwrap(); - &mut render_state.renderer.read() - }; - let render_ctx = egui_renderer - .paint_callback_resources - .get::() - .unwrap(); - - // Query statistics before begin_frame as this might be more accurate if there's resources that we recreate every frame. - render_ctx.gpu_resources.statistics() - }; - - // Look up the blueprint in use for this frame. - // Note that it's important that we save this because it's possible that it will be changed - // by the end up the frame (e.g. if the user selects a different recording), but we need it - // to save changes back to the correct blueprint at the end. - let active_blueprint_id = self - .state - .selected_blueprint_by_app - .get(&self.selected_app_id()) - .cloned() - .unwrap_or_else(|| { - StoreId::from_string(StoreKind::Blueprint, self.selected_app_id().0) - }); - - let store_stats = self - .recording_db() - .map(|store_db| DataStoreStats::from_store(&store_db.entity_db.data_store)) - .unwrap_or_default(); - - let blueprint_stats = self - .store_hub - .blueprint_mut(&active_blueprint_id) - .map(|bp_db| DataStoreStats::from_store(&bp_db.entity_db.data_store)) - .unwrap_or_default(); - - // do early, before doing too many allocations - self.memory_panel - .update(&gpu_resource_stats, &store_stats, &blueprint_stats); - - self.check_keyboard_shortcuts(egui_ctx); - - self.purge_memory_if_needed(); - - self.state.cache.begin_frame(); - - self.show_text_logs_as_notifications(); - self.receive_messages(egui_ctx); - - self.store_hub.purge_empty(); - self.state.cleanup(&self.store_hub); - - file_saver_progress_ui(egui_ctx, &mut self.background_tasks); // toasts for background file saver - - let blueprint_snapshot = self.load_or_create_blueprint(&active_blueprint_id, egui_ctx); - - // Make a mutable copy we can edit. - let mut blueprint = blueprint_snapshot.clone(); - - let blueprint_config = self - .store_hub - .blueprint_mut(&active_blueprint_id) - .map(|bp_db| bp_db.entity_db.data_store.config().clone()) - .unwrap_or_default(); - - self.ui( - egui_ctx, - frame, - &mut blueprint, - &gpu_resource_stats, - &blueprint_config, - &store_stats, - &blueprint_stats, - ); - - if re_ui::CUSTOM_WINDOW_DECORATIONS { - // Paint the main window frame on top of everything else - paint_native_window_frame(egui_ctx); - } - - self.handle_dropping_files(egui_ctx); - - if !self.screenshotter.is_screenshotting() { - self.toasts.show(egui_ctx); - } - - if let Some(cmd) = self.cmd_palette.show(egui_ctx) { - self.pending_commands.push(cmd); - } - - self.run_pending_commands(&mut blueprint, egui_ctx, frame); - - // If there was a real active blueprint that came from the store, save the changes back. - if let Some(blueprint_db) = self.store_hub.blueprint_mut(&active_blueprint_id) { - blueprint.sync_changes_to_store(&blueprint_snapshot, blueprint_db); - } else { - // This shouldn't happen because we should have used `active_blueprint_id` to - // create this same blueprint in `load_or_create_blueprint`, but we couldn't - // keep it around for borrow-checker reasons. - re_log::warn_once!("Blueprint unexpectedly missing from store."); - } - - // Frame time measurer - must be last - self.frame_time_history.add( - egui_ctx.input(|i| i.time), - frame_start.elapsed().as_secs_f32(), - ); - } - - #[cfg(not(target_arch = "wasm32"))] - fn post_rendering(&mut self, _window_size: [u32; 2], frame: &eframe::Frame) { - if let Some(screenshot) = frame.screenshot() { - self.screenshotter.save(&screenshot); - } - } -} - -fn paint_background_fill(ui: &mut egui::Ui) { - // This is required because the streams view (time panel) - // has rounded top corners, which leaves a gap. - // So we fill in that gap (and other) here. - // Of course this does some over-draw, but we have to live with that. - - ui.painter().rect_filled( - ui.max_rect().shrink(0.5), - re_ui::ReUi::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, - ); - - let stroke = egui::Stroke::new(1.0, egui::Color32::from_gray(42)); // from figma 2022-02-06 - - painter.rect_stroke( - egui_ctx.screen_rect().shrink(0.5), - re_ui::ReUi::native_window_rounding(), - stroke, - ); -} -impl App { /// Show recent text log messages to the user as toast notifications. fn show_text_logs_as_notifications(&mut self) { re_tracing::profile_function!(); @@ -977,6 +768,213 @@ impl App { } } +impl eframe::App for App { + fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] { + [0.0; 4] // transparent so we can get rounded corners when doing [`re_ui::CUSTOM_WINDOW_DECORATIONS`] + } + + fn save(&mut self, storage: &mut dyn eframe::Storage) { + if self.startup_options.persist_state { + eframe::set_value(storage, eframe::APP_KEY, &self.state); + } + } + + fn update(&mut self, egui_ctx: &egui::Context, frame: &mut eframe::Frame) { + let frame_start = Instant::now(); + + #[cfg(not(target_arch = "wasm32"))] + if let Some(resolution_in_points) = self.startup_options.resolution_in_points.take() { + frame.set_window_size(resolution_in_points.into()); + } + + #[cfg(not(target_arch = "wasm32"))] + if self.screenshotter.update(egui_ctx, frame).quit { + frame.close(); + return; + } + + if self.startup_options.memory_limit.limit.is_none() { + // we only warn about high memory usage if the user hasn't specified a limit + self.ram_limit_warner.update(); + } + + #[cfg(not(target_arch = "wasm32"))] + if self.screenshotter.is_screenshotting() { + // Make screenshots high-quality by pretending we have a high-dpi display, whether we do or not: + egui_ctx.set_pixels_per_point(2.0); + } else { + // Ensure zoom factor is sane and in 10% steps at all times before applying it. + { + let mut zoom_factor = self.app_options().zoom_factor; + zoom_factor = zoom_factor.clamp(MIN_ZOOM_FACTOR, MAX_ZOOM_FACTOR); + zoom_factor = (zoom_factor * 10.).round() / 10.; + self.app_options_mut().zoom_factor = zoom_factor; + } + + // Apply zoom factor on top of natively reported pixel per point. + let pixels_per_point = frame.info().native_pixels_per_point.unwrap_or(1.0) + * self.app_options().zoom_factor; + egui_ctx.set_pixels_per_point(pixels_per_point); + } + + // TODO(andreas): store the re_renderer somewhere else. + let gpu_resource_stats = { + let egui_renderer = { + let render_state = frame.wgpu_render_state().unwrap(); + &mut render_state.renderer.read() + }; + let render_ctx = egui_renderer + .paint_callback_resources + .get::() + .unwrap(); + + // Query statistics before begin_frame as this might be more accurate if there's resources that we recreate every frame. + render_ctx.gpu_resources.statistics() + }; + + // Look up the blueprint in use for this frame. + // Note that it's important that we save this because it's possible that it will be changed + // by the end up the frame (e.g. if the user selects a different recording), but we need it + // to save changes back to the correct blueprint at the end. + let active_blueprint_id = self + .state + .selected_blueprint_by_app + .get(&self.selected_app_id()) + .cloned() + .unwrap_or_else(|| { + StoreId::from_string(StoreKind::Blueprint, self.selected_app_id().0) + }); + + let store_stats = self + .recording_db() + .map(|store_db| DataStoreStats::from_store(&store_db.entity_db.data_store)) + .unwrap_or_default(); + + let blueprint_stats = self + .store_hub + .blueprint_mut(&active_blueprint_id) + .map(|bp_db| DataStoreStats::from_store(&bp_db.entity_db.data_store)) + .unwrap_or_default(); + + // do early, before doing too many allocations + self.memory_panel + .update(&gpu_resource_stats, &store_stats, &blueprint_stats); + + self.check_keyboard_shortcuts(egui_ctx); + + self.purge_memory_if_needed(); + + self.state.cache.begin_frame(); + + self.show_text_logs_as_notifications(); + self.receive_messages(egui_ctx); + + self.store_hub.purge_empty(); + self.state.cleanup(&self.store_hub); + + file_saver_progress_ui(egui_ctx, &mut self.background_tasks); // toasts for background file saver + + let blueprint_snapshot = self.load_or_create_blueprint(&active_blueprint_id, egui_ctx); + + // Make a mutable copy we can edit. + let mut blueprint = blueprint_snapshot.clone(); + + let blueprint_config = self + .store_hub + .blueprint_mut(&active_blueprint_id) + .map(|bp_db| bp_db.entity_db.data_store.config().clone()) + .unwrap_or_default(); + + self.ui( + egui_ctx, + frame, + &mut blueprint, + &gpu_resource_stats, + &blueprint_config, + &store_stats, + &blueprint_stats, + ); + + if re_ui::CUSTOM_WINDOW_DECORATIONS { + // Paint the main window frame on top of everything else + paint_native_window_frame(egui_ctx); + } + + self.handle_dropping_files(egui_ctx); + + if !self.screenshotter.is_screenshotting() { + self.toasts.show(egui_ctx); + } + + if let Some(cmd) = self.cmd_palette.show(egui_ctx) { + self.pending_commands.push(cmd); + } + + self.run_pending_commands(&mut blueprint, egui_ctx, frame); + + // If there was a real active blueprint that came from the store, save the changes back. + if let Some(blueprint_db) = self.store_hub.blueprint_mut(&active_blueprint_id) { + blueprint.sync_changes_to_store(&blueprint_snapshot, blueprint_db); + } else { + // This shouldn't happen because we should have used `active_blueprint_id` to + // create this same blueprint in `load_or_create_blueprint`, but we couldn't + // keep it around for borrow-checker reasons. + re_log::warn_once!("Blueprint unexpectedly missing from store."); + } + + // Frame time measurer - must be last + self.frame_time_history.add( + egui_ctx.input(|i| i.time), + frame_start.elapsed().as_secs_f32(), + ); + } + + #[cfg(not(target_arch = "wasm32"))] + fn post_rendering(&mut self, _window_size: [u32; 2], frame: &eframe::Frame) { + if let Some(screenshot) = frame.screenshot() { + self.screenshotter.save(&screenshot); + } + } +} + +/// Add built-in space views to the registry. +fn populate_space_view_class_registry_with_builtin( + space_view_class_registry: &mut SpaceViewClassRegistry, +) -> Result<(), SpaceViewClassRegistryError> { + space_view_class_registry.add(re_space_view_text::TextSpaceView::default())?; + space_view_class_registry.add(re_space_view_text_box::TextBoxSpaceView::default())?; + Ok(()) +} + +fn paint_background_fill(ui: &mut egui::Ui) { + // This is required because the streams view (time panel) + // has rounded top corners, which leaves a gap. + // So we fill in that gap (and other) here. + // Of course this does some over-draw, but we have to live with that. + + ui.painter().rect_filled( + ui.max_rect().shrink(0.5), + re_ui::ReUi::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, + ); + + let stroke = egui::Stroke::new(1.0, egui::Color32::from_gray(42)); // from figma 2022-02-06 + + painter.rect_stroke( + egui_ctx.screen_rect().shrink(0.5), + re_ui::ReUi::native_window_rounding(), + stroke, + ); +} + fn preview_files_being_dropped(egui_ctx: &egui::Context) { use egui::{Align2, Color32, Id, LayerId, Order, TextStyle};