#![allow(clippy::mem_forget)]
use ahash::HashMap;
use serde::Deserialize;
use std::str::FromStr as _;
use wasm_bindgen::prelude::*;
use re_log::ResultExt as _;
use re_memory::AccountingAllocator;
use re_viewer_context::{SystemCommand, SystemCommandSender};
use crate::history::install_popstate_listener;
use crate::web_tools::{url_to_receiver, Callback, JsResultExt as _, StringOrStringArray};
#[global_allocator]
static GLOBAL: AccountingAllocator<std::alloc::System> =
AccountingAllocator::new(std::alloc::System);
#[wasm_bindgen]
pub struct WebHandle {
runner: eframe::WebRunner,
tx_channels: HashMap<String, re_smart_channel::Sender<re_log_types::LogMsg>>,
app_options: AppOptions,
}
#[wasm_bindgen]
impl WebHandle {
#[allow(clippy::new_without_default, clippy::use_self)] #[wasm_bindgen(constructor)]
pub fn new(app_options: JsValue) -> Result<WebHandle, JsValue> {
re_log::setup_logging();
let app_options: Option<AppOptions> = serde_wasm_bindgen::from_value(app_options)?;
Ok(Self {
runner: eframe::WebRunner::new(),
tx_channels: Default::default(),
app_options: app_options.unwrap_or_default(),
})
}
#[wasm_bindgen]
pub async fn start(&self, canvas_id: String) -> Result<(), wasm_bindgen::JsValue> {
let app_options = self.app_options.clone();
let web_options = eframe::WebOptions {
follow_system_theme: false,
default_theme: eframe::Theme::Dark,
wgpu_options: crate::wgpu_options(app_options.render_backend.clone()),
depth_buffer: 0,
};
self.runner
.start(
&canvas_id,
web_options,
Box::new(move |cc| Ok(Box::new(create_app(cc, app_options)?))),
)
.await?;
re_log::debug!("Web app started.");
Ok(())
}
#[wasm_bindgen]
pub fn toggle_panel_overrides(&self, value: Option<bool>) {
let Some(mut app) = self.runner.app_mut::<crate::App>() else {
return;
};
match value {
Some(value) => app.panel_state_overrides_active = value,
None => app.panel_state_overrides_active ^= true,
}
app.egui_ctx.request_repaint();
}
#[wasm_bindgen]
pub fn override_panel_state(&self, panel: &str, state: Option<String>) -> Result<(), JsValue> {
let Some(mut app) = self.runner.app_mut::<crate::App>() else {
return Ok(());
};
let panel = Panel::from_str(panel)
.map_err(|err| js_sys::TypeError::new(&format!("invalid panel: {err}")))?;
let state = match state {
Some(state) => Some(
PanelState::from_str(&state)
.map_err(|err| js_sys::TypeError::new(&format!("invalid state: {err}")))?
.into(),
),
None => None,
};
let overrides = &mut app.panel_state_overrides;
match panel {
Panel::Top => overrides.top = state,
Panel::Blueprint => overrides.blueprint = state,
Panel::Selection => overrides.selection = state,
Panel::Time => overrides.time = state,
}
app.egui_ctx.request_repaint();
Ok(())
}
#[wasm_bindgen]
pub fn destroy(&self) {
self.runner.destroy();
}
#[wasm_bindgen]
pub fn has_panicked(&self) -> bool {
self.runner.panic_summary().is_some()
}
#[wasm_bindgen]
pub fn panic_message(&self) -> Option<String> {
self.runner.panic_summary().map(|s| s.message())
}
#[wasm_bindgen]
pub fn panic_callstack(&self) -> Option<String> {
self.runner.panic_summary().map(|s| s.callstack())
}
#[wasm_bindgen]
pub fn add_receiver(&self, url: &str, follow_if_http: Option<bool>) {
let Some(mut app) = self.runner.app_mut::<crate::App>() else {
return;
};
let follow_if_http = follow_if_http.unwrap_or(false);
let rx = url_to_receiver(app.egui_ctx.clone(), follow_if_http, url.to_owned());
if let Some(rx) = rx.ok_or_log_error() {
app.add_receiver(rx);
}
}
#[wasm_bindgen]
pub fn remove_receiver(&self, url: &str) {
let Some(mut app) = self.runner.app_mut::<crate::App>() else {
return;
};
app.msg_receive_set().remove_by_uri(url);
if let Some(store_hub) = app.store_hub.as_mut() {
store_hub.remove_recording_by_uri(url);
}
}
#[wasm_bindgen]
pub fn open_channel(&mut self, id: &str, channel_name: &str) {
let Some(mut app) = self.runner.app_mut::<crate::App>() else {
return;
};
if self.tx_channels.contains_key(id) {
re_log::warn!("Channel with id '{}' already exists.", id);
return;
}
let (tx, rx) = re_smart_channel::smart_channel(
re_smart_channel::SmartMessageSource::JsChannelPush,
re_smart_channel::SmartChannelSource::JsChannel {
channel_name: channel_name.to_owned(),
},
);
app.add_receiver(rx);
self.tx_channels.insert(id.to_owned(), tx);
}
#[wasm_bindgen]
pub fn close_channel(&mut self, id: &str) {
let Some(app) = self.runner.app_mut::<crate::App>() else {
return;
};
if let Some(tx) = self.tx_channels.remove(id) {
tx.quit(None).warn_on_err_once("Failed to send quit marker");
}
app.egui_ctx
.request_repaint_after(std::time::Duration::from_millis(10));
}
#[wasm_bindgen]
pub fn send_rrd_to_channel(&mut self, id: &str, data: &[u8]) {
use std::{ops::ControlFlow, sync::Arc};
let Some(app) = self.runner.app_mut::<crate::App>() else {
return;
};
if let Some(tx) = self.tx_channels.get(id).cloned() {
let data: Vec<u8> = data.to_vec();
let egui_ctx = app.egui_ctx.clone();
let ui_waker = Box::new(move || {
egui_ctx.request_repaint_after(std::time::Duration::from_millis(10));
});
re_log_encoding::stream_rrd_from_http::web_decode::decode_rrd(
data,
Arc::new({
move |msg| {
ui_waker();
use re_log_encoding::stream_rrd_from_http::HttpMessage;
match msg {
HttpMessage::LogMsg(msg) => {
if tx.send(msg).is_ok() {
ControlFlow::Continue(())
} else {
re_log::info_once!("Failed to dispatch log message to viewer.");
ControlFlow::Break(())
}
}
HttpMessage::Success => ControlFlow::Continue(()),
HttpMessage::Failure(err) => {
tx.quit(Some(err))
.warn_on_err_once("Failed to send quit marker");
ControlFlow::Break(())
}
}
}
}),
);
}
}
}
#[derive(Clone, Deserialize, strum_macros::EnumString)]
#[strum(serialize_all = "snake_case")]
enum Panel {
Top,
Blueprint,
Selection,
Time,
}
#[derive(Clone, Deserialize, strum_macros::EnumString)]
#[strum(serialize_all = "snake_case")]
enum PanelState {
Hidden,
Collapsed,
Expanded,
}
impl From<PanelState> for re_types::blueprint::components::PanelState {
fn from(value: PanelState) -> Self {
match value {
PanelState::Hidden => Self::Hidden,
PanelState::Collapsed => Self::Collapsed,
PanelState::Expanded => Self::Expanded,
}
}
}
#[derive(Clone, Default, Deserialize)]
pub struct AppOptions {
url: Option<StringOrStringArray>,
manifest_url: Option<String>,
render_backend: Option<String>,
hide_welcome_screen: Option<bool>,
panel_state_overrides: Option<PanelStateOverrides>,
fullscreen: Option<FullscreenOptions>,
enable_history: Option<bool>,
notebook: Option<bool>,
persist: Option<bool>,
}
#[derive(Clone, Deserialize)]
pub struct FullscreenOptions {
pub get_state: Callback,
pub on_toggle: Callback,
}
#[derive(Clone, Default, Deserialize)]
pub struct PanelStateOverrides {
top: Option<PanelState>,
blueprint: Option<PanelState>,
selection: Option<PanelState>,
time: Option<PanelState>,
}
impl From<PanelStateOverrides> for crate::app_blueprint::PanelStateOverrides {
fn from(value: PanelStateOverrides) -> Self {
Self {
top: value.top.map(|v| v.into()),
blueprint: value.blueprint.map(|v| v.into()),
selection: value.selection.map(|v| v.into()),
time: value.time.map(|v| v.into()),
}
}
}
fn create_app(
cc: &eframe::CreationContext<'_>,
app_options: AppOptions,
) -> Result<crate::App, re_renderer::RenderContextError> {
let build_info = re_build_info::build_info!();
let app_env = crate::AppEnvironment::Web {
url: cc.integration_info.web_info.location.url.clone(),
};
let enable_history = app_options.enable_history.unwrap_or(false);
let startup_options = crate::StartupOptions {
memory_limit: re_memory::MemoryLimit {
max_bytes: Some(2_500_000_000),
},
location: Some(cc.integration_info.web_info.location.clone()),
persist_state: app_options.persist.unwrap_or(true),
is_in_notebook: app_options.notebook.unwrap_or(false),
expect_data_soon: None,
force_wgpu_backend: None,
hide_welcome_screen: app_options.hide_welcome_screen.unwrap_or(false),
fullscreen_options: app_options.fullscreen.clone(),
panel_state_overrides: app_options.panel_state_overrides.unwrap_or_default().into(),
enable_history,
};
crate::customize_eframe_and_setup_renderer(cc)?;
let mut app = crate::App::new(
build_info,
&app_env,
startup_options,
cc.egui_ctx.clone(),
cc.storage,
);
if enable_history {
install_popstate_listener(&mut app).ok_or_log_js_error();
}
if let Some(manifest_url) = app_options.manifest_url {
app.set_examples_manifest_url(manifest_url);
}
if let Some(urls) = app_options.url {
let follow_if_http = false;
for url in urls.into_inner() {
if let Some(receiver) =
url_to_receiver(cc.egui_ctx.clone(), follow_if_http, url).ok_or_log_error()
{
app.command_sender
.send_system(SystemCommand::AddReceiver(receiver));
}
}
}
Ok(app)
}
#[cfg(feature = "analytics")]
#[wasm_bindgen]
pub fn set_email(email: String) {
let mut config = re_analytics::Config::load().unwrap().unwrap_or_default();
config.opt_in_metadata.insert("email".into(), email.into());
config.save().unwrap();
}