Files
Graphite/frontend/wrapper/src/editor_wrapper.rs
Timon 6f8c4b2adb Desktop: Stop compiling the editor into the wasm wrapper (#4348)
* Make wasm wrapper not depend on editor on native

* Introduce pkg-native

* Fix tests

* Fix fmt

* Correct span and make cargo fmt work inside editor_commands block

* Review

* Fix lint
2026-07-18 10:33:38 +00:00

265 lines
9.7 KiB
Rust

//! 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<dyn ResourceStorage> = 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/<id_hex>`.
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<T: Into<Message>>(&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");
}
}