Bundle Graphite using Tauri (#873)

* Setup tauri component for graphite editor

Integrate graphite into tauri app

Split interpreted-executor out of graph-craft

* Add gpu execution node

* General Cleanup
This commit is contained in:
TrueDoctor
2022-12-07 12:49:34 +01:00
committed by Keavon Chambers
parent 52cc770a1e
commit 7d8f94462a
109 changed files with 5661 additions and 544 deletions
+1380
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -7,7 +7,9 @@
"serve": "vue-cli-service serve || echo 'Graphite project failed to build. Did you remember to `npm install` the dependencies?'",
"build": "vue-cli-service build || echo 'Graphite project failed to build. Did you remember to `npm install` the dependencies?'",
"lint": "vue-cli-service lint || echo 'Graphite project had lint errors or otherwise failed. In the latter case, did you remember to `npm install` the dependencies?'",
"lint-no-fix": "vue-cli-service lint --no-fix || echo 'Graphite project had lint errors or otherwise failed. In the latter case, did you remember to `npm install` the dependencies?'"
"lint-no-fix": "vue-cli-service lint --no-fix || echo 'Graphite project had lint errors or otherwise failed. In the latter case, did you remember to `npm install` the dependencies?'",
"tauri:build": "vue-cli-service tauri:build",
"tauri:serve": "vue-cli-service tauri:serve"
},
"repository": {
"type": "git",
@@ -17,6 +19,7 @@
"license": "Apache-2.0",
"homepage": "https://graphite.rs",
"dependencies": {
"@tauri-apps/api": "^1.2.0",
"class-transformer": "^0.5.1",
"idb-keyval": "^6.2.0",
"reflect-metadata": "^0.1.13",
@@ -42,6 +45,7 @@
"sass": "^1.56.1",
"sass-loader": "^13.2.0",
"typescript": "^4.9.3",
"vue-cli-plugin-tauri": "~1.0.0",
"vue-loader": "^17.0.1",
"vue-template-compiler": "^2.7.14"
},
+3
View File
@@ -0,0 +1,3 @@
# Generated by Cargo
# will have compiled files and executables
/target/
+38
View File
@@ -0,0 +1,38 @@
[package]
name = "graphite-desktop"
version = "0.1.0"
description = "Graphite Desktop"
authors = ["Graphite Authors <contact@graphite.rs>"]
license = "Apache-2.0"
repository = ""
default-run = "graphite-desktop"
edition = "2021"
rust-version = "1.59"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.2", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.2", features = ["api-all", "devtools"] }
axum = "0.6.1"
graphite-editor = { version = "0.0.0", path = "../../editor" }
chrono = "^0.4.23"
ron = "0.8"
log = "0.4"
fern = {version = "0.6", features = ["colored"] }
futures = "0.3.25"
[features]
gpu = ["graphite-editor/gpu"]
# by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
default = [ "custom-protocol" ]
# this feature is used for production builds where `devPath` points to the filesystem
# DO NOT remove this
custom-protocol = [ "tauri/custom-protocol" ]
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+125
View File
@@ -0,0 +1,125 @@
#![cfg_attr(all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows")]
use std::sync::Arc;
use axum::body::StreamBody;
use axum::extract::Path;
use axum::http;
use axum::response::IntoResponse;
use axum::{routing::get, Router};
use fern::colors::{Color, ColoredLevelConfig};
use graphite_editor::application::Editor;
use graphite_editor::messages::frontend::utility_types::FrontendImageData;
use graphite_editor::messages::prelude::*;
use http::{Response, StatusCode};
use std::collections::HashMap;
use std::sync::Mutex;
use tauri::Manager;
static IMAGES: Mutex<Option<HashMap<String, FrontendImageData>>> = Mutex::new(None);
static EDITOR: Mutex<Option<Editor>> = Mutex::new(None);
async fn respond_to(id: Path<String>) -> impl IntoResponse {
let builder = Response::builder().header("Access-Control-Allow-Origin", "*").status(StatusCode::OK);
let guard = IMAGES.lock().unwrap();
let images = guard;
let image = images.as_ref().unwrap().get(&id.0).unwrap();
println!("image: {:#?}", image.path);
let result: Result<Vec<u8>, &str> = Ok((*image.image_data).clone());
let stream = futures::stream::once(async move { result });
builder.body(StreamBody::new(stream)).unwrap()
}
fn main() {
println!("Starting server...");
let colors = ColoredLevelConfig::new().debug(Color::Magenta).info(Color::Green).error(Color::Red);
fern::Dispatch::new()
.chain(std::io::stdout())
.level(log::LevelFilter::Trace)
.format(move |out, message, record| {
out.finish(format_args!(
"[{}]{} {}",
// This will color the log level only, not the whole line. Just a touch.
colors.color(record.level()),
chrono::Utc::now().format("[%Y-%m-%d %H:%M:%S]"),
message
))
})
.apply()
.unwrap();
*(IMAGES.lock().unwrap()) = Some(HashMap::new());
graphite_editor::application::set_uuid_seed(0);
*(EDITOR.lock().unwrap()) = Some(Editor::new());
let app = Router::new().route("/", get(|| async { "Hello, World!" })).route("/image/:id", get(respond_to));
// run it with hyper on localhost:3000
tauri::async_runtime::spawn(async {
axum::Server::bind(&"0.0.0.0:3001".parse().unwrap()).serve(app.into_make_service()).await.unwrap();
});
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![set_random_seed, handle_message])
.setup(|app| {
app.get_window("main").unwrap().open_devtools();
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
#[tauri::command]
fn set_random_seed(seed: f64) {
let seed = seed as u64;
graphite_editor::application::set_uuid_seed(seed);
}
#[tauri::command]
fn handle_message(message: String) -> String {
let Ok(message) = ron::from_str::<graphite_editor::messages::message::Message>(&message) else {
panic!("Error parsing message: {}", message)
};
let mut guard = EDITOR.lock().unwrap();
let editor = (*guard).as_mut().unwrap();
let responses = editor.handle_message(message);
// Sends a FrontendMessage to JavaScript
fn send_frontend_message_to_js(message: FrontendMessage) -> FrontendMessage {
// Special case for update image data to avoid serialization times.
if let FrontendMessage::UpdateImageData { document_id, image_data } = message {
let mut guard = IMAGES.lock().unwrap();
let images = (*guard).as_mut().unwrap();
let mut stub_data = Vec::with_capacity(image_data.len());
for image in image_data {
let path = image.path.clone();
let mime = image.mime.clone();
images.insert(format!("{:?}_{}", &image.path, document_id), image);
stub_data.push(FrontendImageData {
path,
mime,
image_data: Arc::new(Vec::new()),
});
}
FrontendMessage::UpdateImageData { document_id, image_data: stub_data }
} else {
message
}
}
for response in &responses {
let serialized = ron::to_string(&send_frontend_message_to_js(response.clone())).unwrap();
if let Err(error) = ron::from_str::<FrontendMessage>(&serialized) {
log::error!("Error deserializing message: {}", error);
log::debug!("{:#?}", response);
log::debug!("{}", serialized);
}
}
// Process any `FrontendMessage` responses resulting from the backend processing the dispatched message
let result: Vec<_> = responses.into_iter().map(send_frontend_message_to_js).collect();
ron::to_string(&result).expect("Failed to serialize FrontendMessage")
}
+66
View File
@@ -0,0 +1,66 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"build": {
"beforeBuildCommand": "npm run build",
"beforeDevCommand": "npm start",
"distDir": "../dist",
"devPath": "http://127.0.0.1:8080"
},
"package": {
"productName": "graphite-tauri",
"version": "0.1.0"
},
"tauri": {
"allowlist": {
"all": true
},
"bundle": {
"active": true,
"category": "DeveloperTool",
"copyright": "",
"deb": {
"depends": ["librustc_codegen_spirv"]
},
"externalBin": [],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "rs.graphite.editor",
"longDescription": "",
"macOS": {
"entitlements": null,
"exceptionDomain": "",
"frameworks": [],
"providerShortName": null,
"signingIdentity": null
},
"resources": [],
"shortDescription": "",
"targets": "all",
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
},
"security": {
"csp": null
},
"updater": {
"active": false
},
"windows": [
{
"fullscreen": false,
"height": 600,
"resizable": true,
"title": "Graphite",
"width": 800
}
]
}
}
+4 -4
View File
@@ -280,7 +280,7 @@ import {
type LayerPanelEntry,
defaultWidgetLayout,
UpdateDocumentLayerDetails,
UpdateDocumentLayerTreeStructure,
UpdateDocumentLayerTreeStructureJs,
UpdateLayerTreeOptionsLayout,
layerTypeData,
} from "@/wasm-communication/messages";
@@ -483,14 +483,14 @@ export default defineComponent({
this.fakeHighlight = undefined;
this.dragInPanel = false;
},
rebuildLayerTree(updateDocumentLayerTreeStructure: UpdateDocumentLayerTreeStructure) {
rebuildLayerTree(updateDocumentLayerTreeStructure: UpdateDocumentLayerTreeStructureJs) {
const layerWithNameBeingEdited = this.layers.find((layer: LayerListingInfo) => layer.editingName);
const layerPathWithNameBeingEdited = layerWithNameBeingEdited?.entry.path;
const layerIdWithNameBeingEdited = layerPathWithNameBeingEdited?.slice(-1)[0];
const path = [] as bigint[];
this.layers = [] as LayerListingInfo[];
const recurse = (folder: UpdateDocumentLayerTreeStructure, layers: LayerListingInfo[], cache: Map<string, LayerPanelEntry>): void => {
const recurse = (folder: UpdateDocumentLayerTreeStructureJs, layers: LayerListingInfo[], cache: Map<string, LayerPanelEntry>): void => {
folder.children.forEach((item, index) => {
// TODO: fix toString
const layerId = BigInt(item.layerId.toString());
@@ -520,7 +520,7 @@ export default defineComponent({
},
},
mounted() {
this.editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerTreeStructure, (updateDocumentLayerTreeStructure) => {
this.editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerTreeStructureJs, (updateDocumentLayerTreeStructure) => {
this.rebuildLayerTree(updateDocumentLayerTreeStructure);
});
@@ -68,7 +68,7 @@
import { defineComponent } from "vue";
import { platformIsMac } from "@/utility-functions/platform";
import { type KeyRaw, type KeysGroup, type MenuBarEntry, type MenuListEntry, UpdateMenuBarLayout } from "@/wasm-communication/messages";
import { type KeyRaw, type LayoutKeysGroup, type MenuBarEntry, type MenuListEntry, UpdateMenuBarLayout } from "@/wasm-communication/messages";
import MenuList from "@/components/floating-menus/MenuList.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
@@ -92,7 +92,7 @@ export default defineComponent({
mounted() {
this.editor.subscriptions.subscribeJsMessage(UpdateMenuBarLayout, (updateMenuBarLayout) => {
const arraysEqual = (a: KeyRaw[], b: KeyRaw[]): boolean => a.length === b.length && a.every((aValue, i) => aValue === b[i]);
const shortcutRequiresLock = (shortcut: KeysGroup): boolean => {
const shortcutRequiresLock = (shortcut: LayoutKeysGroup): boolean => {
const shortcutKeys = shortcut.map((keyWithLabel) => keyWithLabel.key);
// If this shortcut matches any of the browser-reserved shortcuts
@@ -127,7 +127,7 @@ import { defineComponent, type PropType } from "vue";
import { type IconName } from "@/utility-functions/icons";
import { platformIsMac } from "@/utility-functions/platform";
import { type KeyRaw, type KeysGroup, type Key, type MouseMotion } from "@/wasm-communication/messages";
import { type KeyRaw, type LayoutKeysGroup, type Key, type MouseMotion } from "@/wasm-communication/messages";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
@@ -158,7 +158,7 @@ const ICON_WIDTHS = {
export default defineComponent({
inject: ["fullscreen"],
props: {
keysWithLabelsGroups: { type: Array as PropType<KeysGroup[]>, default: () => [] },
keysWithLabelsGroups: { type: Array as PropType<LayoutKeysGroup[]>, default: () => [] },
mouseMotion: { type: String as PropType<MouseMotion | undefined>, required: false },
requiresLock: { type: Boolean as PropType<boolean>, default: false },
},
@@ -182,7 +182,7 @@ export default defineComponent({
},
},
methods: {
keyTextOrIconList(keyGroup: KeysGroup): LabelData[] {
keyTextOrIconList(keyGroup: LayoutKeysGroup): LabelData[] {
return keyGroup.map((key) => this.keyTextOrIcon(key));
},
keyTextOrIcon(keyWithLabel: Key): LabelData {
@@ -49,7 +49,7 @@
import { defineComponent } from "vue";
import { platformIsMac } from "@/utility-functions/platform";
import { type HintData, type HintInfo, type KeysGroup, UpdateInputHints } from "@/wasm-communication/messages";
import { type HintData, type HintInfo, type LayoutKeysGroup, UpdateInputHints } from "@/wasm-communication/messages";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import Separator from "@/components/widgets/labels/Separator.vue";
@@ -63,7 +63,7 @@ export default defineComponent({
};
},
methods: {
inputKeysForPlatform(hint: HintInfo): KeysGroup[] {
inputKeysForPlatform(hint: HintInfo): LayoutKeysGroup[] {
if (platformIsMac() && hint.keyGroupsMac) return hint.keyGroupsMac;
return hint.keyGroups;
},
@@ -214,7 +214,7 @@ import { defineComponent, nextTick, type PropType } from "vue";
import { platformIsMac } from "@/utility-functions/platform";
import { type KeysGroup, type Key } from "@/wasm-communication/messages";
import { type LayoutKeysGroup, type Key } from "@/wasm-communication/messages";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
@@ -258,7 +258,7 @@ export default defineComponent({
openDocument() {
this.editor.instance.documentOpen();
},
platformModifiers(reservedKey: boolean): KeysGroup {
platformModifiers(reservedKey: boolean): LayoutKeysGroup {
// TODO: Remove this by properly feeding these keys from a layout provided by the backend
const ALT: Key = { key: "Alt", label: "Alt" };
+4 -4
View File
@@ -30,7 +30,7 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const listeners: { target: EventListenerTarget; eventName: EventName; action: (event: any) => void; options?: boolean | AddEventListenerOptions }[] = [
{ target: window, eventName: "resize", action: (): void => onWindowResize(container) },
{ target: window, eventName: "beforeunload", action: (e: BeforeUnloadEvent): void => onBeforeUnload(e) },
{ target: window, eventName: "beforeunload", action: (e: BeforeUnloadEvent): Promise<void> => onBeforeUnload(e) },
{ target: window.document, eventName: "contextmenu", action: (e: MouseEvent): void => e.preventDefault() },
{ target: window.document, eventName: "fullscreenchange", action: (): void => fullscreen.fullscreenModeChanged() },
{ target: window, eventName: "keyup", action: (e: KeyboardEvent): Promise<void> => onKeyUp(e) },
@@ -235,15 +235,15 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo
if (boundsOfViewports.length > 0) editor.instance.boundsOfViewports(data);
}
function onBeforeUnload(e: BeforeUnloadEvent): void {
async function onBeforeUnload(e: BeforeUnloadEvent): Promise<void> {
const activeDocument = document.state.documents[document.state.activeDocumentIndex];
if (activeDocument && !activeDocument.isAutoSaved) editor.instance.triggerAutoSave(activeDocument.id);
// Skip the message if the editor crashed, since work is already lost
if (editor.instance.hasCrashed()) return;
if (await editor.instance.hasCrashed()) return;
// Skip the message during development, since it's annoying when testing
if (editor.instance.inDevelopmentMode()) return;
if (await editor.instance.inDevelopmentMode()) return;
const allDocumentsSaved = document.state.documents.reduce((acc, doc) => acc && doc.isSaved, true);
if (!allDocumentsSaved) {
+33 -1
View File
@@ -1,3 +1,5 @@
import { invoke } from "@tauri-apps/api";
import type WasmBindgenPackage from "@/../wasm/pkg";
import { panicProxy } from "@/utility-functions/panic-proxy";
import { type JsMessageType } from "@/wasm-communication/messages";
@@ -24,6 +26,30 @@ export async function updateImage(path: BigUint64Array, mime: string, imageData:
editorInstance?.setImageBlobURL(documentId, path, blobURL, image.naturalWidth, image.naturalHeight);
}
export async function fetchImage(path: BigUint64Array, mime: string, documentId: bigint, url: string): Promise<void> {
const data = await fetch(url);
const blob = await data.blob();
const blobURL = URL.createObjectURL(blob);
// Pre-decode the image so it is ready to be drawn instantly once it's placed into the viewport SVG
const image = new Image();
image.src = blobURL;
await image.decode();
editorInstance?.setImageBlobURL(documentId, path, blobURL, image.naturalWidth, image.naturalHeight);
}
// export async function dispatchTauri(message: string): Promise<string> {
export async function dispatchTauri(message: any): Promise<void> {
try {
const response = await invoke("handle_message", { message });
editorInstance?.tauriResponse(response);
} catch {
console.error("Failed to dispatch Tauri message");
}
}
// Should be called asynchronously before `createEditor()`
export async function initWasm(): Promise<void> {
// Skip if the WASM module is already initialized
@@ -34,8 +60,14 @@ export async function initWasm(): Promise<void> {
wasmImport = await import("@/../wasm/pkg").then(panicProxy);
// Provide a random starter seed which must occur after initializing the WASM module, since WASM can't generate its own random numbers
const randomSeed = BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER));
const randomSeedFloat = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
const randomSeed = BigInt(randomSeedFloat);
wasmImport?.setRandomSeed(randomSeed);
try {
await invoke("set_random_seed", { seed: randomSeedFloat });
} catch {
// Ignore errors
}
}
// Should be called after running `initWasm()` and its promise resolving
+10 -10
View File
@@ -136,9 +136,9 @@ export type HintData = HintGroup[];
export type HintGroup = HintInfo[];
export class HintInfo {
readonly keyGroups!: KeysGroup[];
readonly keyGroups!: LayoutKeysGroup[];
readonly keyGroupsMac!: KeysGroup[] | undefined;
readonly keyGroupsMac!: LayoutKeysGroup[] | undefined;
readonly mouse!: MouseMotion | undefined;
@@ -151,8 +151,8 @@ export class HintInfo {
export type KeyRaw = string;
// Serde converts a Rust `Key` enum variant into this format (via a custom serializer) with both the `Key` variant name (called `RawKey` in TS) and the localized `label` for the key
export type Key = { key: KeyRaw; label: string };
export type KeysGroup = Key[];
export type ActionKeys = { keys: KeysGroup };
export type LayoutKeysGroup = Key[];
export type ActionKeys = { keys: LayoutKeysGroup };
export type MouseMotion = string;
@@ -596,8 +596,8 @@ export class TriggerSavePreferences extends JsMessage {
export class DocumentChanged extends JsMessage {}
export class UpdateDocumentLayerTreeStructure extends JsMessage {
constructor(readonly layerId: bigint, readonly children: UpdateDocumentLayerTreeStructure[]) {
export class UpdateDocumentLayerTreeStructureJs extends JsMessage {
constructor(readonly layerId: bigint, readonly children: UpdateDocumentLayerTreeStructureJs[]) {
super();
}
}
@@ -607,7 +607,7 @@ type DataBuffer = {
length: bigint;
};
export function newUpdateDocumentLayerTreeStructure(input: { dataBuffer: DataBuffer }, wasm: WasmRawInstance): UpdateDocumentLayerTreeStructure {
export function newUpdateDocumentLayerTreeStructure(input: { dataBuffer: DataBuffer }, wasm: WasmRawInstance): UpdateDocumentLayerTreeStructureJs {
const pointerNum = Number(input.dataBuffer.pointer);
const lengthNum = Number(input.dataBuffer.length);
@@ -624,7 +624,7 @@ export function newUpdateDocumentLayerTreeStructure(input: { dataBuffer: DataBuf
const layerIdsSection = new DataView(wasmMemoryBuffer, pointerNum + 8 + structureSectionLength * 8);
let layersEncountered = 0;
let currentFolder = new UpdateDocumentLayerTreeStructure(BigInt(-1), []);
let currentFolder = new UpdateDocumentLayerTreeStructureJs(BigInt(-1), []);
const currentFolderStack = [currentFolder];
for (let i = 0; i < structureSectionLength; i += 1) {
@@ -639,7 +639,7 @@ export function newUpdateDocumentLayerTreeStructure(input: { dataBuffer: DataBuf
const layerId = layerIdsSection.getBigUint64(layersEncountered * 8, true);
layersEncountered += 1;
const childLayer = new UpdateDocumentLayerTreeStructure(layerId, []);
const childLayer = new UpdateDocumentLayerTreeStructureJs(layerId, []);
currentFolder.children.push(childLayer);
}
@@ -1361,7 +1361,7 @@ export const messageMakers: Record<string, MessageMaker> = {
UpdateDocumentArtwork,
UpdateDocumentBarLayout,
UpdateDocumentLayerDetails,
UpdateDocumentLayerTreeStructure: newUpdateDocumentLayerTreeStructure,
UpdateDocumentLayerTreeStructureJs: newUpdateDocumentLayerTreeStructure,
UpdateDocumentModeLayout,
UpdateDocumentOverlays,
UpdateDocumentRulers,
+6
View File
@@ -10,6 +10,10 @@ homepage = "https://graphite.rs"
repository = "https://github.com/GraphiteEditor/Graphite"
license = "Apache-2.0"
[features]
tauri = ["ron"]
default = []
[lib]
crate-type = ["cdylib", "rlib"]
@@ -22,6 +26,8 @@ serde = { version = "1.0", features = ["derive"] }
wasm-bindgen = { version = "0.2.73" }
serde-wasm-bindgen = "0.4.1"
js-sys = "0.3.55"
wasm-bindgen-futures = "0.4.33"
ron = {version = "0.8", optional = true}
[dev-dependencies]
wasm-bindgen-test = "0.3.22"
+51 -17
View File
@@ -33,6 +33,9 @@ pub fn set_random_seed(seed: u64) {
#[wasm_bindgen(module = "@/wasm-communication/editor")]
extern "C" {
fn updateImage(path: Vec<u64>, mime: String, imageData: &[u8], document_id: u64);
fn fetchImage(path: Vec<u64>, mime: String, document_id: u64, identifier: String);
//fn dispatchTauri(message: String) -> String;
fn dispatchTauri(message: String);
}
/// Provides a handle to access the raw WASM memory
@@ -73,37 +76,53 @@ impl JsEditorHandle {
return;
}
// Get the editor instances, dispatch the message, and store the `FrontendMessage` queue response
let frontend_messages = EDITOR_INSTANCES.with(|instances| {
// Mutably borrow the editors, and if successful, we can access them in the closure
instances.try_borrow_mut().map(|mut editors| {
// Get the editor instance for this editor ID, then dispatch the message to the backend, and return its response `FrontendMessage` queue
editors
.get_mut(&self.editor_id)
.expect("EDITOR_INSTANCES does not contain the current editor_id")
.handle_message(message.into())
})
});
#[cfg(feature = "tauri")]
{
let message: Message = message.into();
let message = ron::to_string(&message).unwrap();
// Process any `FrontendMessage` responses resulting from the backend processing the dispatched message
if let Ok(frontend_messages) = frontend_messages {
// Send each `FrontendMessage` to the JavaScript frontend
for message in frontend_messages.into_iter() {
self.send_frontend_message_to_js(message);
dispatchTauri(message);
}
#[cfg(not(feature = "tauri"))]
{
// Get the editor instances, dispatch the message, and store the `FrontendMessage` queue response
let frontend_messages = EDITOR_INSTANCES.with(|instances| {
// Mutably borrow the editors, and if successful, we can access them in the closure
instances.try_borrow_mut().map(|mut editors| {
// Get the editor instance for this editor ID, then dispatch the message to the backend, and return its response `FrontendMessage` queue
editors
.get_mut(&self.editor_id)
.expect("EDITOR_INSTANCES does not contain the current editor_id")
.handle_message(message.into())
})
});
// Process any `FrontendMessage` responses resulting from the backend processing the dispatched message
if let Ok(frontend_messages) = frontend_messages {
// Send each `FrontendMessage` to the JavaScript frontend
for message in frontend_messages.into_iter() {
self.send_frontend_message_to_js(message);
}
}
}
// If the editor cannot be borrowed then it has encountered a panic - we should just ignore new dispatches
}
// Sends a FrontendMessage to JavaScript
fn send_frontend_message_to_js(&self, message: FrontendMessage) {
fn send_frontend_message_to_js(&self, mut message: FrontendMessage) {
// Special case for update image data to avoid serialization times.
if let FrontendMessage::UpdateImageData { document_id, image_data } = message {
for image in image_data {
#[cfg(not(feature = "tauri"))]
updateImage(image.path, image.mime, &image.image_data, document_id);
#[cfg(feature = "tauri")]
fetchImage(image.path.clone(), image.mime, document_id, format!("http://localhost:3001/image/{:?}_{}", &image.path, document_id));
}
return;
}
if let FrontendMessage::UpdateDocumentLayerTreeStructure { data_buffer } = message {
message = FrontendMessage::UpdateDocumentLayerTreeStructureJs { data_buffer: data_buffer.into() };
}
let message_type = message.to_discriminant().local_name();
@@ -139,6 +158,21 @@ impl JsEditorHandle {
self.dispatch(Message::Init);
}
#[wasm_bindgen(js_name = tauriResponse)]
pub fn tauri_response(&self, message: JsValue) {
#[cfg(feature = "tauri")]
match ron::from_str::<Vec<FrontendMessage>>(&message.as_string().unwrap()) {
Ok(response) => {
for message in response {
self.send_frontend_message_to_js(message);
}
}
Err(error) => {
log::error!("tauri response: {:?}\n{:?}", error, message);
}
}
}
/// Displays a dialog with an error message
#[wasm_bindgen(js_name = errorDialog)]
pub fn error_dialog(&self, title: String, description: String) {