Skip to content

Commit 2e2c4fe

Browse files
authored
Restructure frontend TS files so managers/stores export destructors instead of returning them from their constructors (#3919)
* Replace parameter passing with getContext and extract destroy functions to module-level exports * Resend layouts from Rust when editor is re-mounted on HMR * Code review
1 parent 124b17f commit 2e2c4fe

File tree

21 files changed

+898
-812
lines changed

21 files changed

+898
-812
lines changed

editor/src/messages/layout/layout_message.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub enum LayoutMessage {
88
layout_target: LayoutTarget,
99
widget_id: WidgetId,
1010
},
11+
ResendAllLayouts,
1112
SendLayout {
1213
layout: Layout,
1314
layout_target: LayoutTarget,

editor/src/messages/layout/layout_message_handler.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@ impl MessageHandler<LayoutMessage, LayoutMessageContext<'_>> for LayoutMessageHa
3333
// Resend that diff
3434
self.send_diff(vec![diff], layout_target, responses, action_input_mapping);
3535
}
36+
LayoutMessage::ResendAllLayouts => {
37+
// Collect non-empty layouts and their indices, then clear the stored copies so diffs compute as full re-sends
38+
let layouts_to_resend: Vec<_> = self
39+
.layouts
40+
.iter_mut()
41+
.enumerate()
42+
.filter(|(_, layout)| !layout.0.is_empty())
43+
.map(|(i, layout)| (LayoutTarget::from(i as u8), std::mem::take(layout)))
44+
.collect();
45+
46+
for (layout_target, layout) in layouts_to_resend {
47+
self.diff_and_send_layout_to_frontend(layout_target, layout, responses, action_input_mapping);
48+
}
49+
}
3650
LayoutMessage::SendLayout { layout, layout_target } => {
3751
self.diff_and_send_layout_to_frontend(layout_target, layout, responses, action_input_mapping);
3852
}

editor/src/messages/layout/utility_types/layout_widget.rs

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,30 @@ impl core::fmt::Display for WidgetId {
2020
}
2121
}
2222

23-
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
24-
#[derive(PartialEq, Clone, Debug, Hash, Eq, Copy, serde::Serialize, serde::Deserialize)]
25-
#[repr(u8)]
26-
pub enum LayoutTarget {
23+
macro_rules! define_layout_target {
24+
($($(#[$attr:meta])* $variant:ident),* $(,)?) => {
25+
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
26+
#[derive(PartialEq, Clone, Debug, Hash, Eq, Copy, serde::Serialize, serde::Deserialize)]
27+
#[repr(u8)]
28+
pub enum LayoutTarget {
29+
$($(#[$attr])* $variant,)*
30+
// KEEP THIS ENUM LAST
31+
// This is a marker that is used to define an array that is used to hold widgets
32+
#[serde(skip)]
33+
_LayoutTargetLength,
34+
}
35+
36+
impl From<u8> for LayoutTarget {
37+
fn from(value: u8) -> Self {
38+
match value {
39+
$(x if x == Self::$variant as u8 => Self::$variant,)*
40+
_ => panic!("Invalid LayoutTarget discriminant: {value}"),
41+
}
42+
}
43+
}
44+
};
45+
}
46+
define_layout_target!(
2747
/// The spreadsheet panel allows for the visualisation of data in the graph.
2848
DataPanel,
2949
/// Contains the action buttons at the bottom of the dialog. Must be shown with the `FrontendMessage::DisplayDialog` message.
@@ -58,12 +78,7 @@ pub enum LayoutTarget {
5878
WelcomeScreenButtons,
5979
/// The color swatch for the working colors and a flip and reset button found at the bottom of the tool shelf.
6080
WorkingColors,
61-
62-
// KEEP THIS ENUM LAST
63-
// This is a marker that is used to define an array that is used to hold widgets
64-
#[serde(skip)]
65-
_LayoutTargetLength,
66-
}
81+
);
6782

6883
/// For use by structs that define a UI widget layout by implementing the layout() function belonging to this trait.
6984
/// 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.

frontend/eslint.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export default defineConfig([
7070
ignoreRestSiblings: true,
7171
},
7272
],
73+
"@typescript-eslint/no-non-null-assertion": "error",
7374
"@typescript-eslint/consistent-type-imports": "error",
7475
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
7576
"@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "never" }],

frontend/src/components/Editor.svelte

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,20 @@
22
import { onMount, onDestroy, setContext } from "svelte";
33
44
import type { Editor } from "@graphite/editor";
5-
import { createClipboardManager } from "@graphite/managers/clipboard";
6-
import { createFontsManager } from "@graphite/managers/fonts";
7-
import { createHyperlinkManager } from "@graphite/managers/hyperlink";
8-
import { createInputManager } from "@graphite/managers/input";
9-
import { createLocalizationManager } from "@graphite/managers/localization";
10-
import { createPanicManager } from "@graphite/managers/panic";
11-
import { createPersistenceManager } from "@graphite/managers/persistence";
12-
import { createAppWindowStore } from "@graphite/stores/app-window";
13-
import { createDialogStore } from "@graphite/stores/dialog";
14-
import { createDocumentStore } from "@graphite/stores/document";
15-
import { createFullscreenStore } from "@graphite/stores/fullscreen";
16-
import { createNodeGraphStore } from "@graphite/stores/node-graph";
17-
import { createPortfolioStore } from "@graphite/stores/portfolio";
18-
import { createTooltipStore } from "@graphite/stores/tooltip";
5+
import { createClipboardManager, destroyClipboardManager } from "@graphite/managers/clipboard";
6+
import { createFontsManager, destroyFontsManager } from "@graphite/managers/fonts";
7+
import { createHyperlinkManager, destroyHyperlinkManager } from "@graphite/managers/hyperlink";
8+
import { createInputManager, destroyInputManager } from "@graphite/managers/input";
9+
import { createLocalizationManager, destroyLocalizationManager } from "@graphite/managers/localization";
10+
import { createPanicManager, destroyPanicManager } from "@graphite/managers/panic";
11+
import { createPersistenceManager, destroyPersistenceManager } from "@graphite/managers/persistence";
12+
import { createAppWindowStore, destroyAppWindowStore } from "@graphite/stores/app-window";
13+
import { createDialogStore, destroyDialogStore } from "@graphite/stores/dialog";
14+
import { createDocumentStore, destroyDocumentStore } from "@graphite/stores/document";
15+
import { createFullscreenStore, destroyFullscreenStore } from "@graphite/stores/fullscreen";
16+
import { createNodeGraphStore, destroyNodeGraphStore } from "@graphite/stores/node-graph";
17+
import { createPortfolioStore, destroyPortfolioStore } from "@graphite/stores/portfolio";
18+
import { createTooltipStore, destroyTooltipStore } from "@graphite/stores/tooltip";
1919
2020
import MainWindow from "@graphite/components/window/MainWindow.svelte";
2121
@@ -34,24 +34,41 @@
3434
};
3535
Object.entries(stores).forEach(([key, store]) => setContext(key, store));
3636
37-
const managers = {
38-
clipboard: createClipboardManager(editor),
39-
hyperlink: createHyperlinkManager(editor),
40-
localization: createLocalizationManager(editor),
41-
panic: createPanicManager(editor),
42-
persistence: createPersistenceManager(editor, stores.portfolio),
43-
fonts: createFontsManager(editor),
44-
input: createInputManager(editor, stores.dialog, stores.portfolio, stores.document, stores.fullscreen),
45-
};
46-
4737
onMount(() => {
38+
createClipboardManager(editor);
39+
createHyperlinkManager(editor);
40+
createLocalizationManager(editor);
41+
createPanicManager(editor);
42+
createPersistenceManager(editor, stores.portfolio);
43+
createFontsManager(editor);
44+
createInputManager(editor, stores.dialog, stores.portfolio, stores.document);
45+
4846
// Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready.
4947
// The backend handles idempotency, so this is safe to call again during HMR re-mounts.
5048
editor.handle.initAfterFrontendReady();
49+
50+
// Re-send all UI layouts from Rust so the frontend has them after an HMR re-mount
51+
editor.handle.resendAllLayouts();
5152
});
5253
5354
onDestroy(() => {
54-
[...Object.values(stores), ...Object.values(managers)].forEach(({ destroy }) => destroy());
55+
// Stores
56+
destroyDialogStore();
57+
destroyTooltipStore();
58+
destroyDocumentStore();
59+
destroyFullscreenStore();
60+
destroyNodeGraphStore();
61+
destroyPortfolioStore();
62+
destroyAppWindowStore();
63+
64+
// Managers
65+
destroyClipboardManager();
66+
destroyHyperlinkManager();
67+
destroyLocalizationManager();
68+
destroyPanicManager();
69+
destroyPersistenceManager();
70+
destroyFontsManager();
71+
destroyInputManager();
5572
});
5673
</script>
5774

frontend/src/components/floating-menus/Dialog.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
import { wipeDocuments } from "@graphite/managers/persistence";
55
import type { DialogStore } from "@graphite/stores/dialog";
6-
import { crashReportUrl } from "/src/utility-functions/crash-report";
6+
import { crashReportUrl } from "@graphite/utility-functions/crash-report";
77
88
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
99
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";

frontend/src/managers/clipboard.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,32 @@
11
import type { Editor } from "@graphite/editor";
22

3-
let currentCleanup: (() => void) | undefined;
4-
let currentArgs: [Editor] | undefined;
3+
let editorRef: Editor | undefined = undefined;
54

65
export function createClipboardManager(editor: Editor) {
7-
currentArgs = [editor];
6+
editorRef = editor;
87

9-
// Subscribe to process backend event
108
editor.subscriptions.subscribeFrontendMessage("TriggerClipboardWrite", (data) => {
119
// If the Clipboard API is supported in the browser, copy text to the clipboard
1210
navigator.clipboard?.writeText?.(data.content);
1311
});
12+
1413
editor.subscriptions.subscribeFrontendMessage("TriggerSelectionRead", async (data) => {
1514
editor.handle.readSelection(readAtCaret(data.cut), data.cut);
1615
});
16+
1717
editor.subscriptions.subscribeFrontendMessage("TriggerSelectionWrite", async (data) => {
1818
insertAtCaret(data.content);
1919
});
20+
}
2021

21-
function destroy() {
22-
editor.subscriptions.unsubscribeFrontendMessage("TriggerClipboardWrite");
23-
editor.subscriptions.unsubscribeFrontendMessage("TriggerSelectionRead");
24-
editor.subscriptions.unsubscribeFrontendMessage("TriggerSelectionWrite");
25-
}
22+
export function destroyClipboardManager() {
23+
const editor = editorRef;
24+
if (!editor) return;
2625

27-
currentCleanup = destroy;
28-
return { destroy };
26+
editor.subscriptions.unsubscribeFrontendMessage("TriggerClipboardWrite");
27+
editor.subscriptions.unsubscribeFrontendMessage("TriggerSelectionRead");
28+
editor.subscriptions.unsubscribeFrontendMessage("TriggerSelectionWrite");
2929
}
30-
export type ClipboardManager = ReturnType<typeof createClipboardManager>;
3130

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

113112
// Self-accepting HMR: tear down the old instance and re-create with the new module's code
114113
import.meta.hot?.accept((newModule) => {
115-
currentCleanup?.();
116-
if (currentArgs) newModule?.createClipboardManager(...currentArgs);
114+
destroyClipboardManager();
115+
if (editorRef) newModule?.createClipboardManager(editorRef);
117116
});

frontend/src/managers/fonts.ts

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,16 @@ type ApiResponse = { family: string; variants: string[]; files: Record<string, s
44

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

7-
let currentCleanup: (() => void) | undefined;
8-
let currentArgs: [Editor] | undefined;
7+
let editorRef: Editor | undefined = undefined;
8+
let abortController: AbortController | undefined = undefined;
99

1010
export function createFontsManager(editor: Editor) {
11-
currentArgs = [editor];
12-
const abortController = new AbortController();
11+
editorRef = editor;
12+
abortController = new AbortController();
1313

14-
// Subscribe to process backend events
1514
editor.subscriptions.subscribeFrontendMessage("TriggerFontCatalogLoad", async () => {
1615
try {
17-
const response = await fetch(FONT_LIST_API, { signal: abortController.signal });
16+
const response = await fetch(FONT_LIST_API, abortController ? { signal: abortController.signal } : undefined);
1817
if (!response.ok) throw new Error(`Font catalog request failed with status ${response.status}`);
1918
const fontListResponse: { items: ApiResponse } = await response.json();
2019
const fontListData = fontListResponse.items;
@@ -42,7 +41,7 @@ export function createFontsManager(editor: Editor) {
4241

4342
try {
4443
if (!data.url) throw new Error("No URL provided for font data load");
45-
const response = await fetch(data.url, { signal: abortController.signal });
44+
const response = await fetch(data.url, abortController ? { signal: abortController.signal } : undefined);
4645
if (!response.ok) throw new Error(`Font data request failed with status ${response.status}`);
4746
const buffer = await response.arrayBuffer();
4847
const bytes = new Uint8Array(buffer);
@@ -54,20 +53,19 @@ export function createFontsManager(editor: Editor) {
5453
console.error("Failed to load font:", error);
5554
}
5655
});
56+
}
5757

58-
function destroy() {
59-
abortController.abort();
60-
editor.subscriptions.unsubscribeFrontendMessage("TriggerFontCatalogLoad");
61-
editor.subscriptions.unsubscribeFrontendMessage("TriggerFontDataLoad");
62-
}
58+
export function destroyFontsManager() {
59+
const editor = editorRef;
60+
if (!editor) return;
6361

64-
currentCleanup = destroy;
65-
return { destroy };
62+
abortController?.abort();
63+
editor.subscriptions.unsubscribeFrontendMessage("TriggerFontCatalogLoad");
64+
editor.subscriptions.unsubscribeFrontendMessage("TriggerFontDataLoad");
6665
}
67-
export type FontsManager = ReturnType<typeof createFontsManager>;
6866

6967
// Self-accepting HMR: tear down the old instance and re-create with the new module's code
7068
import.meta.hot?.accept((newModule) => {
71-
currentCleanup?.();
72-
if (currentArgs) newModule?.createFontsManager(...currentArgs);
69+
destroyFontsManager();
70+
if (editorRef) newModule?.createFontsManager(editorRef);
7371
});

frontend/src/managers/hyperlink.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,24 @@
11
import type { Editor } from "@graphite/editor";
22

3-
let currentCleanup: (() => void) | undefined;
4-
let currentArgs: [Editor] | undefined;
3+
let editorRef: Editor | undefined = undefined;
54

65
export function createHyperlinkManager(editor: Editor) {
7-
currentArgs = [editor];
6+
editorRef = editor;
87

9-
// Subscribe to process backend event
108
editor.subscriptions.subscribeFrontendMessage("TriggerVisitLink", async (data) => {
119
window.open(data.url, "_blank", "noopener");
1210
});
11+
}
1312

14-
function destroy() {
15-
editor.subscriptions.unsubscribeFrontendMessage("TriggerVisitLink");
16-
}
13+
export function destroyHyperlinkManager() {
14+
const editor = editorRef;
15+
if (!editor) return;
1716

18-
currentCleanup = destroy;
19-
return { destroy };
17+
editor.subscriptions.unsubscribeFrontendMessage("TriggerVisitLink");
2018
}
21-
export type HyperlinkManager = ReturnType<typeof createHyperlinkManager>;
2219

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

0 commit comments

Comments
 (0)