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
@@ -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"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
@@ -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" ]
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 34 KiB |
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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" };
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||