//! JS-facing editor handle. Owns the callback that delivers `FrontendMessage`s to JS; on web `dispatch` runs //! messages through the in-process editor, on native `send` forwards them as `EditorCommand`s. #[cfg(not(feature = "native"))] use crate::EDITOR; #[cfg(not(feature = "native"))] use crate::MESSAGE_BUFFER; #[cfg(all(feature = "native", target_family = "wasm"))] use crate::editor_commands::EditorCommand; #[cfg(not(feature = "native"))] use crate::helpers::poll_node_graph_evaluation; #[cfg(any(not(feature = "native"), target_family = "wasm"))] use crate::helpers::wrapper; #[cfg(feature = "editor")] use crate::helpers::{calculate_hash, render_image_data_to_canvases}; use crate::helpers::{request_animation_frame, set_timeout}; use crate::{EDITOR_HAS_CRASHED, FRONTEND_READY}; #[cfg(all(not(feature = "native"), target_family = "wasm"))] use editor::application::{Editor, Environment, Host, Platform}; #[cfg(feature = "editor")] use editor::messages::prelude::*; #[cfg(feature = "editor")] use serde::Serialize; use std::cell::RefCell; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::Duration; use wasm_bindgen::prelude::*; pub(crate) static IMAGE_DATA_HASH: AtomicU64 = AtomicU64::new(0); #[wasm_bindgen] #[derive(Clone)] pub struct EditorWrapper { /// This callback is called by the editor's dispatcher when directing `FrontendMessage`s from Rust to JS frontend_message_handler_callback: js_sys::Function, } impl EditorWrapper { #[cfg(any(feature = "native", target_family = "wasm"))] fn initialize_wrapper(frontend_message_handler_callback: js_sys::Function) -> EditorWrapper { use crate::{EDITOR_WRAPPER, PANIC_DIALOG_MESSAGE_CALLBACK}; let panic_callback = frontend_message_handler_callback.clone(); let editor_wrapper = EditorWrapper { frontend_message_handler_callback }; if EDITOR_WRAPPER.with(|wrapper| wrapper.lock().ok().map(|mut guard| *guard = Some(editor_wrapper.clone()))).is_none() { log::error!("Attempted to initialize the editor wrapper more than once"); } PANIC_DIALOG_MESSAGE_CALLBACK.with_borrow_mut(|callback| *callback = Some(panic_callback)); editor_wrapper } } #[wasm_bindgen] impl EditorWrapper { #[cfg(all(not(feature = "native"), target_family = "wasm"))] pub async fn create(platform: String, uuid_random_seed: u64, frontend_message_handler_callback: js_sys::Function) -> EditorWrapper { use graph_craft::application_io::PlatformApplicationIo; use graph_craft::application_io::resource::*; let host = match platform.as_str() { "Linux" => Host::Linux, "Mac" => Host::Mac, "Windows" => Host::Windows, _ => unreachable!(), }; let storage: std::sync::Arc = match OpfsResourceStorage::load("resources").await { Ok(storage) => std::sync::Arc::new(storage), Err(error) => { log::error!("Failed to open OPFS resource storage, falling back to in-memory: {error:?}"); std::sync::Arc::new(graph_craft::application_io::resource::HashMapResourceStorage::new()) } }; let application_io = PlatformApplicationIo::new().await; let wake = crate::helpers::async_wake_callback(); // On web the working-copy root is an OPFS directory name (no real filesystem path); each // document mounts under `documents/`. let working_copy_root = Some(std::path::PathBuf::from("documents")); let editor = Editor::new(Environment { platform: Platform::Web, host }, uuid_random_seed, storage, working_copy_root, application_io, wake); if EDITOR.with(|slot| slot.lock().ok().map(|mut guard| *guard = Some(editor))).is_none() { log::error!("Attempted to initialize the editor more than once"); } Self::initialize_wrapper(frontend_message_handler_callback) } #[cfg(feature = "native")] pub fn create(_platform: String, _uuid_random_seed: u64, frontend_message_handler_callback: js_sys::Function) -> EditorWrapper { Self::initialize_wrapper(frontend_message_handler_callback) } // Sends a message to the dispatcher in the Editor Backend #[cfg(not(feature = "native"))] pub(crate) fn dispatch>(&self, message: T) { // Process no further messages after a crash to avoid spamming the console if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) { return; } // Get the editor, dispatch the message, and store the `FrontendMessage` queue response let frontend_messages = EDITOR.with(|editor| { let mut guard = editor.try_lock(); let Ok(Some(editor)) = guard.as_deref_mut() else { // Enqueue messages which can't be procssed currently MESSAGE_BUFFER.with_borrow_mut(|buffer| buffer.push(message.into())); return vec![]; }; editor.handle_message(message) }); // Send each `FrontendMessage` to the JavaScript frontend for message in frontend_messages.into_iter() { self.send_frontend_message_to_js(message); } } #[cfg(feature = "editor")] pub(crate) fn send_frontend_message_to_js(&self, message: FrontendMessage) { if let FrontendMessage::UpdateImageData { ref image_data } = message { let new_hash = calculate_hash(image_data); let prev_hash = IMAGE_DATA_HASH.load(Ordering::Relaxed); if new_hash != prev_hash { render_image_data_to_canvases(image_data.iter()); IMAGE_DATA_HASH.store(new_hash, Ordering::Relaxed); } return; } let message_type = message.to_discriminant().local_name(); let serializer = serde_wasm_bindgen::Serializer::new().serialize_large_number_types_as_bigints(true); let message_data = message.serialize(&serializer).expect("Failed to serialize FrontendMessage"); let js_return_value = self.frontend_message_handler_callback.call2(&JsValue::null(), &JsValue::from(message_type), &message_data); if let Err(error) = js_return_value { error!("While handling FrontendMessage {:?}, JavaScript threw an error:\n{:?}", message.to_discriminant().local_name(), error,) } } pub(crate) fn forward_serialized_frontend_message_to_js(&self, name: &str, data: crate::wasm_value::WasmValue) { let js_return_value = self.frontend_message_handler_callback.call2(&JsValue::null(), &JsValue::from(name), &data.into()); if let Err(error) = js_return_value { error!("While handling FrontendMessage {name:?}, JavaScript threw an error:\n{error:?}") } } #[cfg(all(feature = "native", target_family = "wasm"))] pub(crate) fn send(&self, command: EditorCommand) { // Process no further commands after a crash to avoid spamming the console if EDITOR_HAS_CRASHED.load(Ordering::SeqCst) { return; } let Ok(serialized) = serde_json::to_string(&command) else { log::error!("Failed to serialize editor command"); return; }; crate::native_communication::send_message_to_cef(serialized) } } #[wasm_bindgen] impl EditorWrapper { #[wasm_bindgen(js_name = initAfterFrontendReady)] pub fn init_after_frontend_ready(&self) { // Enforce idempotency, so if this is called again during an HMR re-mount, we don't initialize the editor backend twice if FRONTEND_READY.swap(true, Ordering::SeqCst) { return; } #[cfg(feature = "native")] crate::native_communication::initialize_native_communication(); #[cfg(target_family = "wasm")] self.init_portfolio(); // Poll node graph evaluation on `requestAnimationFrame` { let f = std::rc::Rc::new(RefCell::new(None)); let g = f.clone(); *g.borrow_mut() = Some(Closure::new(move |_timestamp| { #[cfg(not(feature = "native"))] wasm_bindgen_futures::spawn_local(poll_node_graph_evaluation()); #[cfg(any(not(feature = "native"), target_family = "wasm"))] wrapper(|wrapper| { // On web, flush the messages that queued up while the editor was locked before this frame's tick #[cfg(not(feature = "native"))] { let messages = MESSAGE_BUFFER.take(); if !messages.is_empty() { wrapper.dispatch(Message::Batched { messages: messages.into() }); } } #[cfg(target_family = "wasm")] wrapper.animation_frame(js_sys::Date::now() as u64); }); // Schedule ourself for another requestAnimationFrame callback request_animation_frame(f.borrow().as_ref().unwrap()); })); request_animation_frame(g.borrow().as_ref().unwrap()); } const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 1; // Auto save all documents on `setTimeout` { let f = std::rc::Rc::new(RefCell::new(None)); let g = f.clone(); *g.borrow_mut() = Some(Closure::new(move || { #[cfg(target_family = "wasm")] wrapper(|wrapper| wrapper.auto_save_all_documents()); // Schedule ourself for another setTimeout callback set_timeout(f.borrow().as_ref().unwrap(), Duration::from_secs(AUTO_SAVE_TIMEOUT_SECONDS)); })); set_timeout(g.borrow().as_ref().unwrap(), Duration::from_secs(AUTO_SAVE_TIMEOUT_SECONDS)); } } /// Answer whether or not the editor has crashed #[wasm_bindgen(js_name = hasCrashed)] pub fn has_crashed(&self) -> bool { EDITOR_HAS_CRASHED.load(Ordering::SeqCst) } /// Answer whether or not the editor is in development mode #[wasm_bindgen(js_name = inDevelopmentMode)] pub fn in_development_mode(&self) -> bool { cfg!(debug_assertions) } #[wasm_bindgen(js_name = fileExtension)] pub fn file_extension(&self) -> String { "graphite".into() } #[wasm_bindgen(js_name = gddFileExtension)] pub fn gdd_file_extension(&self) -> String { "gdd".into() } /// Load persisted browser storage state (web only; on desktop, persistence is handled natively and this is never triggered) #[cfg(all(feature = "web", not(feature = "native")))] #[wasm_bindgen(js_name = loadPersistedState)] pub fn load_persisted_state(&self, state: editor::messages::frontend::utility_types::PersistedState) { self.dispatch(PersistentStateMessage::LoadState { state }); } #[cfg(feature = "native")] #[wasm_bindgen(js_name = loadPersistedState)] pub fn load_persisted_state(&self, _state: JsValue) { log::error!("loadPersistedState is unavailable on desktop; persistence is handled natively"); } }