Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions editor/src/messages/layout/layout_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub enum LayoutMessage {
layout_target: LayoutTarget,
widget_id: WidgetId,
},
ResendAllLayouts,
SendLayout {
layout: Layout,
layout_target: LayoutTarget,
Expand Down
14 changes: 14 additions & 0 deletions editor/src/messages/layout/layout_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ impl MessageHandler<LayoutMessage, LayoutMessageContext<'_>> for LayoutMessageHa
// Resend that diff
self.send_diff(vec![diff], layout_target, responses, action_input_mapping);
}
LayoutMessage::ResendAllLayouts => {
// Collect non-empty layouts and their indices, then clear the stored copies so diffs compute as full re-sends
let layouts_to_resend: Vec<_> = self
.layouts
.iter_mut()
.enumerate()
.filter(|(_, layout)| !layout.0.is_empty())
.map(|(i, layout)| (LayoutTarget::from(i as u8), std::mem::take(layout)))
.collect();

for (layout_target, layout) in layouts_to_resend {
self.diff_and_send_layout_to_frontend(layout_target, layout, responses, action_input_mapping);
}
}
LayoutMessage::SendLayout { layout, layout_target } => {
self.diff_and_send_layout_to_frontend(layout_target, layout, responses, action_input_mapping);
}
Expand Down
35 changes: 25 additions & 10 deletions editor/src/messages/layout/utility_types/layout_widget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,30 @@ impl core::fmt::Display for WidgetId {
}
}

#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(PartialEq, Clone, Debug, Hash, Eq, Copy, serde::Serialize, serde::Deserialize)]
#[repr(u8)]
pub enum LayoutTarget {
macro_rules! define_layout_target {
($($(#[$attr:meta])* $variant:ident),* $(,)?) => {
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(PartialEq, Clone, Debug, Hash, Eq, Copy, serde::Serialize, serde::Deserialize)]
#[repr(u8)]
pub enum LayoutTarget {
$($(#[$attr])* $variant,)*
// KEEP THIS ENUM LAST
// This is a marker that is used to define an array that is used to hold widgets
#[serde(skip)]
_LayoutTargetLength,
}

impl From<u8> for LayoutTarget {
fn from(value: u8) -> Self {
match value {
$(x if x == Self::$variant as u8 => Self::$variant,)*
_ => panic!("Invalid LayoutTarget discriminant: {value}"),
}
}
}
};
}
define_layout_target!(
/// The spreadsheet panel allows for the visualisation of data in the graph.
DataPanel,
/// Contains the action buttons at the bottom of the dialog. Must be shown with the `FrontendMessage::DisplayDialog` message.
Expand Down Expand Up @@ -58,12 +78,7 @@ pub enum LayoutTarget {
WelcomeScreenButtons,
/// The color swatch for the working colors and a flip and reset button found at the bottom of the tool shelf.
WorkingColors,

// KEEP THIS ENUM LAST
// This is a marker that is used to define an array that is used to hold widgets
#[serde(skip)]
_LayoutTargetLength,
}
);

/// For use by structs that define a UI widget layout by implementing the layout() function belonging to this trait.
/// The send_layout() function can then be called by other code which is a part of the same struct so as to send the layout to the frontend.
Expand Down
1 change: 1 addition & 0 deletions frontend/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export default defineConfig([
ignoreRestSiblings: true,
},
],
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "never" }],
Expand Down
67 changes: 42 additions & 25 deletions frontend/src/components/Editor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@
import { onMount, onDestroy, setContext } from "svelte";

import type { Editor } from "@graphite/editor";
import { createClipboardManager } from "@graphite/managers/clipboard";
import { createFontsManager } from "@graphite/managers/fonts";
import { createHyperlinkManager } from "@graphite/managers/hyperlink";
import { createInputManager } from "@graphite/managers/input";
import { createLocalizationManager } from "@graphite/managers/localization";
import { createPanicManager } from "@graphite/managers/panic";
import { createPersistenceManager } from "@graphite/managers/persistence";
import { createAppWindowStore } from "@graphite/stores/app-window";
import { createDialogStore } from "@graphite/stores/dialog";
import { createDocumentStore } from "@graphite/stores/document";
import { createFullscreenStore } from "@graphite/stores/fullscreen";
import { createNodeGraphStore } from "@graphite/stores/node-graph";
import { createPortfolioStore } from "@graphite/stores/portfolio";
import { createTooltipStore } from "@graphite/stores/tooltip";
import { createClipboardManager, destroyClipboardManager } from "@graphite/managers/clipboard";
import { createFontsManager, destroyFontsManager } from "@graphite/managers/fonts";
import { createHyperlinkManager, destroyHyperlinkManager } from "@graphite/managers/hyperlink";
import { createInputManager, destroyInputManager } from "@graphite/managers/input";
import { createLocalizationManager, destroyLocalizationManager } from "@graphite/managers/localization";
import { createPanicManager, destroyPanicManager } from "@graphite/managers/panic";
import { createPersistenceManager, destroyPersistenceManager } from "@graphite/managers/persistence";
import { createAppWindowStore, destroyAppWindowStore } from "@graphite/stores/app-window";
import { createDialogStore, destroyDialogStore } from "@graphite/stores/dialog";
import { createDocumentStore, destroyDocumentStore } from "@graphite/stores/document";
import { createFullscreenStore, destroyFullscreenStore } from "@graphite/stores/fullscreen";
import { createNodeGraphStore, destroyNodeGraphStore } from "@graphite/stores/node-graph";
import { createPortfolioStore, destroyPortfolioStore } from "@graphite/stores/portfolio";
import { createTooltipStore, destroyTooltipStore } from "@graphite/stores/tooltip";

import MainWindow from "@graphite/components/window/MainWindow.svelte";

Expand All @@ -34,24 +34,41 @@
};
Object.entries(stores).forEach(([key, store]) => setContext(key, store));

const managers = {
clipboard: createClipboardManager(editor),
hyperlink: createHyperlinkManager(editor),
localization: createLocalizationManager(editor),
panic: createPanicManager(editor),
persistence: createPersistenceManager(editor, stores.portfolio),
fonts: createFontsManager(editor),
input: createInputManager(editor, stores.dialog, stores.portfolio, stores.document, stores.fullscreen),
};

onMount(() => {
createClipboardManager(editor);
createHyperlinkManager(editor);
createLocalizationManager(editor);
createPanicManager(editor);
createPersistenceManager(editor, stores.portfolio);
createFontsManager(editor);
createInputManager(editor, stores.dialog, stores.portfolio, stores.document);

// Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready.
// The backend handles idempotency, so this is safe to call again during HMR re-mounts.
editor.handle.initAfterFrontendReady();

// Re-send all UI layouts from Rust so the frontend has them after an HMR re-mount
editor.handle.resendAllLayouts();
});

onDestroy(() => {
[...Object.values(stores), ...Object.values(managers)].forEach(({ destroy }) => destroy());
// Stores
destroyDialogStore();
destroyTooltipStore();
destroyDocumentStore();
destroyFullscreenStore();
destroyNodeGraphStore();
destroyPortfolioStore();
destroyAppWindowStore();

// Managers
destroyClipboardManager();
destroyHyperlinkManager();
destroyLocalizationManager();
destroyPanicManager();
destroyPersistenceManager();
destroyFontsManager();
destroyInputManager();
});
</script>

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/floating-menus/Dialog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { wipeDocuments } from "@graphite/managers/persistence";
import type { DialogStore } from "@graphite/stores/dialog";
import { crashReportUrl } from "/src/utility-functions/crash-report";
import { crashReportUrl } from "@graphite/utility-functions/crash-report";

import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
Expand Down
27 changes: 13 additions & 14 deletions frontend/src/managers/clipboard.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,32 @@
import type { Editor } from "@graphite/editor";

let currentCleanup: (() => void) | undefined;
let currentArgs: [Editor] | undefined;
let editorRef: Editor | undefined = undefined;

export function createClipboardManager(editor: Editor) {
currentArgs = [editor];
editorRef = editor;

// Subscribe to process backend event
editor.subscriptions.subscribeFrontendMessage("TriggerClipboardWrite", (data) => {
// If the Clipboard API is supported in the browser, copy text to the clipboard
navigator.clipboard?.writeText?.(data.content);
});

editor.subscriptions.subscribeFrontendMessage("TriggerSelectionRead", async (data) => {
editor.handle.readSelection(readAtCaret(data.cut), data.cut);
});

editor.subscriptions.subscribeFrontendMessage("TriggerSelectionWrite", async (data) => {
insertAtCaret(data.content);
});
}

function destroy() {
editor.subscriptions.unsubscribeFrontendMessage("TriggerClipboardWrite");
editor.subscriptions.unsubscribeFrontendMessage("TriggerSelectionRead");
editor.subscriptions.unsubscribeFrontendMessage("TriggerSelectionWrite");
}
export function destroyClipboardManager() {
const editor = editorRef;
if (!editor) return;

currentCleanup = destroy;
return { destroy };
editor.subscriptions.unsubscribeFrontendMessage("TriggerClipboardWrite");
editor.subscriptions.unsubscribeFrontendMessage("TriggerSelectionRead");
editor.subscriptions.unsubscribeFrontendMessage("TriggerSelectionWrite");
}
export type ClipboardManager = ReturnType<typeof createClipboardManager>;

function readAtCaret(cut: boolean): string | undefined {
const element = window.document.activeElement;
Expand Down Expand Up @@ -112,6 +111,6 @@ function insertAtCaret(text: string) {

// Self-accepting HMR: tear down the old instance and re-create with the new module's code
import.meta.hot?.accept((newModule) => {
currentCleanup?.();
if (currentArgs) newModule?.createClipboardManager(...currentArgs);
destroyClipboardManager();
if (editorRef) newModule?.createClipboardManager(editorRef);
});
28 changes: 13 additions & 15 deletions frontend/src/managers/fonts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ type ApiResponse = { family: string; variants: string[]; files: Record<string, s

const FONT_LIST_API = "https://api.graphite.art/font-list";

let currentCleanup: (() => void) | undefined;
let currentArgs: [Editor] | undefined;
let editorRef: Editor | undefined = undefined;

const abortController = new AbortController();
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated

export function createFontsManager(editor: Editor) {
currentArgs = [editor];
const abortController = new AbortController();
editorRef = editor;

// Subscribe to process backend events
editor.subscriptions.subscribeFrontendMessage("TriggerFontCatalogLoad", async () => {
try {
const response = await fetch(FONT_LIST_API, { signal: abortController.signal });
Expand Down Expand Up @@ -54,20 +53,19 @@ export function createFontsManager(editor: Editor) {
console.error("Failed to load font:", error);
}
});
}

function destroy() {
abortController.abort();
editor.subscriptions.unsubscribeFrontendMessage("TriggerFontCatalogLoad");
editor.subscriptions.unsubscribeFrontendMessage("TriggerFontDataLoad");
}
export function destroyFontsManager() {
const editor = editorRef;
if (!editor) return;

currentCleanup = destroy;
return { destroy };
abortController.abort();
editor.subscriptions.unsubscribeFrontendMessage("TriggerFontCatalogLoad");
editor.subscriptions.unsubscribeFrontendMessage("TriggerFontDataLoad");
}
export type FontsManager = ReturnType<typeof createFontsManager>;

// Self-accepting HMR: tear down the old instance and re-create with the new module's code
import.meta.hot?.accept((newModule) => {
currentCleanup?.();
if (currentArgs) newModule?.createFontsManager(...currentArgs);
destroyFontsManager();
if (editorRef) newModule?.createFontsManager(editorRef);
});
21 changes: 9 additions & 12 deletions frontend/src/managers/hyperlink.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,24 @@
import type { Editor } from "@graphite/editor";

let currentCleanup: (() => void) | undefined;
let currentArgs: [Editor] | undefined;
let editorRef: Editor | undefined = undefined;

export function createHyperlinkManager(editor: Editor) {
currentArgs = [editor];
editorRef = editor;

// Subscribe to process backend event
editor.subscriptions.subscribeFrontendMessage("TriggerVisitLink", async (data) => {
window.open(data.url, "_blank", "noopener");
});
}

function destroy() {
editor.subscriptions.unsubscribeFrontendMessage("TriggerVisitLink");
}
export function destroyHyperlinkManager() {
const editor = editorRef;
if (!editor) return;

currentCleanup = destroy;
return { destroy };
editor.subscriptions.unsubscribeFrontendMessage("TriggerVisitLink");
}
export type HyperlinkManager = ReturnType<typeof createHyperlinkManager>;

// Self-accepting HMR: tear down the old instance and re-create with the new module's code
import.meta.hot?.accept((newModule) => {
currentCleanup?.();
if (currentArgs) newModule?.createHyperlinkManager(...currentArgs);
destroyHyperlinkManager();
if (editorRef) newModule?.createHyperlinkManager(editorRef);
});
Loading
Loading