Skip to content

Commit 774a356

Browse files
committed
Restructure and partially flatten managers/stores
1 parent 12b30a7 commit 774a356

32 files changed

Lines changed: 708 additions & 615 deletions

frontend/src/README.md

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,21 @@
44

55
Svelte components that build the Graphite editor GUI. These each contain a TypeScript section, a Svelte-templated HTML template section, and an SCSS stylesheet section. The aim is to avoid implementing much editor business logic here, just enough to make things interactive and communicate to the backend where the real business logic should occur.
66

7-
## I/O managers: `io-managers/`
7+
## Managers: `managers/`
88

9-
TypeScript files which manage the input/output of browser APIs and link this functionality with the editor backend. These files subscribe to backend events to execute JS APIs, and in response to these APIs or user interactions, they may call functions into the backend (defined in `/frontend/wasm/editor_api.rs`).
9+
TypeScript files which manage the input/output of browser APIs and link this functionality with the editor backend. These files subscribe to backend messages to execute JS APIs, and in response to these APIs or user interactions, they may call functions into the backend (defined in `/frontend/wasm/editor_api.rs`).
1010

11-
Each I/O manager is a self-contained module where one instance is created in `Editor.svelte` when it's mounted to the DOM at app startup.
11+
Each manager module exports a factory function (e.g. `createClipboardManager(editor)`) that sets up message subscriptions and returns a `{ destroy }` object. In `Editor.svelte`, each manager is created at startup and its `destroy()` method is called on unmount to clean up subscriptions and side-effects (e.g. event listeners). Managers use self-accepting HMR to tear down and re-create with updated code during development.
1212

13-
During development when HMR (hot-module replacement) occurs, these are also unmounted to clean up after themselves, so they can be mounted again with the updated code. Therefore, any side-effects that these managers cause (e.g. adding event listeners to the page) need a destructor function that cleans them up. The destructor function, when applicable, is returned by the module and automatically called in `Editor.svelte` on unmount.
13+
## Stores: `stores/`
1414

15-
## State providers: `state-providers/`
15+
TypeScript files which provide reactive state to Svelte components. Each module persists a Svelte writable store at module level (surviving HMR via `import.meta.hot.data`) and exports a factory function (e.g. `createDialogStore(editor)`) that sets up backend message subscriptions and returns an object containing the store's `subscribe` method, any action methods for components to call, and a `destroy` method.
1616

17-
TypeScript files which provide reactive state and importable functions to Svelte components. Each module defines a Svelte writable store `const { subscribe, update } = writable({ .. });` and exports the `subscribe` method from the module in the returned object. Other functions may also be defined in the module and exported after `subscribe`, which provide a way for Svelte components to call functions to manipulate the state.
17+
In `Editor.svelte`, each store is created and passed to Svelte's `setContext()`. Components access stores via `getContext<DialogStore>("dialog")` and use the `subscribe` method for reactive state and action methods (like `createCrashDialog()`) to trigger state changes.
1818

19-
In `Editor.svelte`, an instance of each of these are given to Svelte's `setContext()` function. This allows any component to access the state provider instance using `const exampleStateProvider = getContext<ExampleStateProvider>("exampleStateProvider");`.
19+
## *Managers vs. stores*
2020

21-
## *I/O managers vs. state providers*
22-
23-
*Some state providers, similarly to I/O managers, may subscribe to backend events, call functions from `editor_api.rs` into the backend, and interact with browser APIs and user input. The difference is that state providers are meant to be made available to components via `getContext()` to use them for reactive state, while I/O managers are meant to be self-contained systems that operate for the lifetime of the application and aren't touched by Svelte components.*
21+
*Both managers and stores subscribe to backend messages and may interact with browser APIs. The difference is that stores expose reactive state to components via `setContext()`/`getContext()`, while managers are self-contained systems that operate for the lifetime of the application and aren't accessed by Svelte components.*
2422

2523
## Utility functions: `utility-functions/`
2624

@@ -30,7 +28,7 @@ TypeScript files which define and `export` individual helper functions for use e
3028

3129
Instantiates the Wasm and editor backend instances. The function `initWasm()` asynchronously constructs and initializes an instance of the Wasm bindings JS module provided by wasm-bindgen/wasm-pack. The function `createEditor()` constructs an instance of the editor backend. In theory there could be multiple editor instances sharing the same Wasm module instance. The function returns an object where `raw` is the Wasm memory, `handle` provides access to callable backend functions, and `subscriptions` is the subscription router (described below).
3230

33-
`initWasm()` occurs in `main.ts` right before the Svelte application is mounted, then `createEditor()` is run in `Editor.svelte` during the Svelte app's creation. Similarly to the state providers described above, the editor is given via `setContext()` so other components can get it via `getContext` and call functions on `editor.handle` or `editor.subscriptions`.
31+
`initWasm()` occurs in `main.ts` right before the Svelte application is mounted, then `createEditor()` is run in `Editor.svelte` during the Svelte app's creation. Similarly to the stores described above, the editor is given via `setContext()` so other components can get it via `getContext` and call functions on `editor.handle` or `editor.subscriptions`.
3432

3533
## Subscription router: `subscription-router.ts`
3634

@@ -42,7 +40,7 @@ The entry point for the Svelte application.
4240

4341
## Editor base instance: `Editor.svelte`
4442

45-
This is where we define global CSS style rules, create/destroy the editor instance, construct/destruct the I/O managers, and construct and `setContext()` the state providers.
43+
This is where we define global CSS style rules, construct all stores and managers with the editor instance, set store contexts for component access, and clean up all `destroy()` methods on unmount.
4644

4745
## Global type augmentations: `global.d.ts`
4846

frontend/src/components/Editor.svelte

Lines changed: 35 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,51 +2,47 @@
22
import { onMount, onDestroy, setContext } from "svelte";
33
44
import type { Editor } from "@graphite/editor";
5-
import { createClipboardManager } from "@graphite/io-managers/clipboard";
6-
import { createHyperlinkManager } from "@graphite/io-managers/hyperlink";
7-
import { createInputManager } from "@graphite/io-managers/input";
8-
import { createLocalizationManager } from "@graphite/io-managers/localization";
9-
import { createPanicManager } from "@graphite/io-managers/panic";
10-
import { createPersistenceManager } from "@graphite/io-managers/persistence";
11-
import { createAppWindowState } from "@graphite/state-providers/app-window";
12-
import { createDialogState } from "@graphite/state-providers/dialog";
13-
import { createDocumentState } from "@graphite/state-providers/document";
14-
import { createFontsManager } from "/src/io-managers/fonts";
15-
import { createFullscreenState } from "@graphite/state-providers/fullscreen";
16-
import { createNodeGraphState } from "@graphite/state-providers/node-graph";
17-
import { createPortfolioState } from "@graphite/state-providers/portfolio";
18-
import { createTooltipState } from "@graphite/state-providers/tooltip";
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";
1919
2020
import MainWindow from "@graphite/components/window/MainWindow.svelte";
2121
2222
// Graphite Wasm editor
2323
export let editor: Editor;
2424
setContext("editor", editor);
2525
26-
// State provider systems
27-
let dialog = createDialogState(editor);
28-
setContext("dialog", dialog);
29-
let tooltip = createTooltipState(editor);
30-
setContext("tooltip", tooltip);
31-
let document = createDocumentState(editor);
32-
setContext("document", document);
33-
let fullscreen = createFullscreenState(editor);
34-
setContext("fullscreen", fullscreen);
35-
let nodeGraph = createNodeGraphState(editor);
36-
setContext("nodeGraph", nodeGraph);
37-
let portfolio = createPortfolioState(editor);
38-
setContext("portfolio", portfolio);
39-
let appWindow = createAppWindowState(editor);
40-
setContext("appWindow", appWindow);
41-
42-
// Initialize managers, which are isolated systems that subscribe to backend messages to link them to browser API functionality (like JS events, IndexedDB, etc.)
43-
const clipboardManagerDestructor = createClipboardManager(editor);
44-
const hyperlinkManagerDestructor = createHyperlinkManager(editor);
45-
const localizationManagerDestructor = createLocalizationManager(editor);
46-
const panicManagerDestructor = createPanicManager(editor, dialog);
47-
const persistenceManagerDestructor = createPersistenceManager(editor, portfolio);
48-
const fontsManagerDestructor = createFontsManager(editor);
49-
const inputManagerDestructor = createInputManager(editor, dialog, portfolio, document, fullscreen);
26+
const stores = {
27+
dialog: createDialogStore(editor),
28+
tooltip: createTooltipStore(editor),
29+
document: createDocumentStore(editor),
30+
fullscreen: createFullscreenStore(editor),
31+
nodeGraph: createNodeGraphStore(editor),
32+
portfolio: createPortfolioStore(editor),
33+
appWindow: createAppWindowStore(editor),
34+
};
35+
Object.entries(stores).forEach(([key, store]) => setContext(key, store));
36+
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+
};
5046
5147
onMount(() => {
5248
// Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready.
@@ -55,22 +51,7 @@
5551
});
5652
5753
onDestroy(() => {
58-
// Call the destructor for each manager
59-
clipboardManagerDestructor();
60-
hyperlinkManagerDestructor();
61-
localizationManagerDestructor();
62-
panicManagerDestructor();
63-
persistenceManagerDestructor();
64-
fontsManagerDestructor();
65-
inputManagerDestructor();
66-
// Clean up state provider subscriptions and event listeners
67-
dialog.destroy();
68-
tooltip.destroy();
69-
document.destroy();
70-
fullscreen.destroy();
71-
nodeGraph.destroy();
72-
portfolio.destroy();
73-
appWindow.destroy();
54+
[...Object.values(stores), ...Object.values(managers)].forEach(({ destroy }) => destroy());
7455
});
7556
</script>
7657

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
import { isPlatformNative } from "@graphite/../wasm/pkg/graphite_wasm";
55
import type { FillChoice, MenuDirection, Color } from "@graphite/../wasm/pkg/graphite_wasm";
6-
import type { TooltipState } from "@graphite/state-providers/tooltip";
6+
import type { TooltipStore } from "@graphite/stores/tooltip";
77
import {
88
contrastingOutlineFactor,
99
fillChoiceColor,
@@ -22,7 +22,6 @@
2222
gradientFirstColor,
2323
} from "@graphite/utility-functions/colors";
2424
import type { HSV, RGB } from "@graphite/utility-functions/colors";
25-
import { clamp } from "@graphite/utility-functions/math";
2625
2726
import FloatingMenu, { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
2827
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
@@ -57,7 +56,7 @@
5756
];
5857
5958
const dispatch = createEventDispatcher<{ colorOrGradient: FillChoice; startHistoryTransaction: undefined; commitHistoryTransaction: undefined }>();
60-
const tooltip = getContext<TooltipState>("tooltip");
59+
const tooltip = getContext<TooltipStore>("tooltip");
6160
6261
export let colorOrGradient: FillChoice;
6362
export let allowNone = false;
@@ -438,6 +437,10 @@
438437
setOldHSVA(hsv.h, hsv.s, hsv.v, color.alpha, false);
439438
}
440439
440+
function clamp(value: number, min = 0, max = 1): number {
441+
return Math.max(min, Math.min(value, max));
442+
}
443+
441444
export function div(): HTMLDivElement | undefined {
442445
return self?.div();
443446
}

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
<script lang="ts">
22
import { getContext, onMount } from "svelte";
33
4-
import { githubUrl } from "@graphite/io-managers/panic";
5-
import { wipeDocuments } from "@graphite/io-managers/persistence";
6-
7-
import type { DialogState } from "@graphite/state-providers/dialog";
4+
import { wipeDocuments } from "@graphite/managers/persistence";
5+
import type { DialogStore } from "@graphite/stores/dialog";
6+
import { crashReportUrl } from "/src/utility-functions/crash-report";
87
98
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
109
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
@@ -14,7 +13,7 @@
1413
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
1514
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
1615
17-
const dialog = getContext<DialogState>("dialog");
16+
const dialog = getContext<DialogStore>("dialog");
1817
1918
let self: FloatingMenu | undefined;
2019
@@ -43,7 +42,7 @@
4342
<div class="widget-layout details">
4443
<div class="widget-span row"><TextLabel bold={true}>The editor crashed — sorry about that</TextLabel></div>
4544
<div class="widget-span row"><TextLabel>Please report this by filing an issue on GitHub:</TextLabel></div>
46-
<div class="widget-span row"><TextButton label="Report Bug" icon="Warning" flush={true} action={() => window.open(githubUrl($dialog.panicDetails), "_blank")} /></div>
45+
<div class="widget-span row"><TextButton label="Report Bug" icon="Warning" flush={true} action={() => window.open(crashReportUrl($dialog.panicDetails), "_blank")} /></div>
4746
<div class="widget-span row"><TextLabel multiline={true}>Reload the editor to continue. If this occurs<br />immediately on repeated reloads, clear storage:</TextLabel></div>
4847
<div class="widget-span row">
4948
<TextButton

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
import { SvelteMap } from "svelte/reactivity";
44
55
import type { FrontendNodeType } from "@graphite/../wasm/pkg/graphite_wasm";
6-
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
6+
import type { NodeGraphStore } from "@graphite/stores/node-graph";
77
88
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
99
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
1010
import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte";
1111
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
1212
1313
const dispatch = createEventDispatcher<{ selectNodeType: string }>();
14-
const nodeGraph = getContext<NodeGraphState>("nodeGraph");
14+
const nodeGraph = getContext<NodeGraphStore>("nodeGraph");
1515
1616
// Content
1717
export let disabled = false;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
44
import type { LabeledShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
55
import type { Editor } from "@graphite/editor";
6-
import type { TooltipState } from "@graphite/state-providers/tooltip";
6+
import type { TooltipStore } from "@graphite/stores/tooltip";
77
88
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
99
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
1010
import ShortcutLabel from "@graphite/components/widgets/labels/ShortcutLabel.svelte";
1111
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
1212
13-
const tooltip = getContext<TooltipState>("tooltip");
13+
const tooltip = getContext<TooltipStore>("tooltip");
1414
const editor = getContext<Editor>("editor");
1515
1616
let self: FloatingMenu | undefined;

frontend/src/components/panels/Document.svelte

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
44
import type { Color, MenuDirection, MouseCursorIcon } from "@graphite/../wasm/pkg/graphite_wasm";
55
import type { Editor } from "@graphite/editor";
6-
import type { AppWindowState } from "@graphite/state-providers/app-window";
7-
import type { DocumentState } from "@graphite/state-providers/document";
6+
import type { AppWindowStore } from "@graphite/stores/app-window";
7+
import type { DocumentStore } from "@graphite/stores/document";
88
import type { MessageBody } from "@graphite/subscription-router";
99
import { fillChoiceColor, createColor } from "@graphite/utility-functions/colors";
1010
import { pasteFile } from "@graphite/utility-functions/files";
@@ -27,8 +27,8 @@
2727
let gradientStopPicker: ColorPicker | undefined;
2828
2929
const editor = getContext<Editor>("editor");
30-
const appWindow = getContext<AppWindowState>("appWindow");
31-
const document = getContext<DocumentState>("document");
30+
const appWindow = getContext<AppWindowStore>("appWindow");
31+
const document = getContext<DocumentStore>("document");
3232
3333
// Interactive text editing
3434
let textInput: undefined | HTMLDivElement = undefined;

frontend/src/components/panels/Layers.svelte

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
55
import type { LayerPanelEntry, LayerStructureEntry, Layout } from "@graphite/../wasm/pkg/graphite_wasm";
66
import type { Editor } from "@graphite/editor";
7-
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
8-
import type { TooltipState } from "@graphite/state-providers/tooltip";
7+
import type { NodeGraphStore } from "@graphite/stores/node-graph";
8+
import type { TooltipStore } from "@graphite/stores/tooltip";
99
import { pasteFile } from "@graphite/utility-functions/files";
1010
import { operatingSystem } from "@graphite/utility-functions/platform";
1111
import { patchLayout } from "@graphite/utility-functions/widgets";
@@ -42,8 +42,8 @@
4242
};
4343
4444
const editor = getContext<Editor>("editor");
45-
const nodeGraph = getContext<NodeGraphState>("nodeGraph");
46-
const tooltip = getContext<TooltipState>("tooltip");
45+
const nodeGraph = getContext<NodeGraphStore>("nodeGraph");
46+
const tooltip = getContext<TooltipStore>("tooltip");
4747
4848
let list: LayoutCol | undefined;
4949

0 commit comments

Comments
 (0)