From 0763c2196f9dd937b1e3485e751183e8b3263828 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 22 Aug 2023 15:05:29 +0200 Subject: [PATCH 1/3] Implement "Open file" dialog on Web --- Cargo.lock | 2 + crates/re_ui/src/command.rs | 3 - crates/re_viewer/Cargo.toml | 2 +- crates/re_viewer/src/app.rs | 73 +++++++++++++++++-- crates/re_viewer/src/ui/recordings_panel.rs | 6 +- crates/re_viewer/src/ui/rerun_menu.rs | 4 +- crates/re_viewer/src/ui/wait_screen_ui.rs | 7 +- .../re_viewer_context/src/command_sender.rs | 12 ++- crates/re_viewer_context/src/lib.rs | 3 +- 9 files changed, 88 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb9cebeaa44d..e83728b42532 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3566,6 +3566,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcf2a02372dfae23c9c01267fb296b8a3413bb4e45fbd589c3ac73c6dcfbb305" dependencies = [ "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", ] [[package]] diff --git a/crates/re_ui/src/command.rs b/crates/re_ui/src/command.rs index 0cdf13a88582..50d1db68747c 100644 --- a/crates/re_ui/src/command.rs +++ b/crates/re_ui/src/command.rs @@ -13,7 +13,6 @@ pub trait UICommandSender { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, strum_macros::EnumIter)] pub enum UICommand { // Listed in the order they show up in the command palette by default! - #[cfg(not(target_arch = "wasm32"))] Open, #[cfg(not(target_arch = "wasm32"))] Save, @@ -84,7 +83,6 @@ impl UICommand { "Save data for the current loop selection to a Rerun data file (.rrd)", ), - #[cfg(not(target_arch = "wasm32"))] UICommand::Open => ("Open…", "Open a Rerun Data File (.rrd)"), UICommand::CloseCurrentRecording => ( @@ -190,7 +188,6 @@ impl UICommand { UICommand::Save => Some(cmd(Key::S)), #[cfg(not(target_arch = "wasm32"))] UICommand::SaveSelection => Some(cmd_alt(Key::S)), - #[cfg(not(target_arch = "wasm32"))] UICommand::Open => Some(cmd(Key::O)), UICommand::CloseCurrentRecording => None, diff --git a/crates/re_viewer/Cargo.toml b/crates/re_viewer/Cargo.toml index e4b33a75fc76..4f930f7119d3 100644 --- a/crates/re_viewer/Cargo.toml +++ b/crates/re_viewer/Cargo.toml @@ -88,7 +88,7 @@ egui-wgpu.workspace = true image = { workspace = true, default-features = false, features = ["png"] } itertools = { workspace = true } once_cell = { workspace = true } -poll-promise = "0.2" +poll-promise = { version = "0.2", features = ["web"] } rfd.workspace = true ron = "0.8.0" serde = { version = "1", features = ["derive"] } diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index f8fd39e19465..131f163b8e34 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -7,8 +7,8 @@ use re_smart_channel::{Receiver, SmartChannelSource}; use re_ui::{toasts, UICommand, UICommandSender}; use re_viewer_context::{ command_channel, AppOptions, CommandReceiver, CommandSender, ComponentUiRegistry, - DynSpaceViewClass, PlayState, SpaceViewClassRegistry, SpaceViewClassRegistryError, - StoreContext, SystemCommand, SystemCommandSender, + DynSpaceViewClass, FileContents, PlayState, SpaceViewClassRegistry, + SpaceViewClassRegistryError, StoreContext, SystemCommand, SystemCommandSender, }; use crate::{ @@ -93,6 +93,9 @@ pub struct App { rx: Receiver, + #[cfg(target_arch = "wasm32")] + open_file_promise: Option>>, + /// What is serialized pub(crate) state: AppState, @@ -201,6 +204,8 @@ impl App { text_log_rx, component_ui_registry: re_data_ui::create_component_ui_registry(), rx, + #[cfg(target_arch = "wasm32")] + open_file_promise: Default::default(), state, background_tasks: Default::default(), store_hub: Some(StoreHub::new()), @@ -297,8 +302,9 @@ impl App { SystemCommand::CloseRecordingId(recording_id) => { store_hub.remove_recording_id(&recording_id); } + #[cfg(not(target_arch = "wasm32"))] - SystemCommand::LoadRrd(path) => { + SystemCommand::LoadRrdPath(path) => { let with_notification = true; if let Some(rrd) = crate::loading::load_file_path(&path, with_notification) { let store_id = rrd.store_dbs().next().map(|db| db.store_id().clone()); @@ -308,6 +314,18 @@ impl App { } } } + + SystemCommand::LoadRrdContents(FileContents { file_name, bytes }) => { + let bytes: &[u8] = &bytes; + if let Some(rrd) = crate::loading::load_file_contents(&file_name, bytes) { + let store_id = rrd.store_dbs().next().map(|db| db.store_id().clone()); + store_hub.add_bundle(rrd); + if let Some(store_id) = store_id { + store_hub.set_recording_id(store_id); + } + } + } + SystemCommand::ResetViewer => self.reset(store_hub, egui_ctx), SystemCommand::UpdateBlueprint(blueprint_id, updates) => { let blueprint_db = store_hub.store_db_mut(&blueprint_id); @@ -347,9 +365,14 @@ impl App { UICommand::Open => { if let Some(rrd_file) = open_rrd_dialog() { self.command_sender - .send_system(SystemCommand::LoadRrd(rrd_file)); + .send_system(SystemCommand::LoadRrdPath(rrd_file)); } } + #[cfg(target_arch = "wasm32")] + UICommand::Open => { + self.open_file_promise = + Some(poll_promise::Promise::spawn_async(open_rrd_dialog())); + } UICommand::CloseCurrentRecording => { let cur_rec = store_context .and_then(|ctx| ctx.recording) @@ -919,6 +942,17 @@ impl eframe::App for App { self.ram_limit_warner.update(); } + #[cfg(target_arch = "wasm32")] + if let Some(promise) = &self.open_file_promise { + if let Some(contents_opt) = promise.ready() { + if let Some(contents) = contents_opt { + self.command_sender + .send_system(SystemCommand::LoadRrdContents(contents.clone())); + } + self.open_file_promise = None; + } + } + #[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: @@ -1138,14 +1172,37 @@ fn file_saver_progress_ui(egui_ctx: &egui::Context, background_tasks: &mut Backg } #[cfg(not(target_arch = "wasm32"))] -use std::path::PathBuf; -#[cfg(not(target_arch = "wasm32"))] -fn open_rrd_dialog() -> Option { +fn open_rrd_dialog() -> Option { rfd::FileDialog::new() - .add_filter("rerun data file", &["rrd"]) + .add_filter("Rerun data file", &["rrd"]) .pick_file() } +#[cfg(target_arch = "wasm32")] +async fn open_rrd_dialog() -> Option { + let res = rfd::AsyncFileDialog::new() + .add_filter("Rerun data file", &["rrd"]) + .pick_file() + .await; + + match res { + Some(file) => Some({ + let file_name = file.file_name(); + re_log::debug!("Reading {file_name}…"); + let bytes = file.read().await; + re_log::debug!( + "{file_name} was {}", + re_format::format_bytes(bytes.len() as _) + ); + FileContents { + file_name, + bytes: bytes.into(), + } + }), + None => None, + } +} + #[cfg(not(target_arch = "wasm32"))] fn save( app: &mut App, diff --git a/crates/re_viewer/src/ui/recordings_panel.rs b/crates/re_viewer/src/ui/recordings_panel.rs index d787c10fd769..3ddfea69ce9e 100644 --- a/crates/re_viewer/src/ui/recordings_panel.rs +++ b/crates/re_viewer/src/ui/recordings_panel.rs @@ -13,9 +13,8 @@ pub fn recordings_panel_ui(ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui) { ui, "Recordings", Some("These are the Recordings currently loaded in the Viewer"), - |_ui| { - #[cfg(not(target_arch = "wasm32"))] - add_button_ui(ctx, _ui); + |ui| { + add_button_ui(ctx, ui); }, ); }); @@ -149,7 +148,6 @@ fn recording_ui( .show(ui) } -#[cfg(not(target_arch = "wasm32"))] fn add_button_ui(ctx: &mut ViewerContext<'_>, ui: &mut egui::Ui) { use re_ui::UICommandSender; diff --git a/crates/re_viewer/src/ui/rerun_menu.rs b/crates/re_viewer/src/ui/rerun_menu.rs index a5f69f9b2d18..0257ae3f1536 100644 --- a/crates/re_viewer/src/ui/rerun_menu.rs +++ b/crates/re_viewer/src/ui/rerun_menu.rs @@ -34,10 +34,10 @@ impl App { ui.add_space(spacing); + UICommand::Open.menu_button_ui(ui, &self.command_sender); + #[cfg(not(target_arch = "wasm32"))] { - UICommand::Open.menu_button_ui(ui, &self.command_sender); - self.save_buttons_ui(ui, _store_context); UICommand::CloseCurrentRecording.menu_button_ui(ui, &self.command_sender); diff --git a/crates/re_viewer/src/ui/wait_screen_ui.rs b/crates/re_viewer/src/ui/wait_screen_ui.rs index 71b5fc7e367e..4f768a760bb2 100644 --- a/crates/re_viewer/src/ui/wait_screen_ui.rs +++ b/crates/re_viewer/src/ui/wait_screen_ui.rs @@ -109,7 +109,7 @@ fn welcome_ui_impl( fn onboarding_content_ui( re_ui: &ReUi, ui: &mut Ui, - _command_sender: &re_viewer_context::CommandSender, + command_sender: &re_viewer_context::CommandSender, ) { let column_spacing = 15.0; let stability_adjustment = 1.0; // minimize jitter with sizing and scroll bars @@ -198,12 +198,11 @@ fn onboarding_content_ui( url_large_text_button(re_ui, ui, "Rust", RUST_QUICKSTART); }); - #[cfg(not(target_arch = "wasm32"))] { - use re_ui::UICommandSender; + use re_ui::UICommandSender as _; ui.horizontal(|ui| { if large_text_button(ui, "Open file…").clicked() { - _command_sender.send_ui(re_ui::UICommand::Open); + command_sender.send_ui(re_ui::UICommand::Open); } button_centered_label(ui, "Or drop a file anywhere!"); }); diff --git a/crates/re_viewer_context/src/command_sender.rs b/crates/re_viewer_context/src/command_sender.rs index 31bee8798e0c..74fac259efa6 100644 --- a/crates/re_viewer_context/src/command_sender.rs +++ b/crates/re_viewer_context/src/command_sender.rs @@ -3,12 +3,22 @@ use re_ui::{UICommand, UICommandSender}; // ---------------------------------------------------------------------------- +/// The contents of as file +#[derive(Clone)] +pub struct FileContents { + pub file_name: String, + pub bytes: std::sync::Arc<[u8]>, +} + /// Commands used by internal system components // TODO(jleibs): Is there a better crate for this? pub enum SystemCommand { /// Load an RRD by Filename #[cfg(not(target_arch = "wasm32"))] - LoadRrd(std::path::PathBuf), + LoadRrdPath(std::path::PathBuf), + + /// Load an RRD by content + LoadRrdContents(FileContents), /// Reset the `Viewer` to the default state ResetViewer, diff --git a/crates/re_viewer_context/src/lib.rs b/crates/re_viewer_context/src/lib.rs index 5503f5fea65d..e8867796a955 100644 --- a/crates/re_viewer_context/src/lib.rs +++ b/crates/re_viewer_context/src/lib.rs @@ -26,7 +26,8 @@ pub use annotations::{AnnotationMap, Annotations, ResolvedAnnotationInfo, MISSIN pub use app_options::AppOptions; pub use caches::{Cache, Caches}; pub use command_sender::{ - command_channel, CommandReceiver, CommandSender, SystemCommand, SystemCommandSender, + command_channel, CommandReceiver, CommandSender, FileContents, SystemCommand, + SystemCommandSender, }; pub use component_ui_registry::{ComponentUiRegistry, UiVerbosity}; pub use item::{resolve_mono_instance_path, resolve_mono_instance_path_item, Item, ItemCollection}; From 7ac33e9cd75ee1b038b2c5da1b07e6cc206e8226 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 22 Aug 2023 15:41:56 +0200 Subject: [PATCH 2/3] Wake up ui thread when file has finished loading --- crates/re_viewer/src/app.rs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index 131f163b8e34..42e4a19e5484 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -279,12 +279,13 @@ impl App { fn run_pending_ui_commands( &mut self, + frame: &mut eframe::Frame, + egui_ctx: &egui::Context, app_blueprint: &AppBlueprint<'_>, store_context: Option<&StoreContext<'_>>, - frame: &mut eframe::Frame, ) { while let Some(cmd) = self.command_receiver.recv_ui() { - self.run_ui_command(cmd, app_blueprint, store_context, frame); + self.run_ui_command(frame, egui_ctx, app_blueprint, store_context, cmd); } } @@ -343,10 +344,11 @@ impl App { fn run_ui_command( &mut self, - cmd: UICommand, + _frame: &mut eframe::Frame, + egui_ctx: &egui::Context, app_blueprint: &AppBlueprint<'_>, store_context: Option<&StoreContext<'_>>, - _frame: &mut eframe::Frame, + cmd: UICommand, ) { match cmd { #[cfg(not(target_arch = "wasm32"))] @@ -370,8 +372,12 @@ impl App { } #[cfg(target_arch = "wasm32")] UICommand::Open => { - self.open_file_promise = - Some(poll_promise::Promise::spawn_async(open_rrd_dialog())); + let egui_ctx = egui_ctx.clone(); + self.open_file_promise = Some(poll_promise::Promise::spawn_async(async move { + let file = async_open_rrd_dialog().await; + egui_ctx.request_repaint(); // Wake ui thread + file + })); } UICommand::CloseCurrentRecording => { let cur_rec = store_context @@ -1036,7 +1042,7 @@ impl eframe::App for App { self.command_sender.send_ui(cmd); } - self.run_pending_ui_commands(&app_blueprint, store_context.as_ref(), frame); + self.run_pending_ui_commands(frame, egui_ctx, &app_blueprint, store_context.as_ref()); self.run_pending_system_commands(&mut store_hub, egui_ctx); @@ -1179,7 +1185,7 @@ fn open_rrd_dialog() -> Option { } #[cfg(target_arch = "wasm32")] -async fn open_rrd_dialog() -> Option { +async fn async_open_rrd_dialog() -> Option { let res = rfd::AsyncFileDialog::new() .add_filter("Rerun data file", &["rrd"]) .pick_file() From 21a72fac2a3da221b99682c94c2b8e84cf24dc6c Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Tue, 22 Aug 2023 15:59:15 +0200 Subject: [PATCH 3/3] silence warning --- crates/re_viewer/src/app.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/re_viewer/src/app.rs b/crates/re_viewer/src/app.rs index 42e4a19e5484..180445e2a6cd 100644 --- a/crates/re_viewer/src/app.rs +++ b/crates/re_viewer/src/app.rs @@ -345,7 +345,7 @@ impl App { fn run_ui_command( &mut self, _frame: &mut eframe::Frame, - egui_ctx: &egui::Context, + _egui_ctx: &egui::Context, app_blueprint: &AppBlueprint<'_>, store_context: Option<&StoreContext<'_>>, cmd: UICommand, @@ -372,7 +372,7 @@ impl App { } #[cfg(target_arch = "wasm32")] UICommand::Open => { - let egui_ctx = egui_ctx.clone(); + let egui_ctx = _egui_ctx.clone(); self.open_file_promise = Some(poll_promise::Promise::spawn_async(async move { let file = async_open_rrd_dialog().await; egui_ctx.request_repaint(); // Wake ui thread