mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
* 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
265 lines
9.7 KiB
Rust
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");
|
|
}
|
|
}
|