mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-29 08:08:12 +08:00
Fix frontend HMR so the page doesn't break upon saving TS files and restructure frontend architecture (#3871)
* Clean up component setup/tear-down side effects * Clean up more component setup/tear-down side effects * Remove nonfunctional debouncer * Clean up even more component setup/tear-down side effects * Reuse backend state * Fix HMR for IO Managers and for State Providers * Rename IO Managers -> Managers and State Providers -> Stores * Restructure and partially flatten managers/stores * Code review fixes * Review fixes
This commit is contained in:
@@ -2,20 +2,20 @@
|
||||
import { onMount, onDestroy, setContext } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import { createClipboardManager } from "@graphite/io-managers/clipboard";
|
||||
import { createHyperlinkManager } from "@graphite/io-managers/hyperlink";
|
||||
import { createInputManager } from "@graphite/io-managers/input";
|
||||
import { createLocalizationManager } from "@graphite/io-managers/localization";
|
||||
import { createPanicManager } from "@graphite/io-managers/panic";
|
||||
import { createPersistenceManager } from "@graphite/io-managers/persistence";
|
||||
import { createAppWindowState } from "@graphite/state-providers/app-window";
|
||||
import { createDialogState } from "@graphite/state-providers/dialog";
|
||||
import { createDocumentState } from "@graphite/state-providers/document";
|
||||
import { createFontsManager } from "/src/io-managers/fonts";
|
||||
import { createFullscreenState } from "@graphite/state-providers/fullscreen";
|
||||
import { createNodeGraphState } from "@graphite/state-providers/node-graph";
|
||||
import { createPortfolioState } from "@graphite/state-providers/portfolio";
|
||||
import { createTooltipState } from "@graphite/state-providers/tooltip";
|
||||
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 MainWindow from "@graphite/components/window/MainWindow.svelte";
|
||||
|
||||
@@ -23,39 +23,35 @@
|
||||
export let editor: Editor;
|
||||
setContext("editor", editor);
|
||||
|
||||
// State provider systems
|
||||
let dialog = createDialogState(editor);
|
||||
setContext("dialog", dialog);
|
||||
let tooltip = createTooltipState(editor);
|
||||
setContext("tooltip", tooltip);
|
||||
let document = createDocumentState(editor);
|
||||
setContext("document", document);
|
||||
let fullscreen = createFullscreenState(editor);
|
||||
setContext("fullscreen", fullscreen);
|
||||
let nodeGraph = createNodeGraphState(editor);
|
||||
setContext("nodeGraph", nodeGraph);
|
||||
let portfolio = createPortfolioState(editor);
|
||||
setContext("portfolio", portfolio);
|
||||
let appWindow = createAppWindowState(editor);
|
||||
setContext("appWindow", appWindow);
|
||||
const stores = {
|
||||
dialog: createDialogStore(editor),
|
||||
tooltip: createTooltipStore(editor),
|
||||
document: createDocumentStore(editor),
|
||||
fullscreen: createFullscreenStore(editor),
|
||||
nodeGraph: createNodeGraphStore(editor),
|
||||
portfolio: createPortfolioStore(editor),
|
||||
appWindow: createAppWindowStore(editor),
|
||||
};
|
||||
Object.entries(stores).forEach(([key, store]) => setContext(key, store));
|
||||
|
||||
// Initialize managers, which are isolated systems that subscribe to backend messages to link them to browser API functionality (like JS events, IndexedDB, etc.)
|
||||
createClipboardManager(editor);
|
||||
createHyperlinkManager(editor);
|
||||
createLocalizationManager(editor);
|
||||
createPanicManager(editor, dialog);
|
||||
createPersistenceManager(editor, portfolio);
|
||||
createFontsManager(editor);
|
||||
let inputManagerDestructor = createInputManager(editor, dialog, portfolio, document, fullscreen);
|
||||
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(() => {
|
||||
// Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready
|
||||
// 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();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
// Call the destructor for each manager
|
||||
inputManagerDestructor();
|
||||
[...Object.values(stores), ...Object.values(managers)].forEach(({ destroy }) => destroy());
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { isPlatformNative } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { FillChoice, MenuDirection, Color } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { TooltipState } from "@graphite/state-providers/tooltip";
|
||||
import type { TooltipStore } from "@graphite/stores/tooltip";
|
||||
import {
|
||||
contrastingOutlineFactor,
|
||||
fillChoiceColor,
|
||||
@@ -22,7 +22,6 @@
|
||||
gradientFirstColor,
|
||||
} from "@graphite/utility-functions/colors";
|
||||
import type { HSV, RGB } from "@graphite/utility-functions/colors";
|
||||
import { clamp } from "@graphite/utility-functions/math";
|
||||
|
||||
import FloatingMenu, { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
@@ -57,7 +56,7 @@
|
||||
];
|
||||
|
||||
const dispatch = createEventDispatcher<{ colorOrGradient: FillChoice; startHistoryTransaction: undefined; commitHistoryTransaction: undefined }>();
|
||||
const tooltip = getContext<TooltipState>("tooltip");
|
||||
const tooltip = getContext<TooltipStore>("tooltip");
|
||||
|
||||
export let colorOrGradient: FillChoice;
|
||||
export let allowNone = false;
|
||||
@@ -438,6 +437,10 @@
|
||||
setOldHSVA(hsv.h, hsv.s, hsv.v, color.alpha, false);
|
||||
}
|
||||
|
||||
function clamp(value: number, min = 0, max = 1): number {
|
||||
return Math.max(min, Math.min(value, max));
|
||||
}
|
||||
|
||||
export function div(): HTMLDivElement | undefined {
|
||||
return self?.div();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from "svelte";
|
||||
|
||||
import { githubUrl } from "@graphite/io-managers/panic";
|
||||
import { wipeDocuments } from "@graphite/io-managers/persistence";
|
||||
|
||||
import type { DialogState } from "@graphite/state-providers/dialog";
|
||||
import { wipeDocuments } from "@graphite/managers/persistence";
|
||||
import type { DialogStore } from "@graphite/stores/dialog";
|
||||
import { crashReportUrl } from "/src/utility-functions/crash-report";
|
||||
|
||||
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
@@ -14,7 +13,7 @@
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
|
||||
|
||||
const dialog = getContext<DialogState>("dialog");
|
||||
const dialog = getContext<DialogStore>("dialog");
|
||||
|
||||
let self: FloatingMenu | undefined;
|
||||
|
||||
@@ -43,7 +42,7 @@
|
||||
<div class="widget-layout details">
|
||||
<div class="widget-span row"><TextLabel bold={true}>The editor crashed — sorry about that</TextLabel></div>
|
||||
<div class="widget-span row"><TextLabel>Please report this by filing an issue on GitHub:</TextLabel></div>
|
||||
<div class="widget-span row"><TextButton label="Report Bug" icon="Warning" flush={true} action={() => window.open(githubUrl($dialog.panicDetails), "_blank")} /></div>
|
||||
<div class="widget-span row"><TextButton label="Report Bug" icon="Warning" flush={true} action={() => window.open(crashReportUrl($dialog.panicDetails), "_blank")} /></div>
|
||||
<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>
|
||||
<div class="widget-span row">
|
||||
<TextButton
|
||||
|
||||
@@ -12,10 +12,7 @@
|
||||
|
||||
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
|
||||
const temporaryCanvas = document.createElement("canvas");
|
||||
temporaryCanvas.width = ZOOM_WINDOW_DIMENSIONS;
|
||||
temporaryCanvas.height = ZOOM_WINDOW_DIMENSIONS;
|
||||
|
||||
let temporaryCanvas: HTMLCanvasElement | undefined;
|
||||
let zoomPreviewCanvas: HTMLCanvasElement | undefined;
|
||||
|
||||
export let imageData: ImageData | undefined = undefined;
|
||||
@@ -31,6 +28,11 @@
|
||||
if (!zoomPreviewCanvas) return;
|
||||
const context = zoomPreviewCanvas.getContext("2d");
|
||||
|
||||
if (!temporaryCanvas) {
|
||||
temporaryCanvas = document.createElement("canvas");
|
||||
temporaryCanvas.width = ZOOM_WINDOW_DIMENSIONS;
|
||||
temporaryCanvas.height = ZOOM_WINDOW_DIMENSIONS;
|
||||
}
|
||||
const temporaryContext = temporaryCanvas.getContext("2d");
|
||||
|
||||
if (!imageData || !context || !temporaryContext) return;
|
||||
|
||||
@@ -47,6 +47,8 @@
|
||||
let reactiveEntries = entries;
|
||||
let highlighted: MenuListEntry | undefined = activeEntry;
|
||||
let virtualScrollingEntriesStart = 0;
|
||||
let keydownListenerAdded = false;
|
||||
let destroyed = false;
|
||||
|
||||
// `watchOpen` is called only when `open` is changed from outside this component
|
||||
$: watchOpen(open);
|
||||
@@ -67,11 +69,15 @@
|
||||
// TODO: The current approach is hacky and blocks the allowances for shortcuts like the key to open the browser's dev tools.
|
||||
onMount(async () => {
|
||||
await tick();
|
||||
if (open && !inNestedMenuList()) addEventListener("keydown", keydown);
|
||||
if (!destroyed && open && !inNestedMenuList() && !keydownListenerAdded) {
|
||||
addEventListener("keydown", keydown);
|
||||
keydownListenerAdded = true;
|
||||
}
|
||||
});
|
||||
onDestroy(async () => {
|
||||
await tick();
|
||||
if (!inNestedMenuList()) removeEventListener("keydown", keydown);
|
||||
onDestroy(() => {
|
||||
removeEventListener("keydown", keydown);
|
||||
// Set the destroyed status in the closure kept by the awaited `tick()` in `onMount` in case that delayed run occurs after the component is destroyed
|
||||
destroyed = true;
|
||||
});
|
||||
|
||||
function inNestedMenuList(): boolean {
|
||||
@@ -129,8 +135,13 @@
|
||||
}
|
||||
|
||||
function watchOpen(open: boolean) {
|
||||
if (open && !inNestedMenuList()) addEventListener("keydown", keydown);
|
||||
else if (!inNestedMenuList()) removeEventListener("keydown", keydown);
|
||||
if (open && !inNestedMenuList() && !keydownListenerAdded) {
|
||||
addEventListener("keydown", keydown);
|
||||
keydownListenerAdded = true;
|
||||
} else if (!open && !inNestedMenuList() && keydownListenerAdded) {
|
||||
removeEventListener("keydown", keydown);
|
||||
keydownListenerAdded = false;
|
||||
}
|
||||
|
||||
highlighted = activeEntry;
|
||||
dispatch("open", open);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { SvelteMap } from "svelte/reactivity";
|
||||
|
||||
import type { FrontendNodeType } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
|
||||
import type { NodeGraphStore } from "@graphite/stores/node-graph";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
|
||||
@@ -11,7 +11,7 @@
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher<{ selectNodeType: string }>();
|
||||
const nodeGraph = getContext<NodeGraphState>("nodeGraph");
|
||||
const nodeGraph = getContext<NodeGraphStore>("nodeGraph");
|
||||
|
||||
// Content
|
||||
export let disabled = false;
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
|
||||
import type { LabeledShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { TooltipState } from "@graphite/state-providers/tooltip";
|
||||
import type { TooltipStore } from "@graphite/stores/tooltip";
|
||||
|
||||
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import ShortcutLabel from "@graphite/components/widgets/labels/ShortcutLabel.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
||||
const tooltip = getContext<TooltipState>("tooltip");
|
||||
const tooltip = getContext<TooltipStore>("tooltip");
|
||||
const editor = getContext<Editor>("editor");
|
||||
|
||||
let self: FloatingMenu | undefined;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount, afterUpdate, createEventDispatcher, tick } from "svelte";
|
||||
import { onMount, onDestroy, afterUpdate, createEventDispatcher, tick } from "svelte";
|
||||
|
||||
import type { MenuDirection } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import { browserVersion } from "@graphite/utility-functions/platform";
|
||||
@@ -54,9 +54,11 @@
|
||||
// tell the floating menu content to use it as a min-width so the floating menu is at least the width of the parent element's floating menu spawner.
|
||||
// This is the opposite concern of the natural width measurement system, which gets the natural width of the floating menu content in order for the
|
||||
// spawner widget to optionally set its min-size to the floating menu's natural width.
|
||||
let containerResizeObserver = new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
||||
const containerResizeObserver = new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
||||
resizeObserverCallback(entries);
|
||||
});
|
||||
|
||||
let dialogResizeObserver: ResizeObserver | undefined;
|
||||
let wasOpen = open;
|
||||
let measuringOngoing = false;
|
||||
let measuringOngoingGuard = false;
|
||||
@@ -79,16 +81,7 @@
|
||||
|
||||
// Called only when `open` is changed from outside this component
|
||||
async function watchOpenChange(isOpen: boolean) {
|
||||
// Mitigate a Safari rendering bug which clips the floating menu extending beyond a scrollable container.
|
||||
// The bug is possibly related to <https://bugs.webkit.org/show_bug.cgi?id=160953>, but in our case it happens when `overflow` of a parent is `auto` rather than `hidden`.
|
||||
if (browserVersion().toLowerCase().includes("safari")) {
|
||||
const scrollable = self?.closest("[data-scrollable-x], [data-scrollable-y]");
|
||||
if (scrollable instanceof HTMLElement) {
|
||||
// The issue exists when the container is set to `overflow: auto` but fine when `overflow: hidden`. So this workaround temporarily sets
|
||||
// the scrollable container to `overflow: hidden`, thus removing the scrollbars and ability to scroll until the floating menu is closed.
|
||||
scrollable.style.overflow = isOpen ? "hidden" : "";
|
||||
}
|
||||
}
|
||||
setSafariScrollableOverflow(isOpen);
|
||||
|
||||
// Switching from closed to open
|
||||
if (isOpen && !wasOpen) {
|
||||
@@ -129,12 +122,22 @@
|
||||
wasOpen = isOpen;
|
||||
}
|
||||
|
||||
// Mitigate a Safari rendering bug which clips the floating menu extending beyond a scrollable container. The bug is possibly related to
|
||||
// <https://bugs.webkit.org/show_bug.cgi?id=160953>, but in our case it happens when `overflow` of a parent is `auto` rather than `hidden`.
|
||||
// The issue exists when the container is set to `overflow: auto` but fine when `overflow: hidden`. So this workaround temporarily sets
|
||||
// the scrollable container to `overflow: hidden`, thus removing the scrollbars and ability to scroll until the floating menu is closed.
|
||||
function setSafariScrollableOverflow(hidden: boolean) {
|
||||
if (!browserVersion().toLowerCase().includes("safari")) return;
|
||||
const scrollable = self?.closest("[data-scrollable-x], [data-scrollable-y]");
|
||||
if (scrollable instanceof HTMLElement) scrollable.style.overflow = hidden ? "hidden" : "";
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Measure the content and round up its width and height to the nearest even integer.
|
||||
// This solves antialiasing issues when the content isn't cleanly divisible by 2 and gets translated by (-50%, -50%) causing all its content to be blurry.
|
||||
const floatingMenuContentDiv = floatingMenuContent?.div?.();
|
||||
if (type === "Dialog" && floatingMenuContentDiv) {
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
dialogResizeObserver = new ResizeObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
const existingWidth = Number(floatingMenuContentDiv.style.getPropertyValue("--even-integer-subpixel-expansion-x"));
|
||||
const existingHeight = Number(floatingMenuContentDiv.style.getPropertyValue("--even-integer-subpixel-expansion-y"));
|
||||
@@ -153,10 +156,23 @@
|
||||
floatingMenuContentDiv.style.setProperty("--even-integer-subpixel-expansion-y", `${targetHeight - height}`);
|
||||
});
|
||||
});
|
||||
resizeObserver.observe(floatingMenuContentDiv);
|
||||
dialogResizeObserver.observe(floatingMenuContentDiv);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
containerResizeObserver.disconnect();
|
||||
dialogResizeObserver?.disconnect();
|
||||
window.removeEventListener("pointermove", pointerMoveHandler);
|
||||
window.removeEventListener("keydown", keyDownHandler);
|
||||
window.removeEventListener("pointerdown", pointerDownHandler);
|
||||
window.removeEventListener("pointerup", pointerUpHandler);
|
||||
window.removeEventListener("click", clickHandlerCapture, true);
|
||||
|
||||
// Revert Safari overflow workaround if the menu was open when destroyed
|
||||
if (open) setSafariScrollableOverflow(false);
|
||||
});
|
||||
|
||||
afterUpdate(() => {
|
||||
// Gets the client bounds of the elements and apply relevant styles to them.
|
||||
// TODO: Use DOM attribute bindings more whilst not causing recursive updates. Turning measuring on and off both causes the component to change,
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
|
||||
import type { Color, MenuDirection, MouseCursorIcon } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { AppWindowState } from "@graphite/state-providers/app-window";
|
||||
import type { DocumentState } from "@graphite/state-providers/document";
|
||||
import type { AppWindowStore } from "@graphite/stores/app-window";
|
||||
import type { DocumentStore } from "@graphite/stores/document";
|
||||
import type { MessageBody } from "@graphite/subscription-router";
|
||||
import { fillChoiceColor, createColor } from "@graphite/utility-functions/colors";
|
||||
import { pasteFile } from "@graphite/utility-functions/files";
|
||||
import { textInputCleanup } from "@graphite/utility-functions/keyboard-entry";
|
||||
import { rasterizeSVGCanvas } from "@graphite/utility-functions/rasterization";
|
||||
import { setupViewportResizeObserver, cleanupViewportResizeObserver } from "@graphite/utility-functions/viewports";
|
||||
import { setupViewportResizeObserver } from "@graphite/utility-functions/viewports";
|
||||
|
||||
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
|
||||
import EyedropperPreview, { ZOOM_WINDOW_DIMENSIONS } from "@graphite/components/floating-menus/EyedropperPreview.svelte";
|
||||
@@ -27,8 +27,8 @@
|
||||
let gradientStopPicker: ColorPicker | undefined;
|
||||
|
||||
const editor = getContext<Editor>("editor");
|
||||
const appWindow = getContext<AppWindowState>("appWindow");
|
||||
const document = getContext<DocumentState>("document");
|
||||
const appWindow = getContext<AppWindowStore>("appWindow");
|
||||
const document = getContext<DocumentStore>("document");
|
||||
|
||||
// Interactive text editing
|
||||
let textInput: undefined | HTMLDivElement = undefined;
|
||||
@@ -74,6 +74,10 @@
|
||||
let canvasHeight: number | undefined = undefined;
|
||||
|
||||
let devicePixelRatio: number | undefined;
|
||||
let removeUpdatePixelRatio: (() => void) | undefined;
|
||||
let viewportResizeObserver: ResizeObserver | undefined;
|
||||
let cleanupViewportResizeObserver: (() => void) | undefined;
|
||||
let addedFontFaces: FontFace[] = [];
|
||||
|
||||
// Dimension is rounded up to the nearest even number because resizing is centered, and dividing an odd number by 2 for centering causes antialiasing
|
||||
$: canvasWidthRoundedToEven = canvasWidth && (canvasWidth % 2 === 1 ? canvasWidth + 1 : canvasWidth);
|
||||
@@ -375,7 +379,9 @@
|
||||
|
||||
if (data.fontData.length > 0 && data.fontData.buffer instanceof ArrayBuffer) {
|
||||
const fontView = new Uint8Array(data.fontData.buffer, data.fontData.byteOffset, data.fontData.byteLength);
|
||||
window.document.fonts.add(new FontFace("text-font", fontView));
|
||||
const face = new FontFace("text-font", fontView);
|
||||
window.document.fonts.add(face);
|
||||
addedFontFaces.push(face);
|
||||
textInput.style.fontFamily = "text-font";
|
||||
}
|
||||
|
||||
@@ -436,7 +442,6 @@
|
||||
// Not compatible with Safari:
|
||||
// <https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#browser_compatibility>
|
||||
// <https://bugs.webkit.org/show_bug.cgi?id=124862>
|
||||
let removeUpdatePixelRatio: (() => void) | undefined = undefined;
|
||||
const updatePixelRatio = () => {
|
||||
removeUpdatePixelRatio?.();
|
||||
const mediaQueryList = matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
||||
@@ -510,7 +515,9 @@
|
||||
|
||||
if (textInput && data.fontData.length > 0 && data.fontData.buffer instanceof ArrayBuffer) {
|
||||
const fontView = new Uint8Array(data.fontData.buffer, data.fontData.byteOffset, data.fontData.byteLength);
|
||||
window.document.fonts.add(new FontFace("text-font", fontView));
|
||||
const face = new FontFace("text-font", fontView);
|
||||
window.document.fonts.add(face);
|
||||
addedFontFaces.push(face);
|
||||
textInput.style.fontFamily = "text-font";
|
||||
}
|
||||
});
|
||||
@@ -525,18 +532,32 @@
|
||||
|
||||
// Setup ResizeObserver for pixel-perfect viewport tracking with physical dimensions
|
||||
// This must happen in onMount to ensure the viewport container element exists
|
||||
setupViewportResizeObserver(editor);
|
||||
cleanupViewportResizeObserver = setupViewportResizeObserver(editor);
|
||||
|
||||
// Also observe the inner viewport for canvas sizing and ruler updates
|
||||
const viewportResizeObserver = new ResizeObserver(() => {
|
||||
viewportResizeObserver = new ResizeObserver(() => {
|
||||
updateViewportInfo();
|
||||
});
|
||||
if (viewport) viewportResizeObserver.observe(viewport);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
// Cleanup the viewport resize observer
|
||||
cleanupViewportResizeObserver();
|
||||
cleanupViewportResizeObserver?.();
|
||||
viewportResizeObserver?.disconnect();
|
||||
removeUpdatePixelRatio?.();
|
||||
addedFontFaces.forEach((face) => window.document.fonts.delete(face));
|
||||
|
||||
editor.subscriptions.unsubscribeFrontendMessage("UpdateDocumentArtwork");
|
||||
editor.subscriptions.unsubscribeFrontendMessage("UpdateEyedropperSamplingState");
|
||||
editor.subscriptions.unsubscribeFrontendMessage("UpdateGradientStopColorPickerPosition");
|
||||
editor.subscriptions.unsubscribeFrontendMessage("UpdateDocumentScrollbars");
|
||||
editor.subscriptions.unsubscribeFrontendMessage("UpdateDocumentRulers");
|
||||
editor.subscriptions.unsubscribeFrontendMessage("UpdateMouseCursor");
|
||||
editor.subscriptions.unsubscribeFrontendMessage("TriggerTextCommit");
|
||||
editor.subscriptions.unsubscribeFrontendMessage("DisplayEditableTextbox");
|
||||
editor.subscriptions.unsubscribeFrontendMessage("DisplayEditableTextboxUpdateFontData");
|
||||
editor.subscriptions.unsubscribeFrontendMessage("DisplayEditableTextboxTransform");
|
||||
editor.subscriptions.unsubscribeFrontendMessage("DisplayRemoveEditableTextbox");
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
import type { LayerPanelEntry, LayerStructureEntry, Layout } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
|
||||
import type { TooltipState } from "@graphite/state-providers/tooltip";
|
||||
import type { NodeGraphStore } from "@graphite/stores/node-graph";
|
||||
import type { TooltipStore } from "@graphite/stores/tooltip";
|
||||
import { pasteFile } from "@graphite/utility-functions/files";
|
||||
import { operatingSystem } from "@graphite/utility-functions/platform";
|
||||
import { patchLayout } from "@graphite/utility-functions/widgets";
|
||||
@@ -42,8 +42,8 @@
|
||||
};
|
||||
|
||||
const editor = getContext<Editor>("editor");
|
||||
const nodeGraph = getContext<NodeGraphState>("nodeGraph");
|
||||
const tooltip = getContext<TooltipState>("tooltip");
|
||||
const nodeGraph = getContext<NodeGraphStore>("nodeGraph");
|
||||
const tooltip = getContext<TooltipStore>("tooltip");
|
||||
|
||||
let list: LayoutCol | undefined;
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
|
||||
import type { FrontendGraphInput, FrontendGraphOutput, FrontendNode } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { DocumentState } from "@graphite/state-providers/document";
|
||||
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
|
||||
import type { DocumentStore } from "@graphite/stores/document";
|
||||
import { closeContextMenu } from "@graphite/stores/node-graph";
|
||||
import type { NodeGraphStore } from "@graphite/stores/node-graph";
|
||||
|
||||
import NodeCatalog from "@graphite/components/floating-menus/NodeCatalog.svelte";
|
||||
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
@@ -20,8 +21,8 @@
|
||||
const FADE_TRANSITION = { duration: 200, easing: cubicInOut };
|
||||
|
||||
const editor = getContext<Editor>("editor");
|
||||
const nodeGraph = getContext<NodeGraphState>("nodeGraph");
|
||||
const documentState = getContext<DocumentState>("document");
|
||||
const nodeGraph = getContext<NodeGraphStore>("nodeGraph");
|
||||
const documentState = getContext<DocumentStore>("document");
|
||||
|
||||
let graph: HTMLDivElement | undefined;
|
||||
|
||||
@@ -29,7 +30,7 @@
|
||||
$: gridDotRadius = 1 + Math.floor($nodeGraph.transform.scale - 0.5 + 0.001) / 2;
|
||||
|
||||
// Close the context menu when the graph view overlay is closed
|
||||
$: if (!$documentState.graphViewOverlayOpen) nodeGraph.closeContextMenu();
|
||||
$: if (!$documentState.graphViewOverlayOpen) closeContextMenu();
|
||||
|
||||
let inputElement: HTMLInputElement;
|
||||
let hoveringImportIndex: number | undefined = undefined;
|
||||
@@ -220,7 +221,7 @@
|
||||
label="Merge Selected Nodes"
|
||||
action={() => {
|
||||
editor.handle.mergeSelectedNodes();
|
||||
nodeGraph.closeContextMenu();
|
||||
closeContextMenu();
|
||||
}}
|
||||
flush={true}
|
||||
/>
|
||||
@@ -231,7 +232,7 @@
|
||||
if ($nodeGraph.contextMenuInformation?.contextMenuData.type === "ModifyNode") {
|
||||
editor.handle.setToNodeOrLayer($nodeGraph.contextMenuInformation.contextMenuData.data.nodeId, currentlyIsNode);
|
||||
}
|
||||
nodeGraph.closeContextMenu();
|
||||
closeContextMenu();
|
||||
}}
|
||||
disabled={!$nodeGraph.contextMenuInformation.contextMenuData.data.canBeLayer}
|
||||
flush={true}
|
||||
@@ -247,7 +248,7 @@
|
||||
} else {
|
||||
editor.handle.toggleLayerLock(nodeId);
|
||||
}
|
||||
nodeGraph.closeContextMenu();
|
||||
closeContextMenu();
|
||||
}}
|
||||
flush={true}
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import type { LayoutTarget, Widget, WidgetInstance } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import { parseFillChoice } from "@graphite/utility-functions/colors";
|
||||
import { debouncer } from "@graphite/utility-functions/debounce";
|
||||
|
||||
import NodeCatalog from "@graphite/components/floating-menus/NodeCatalog.svelte";
|
||||
import BreadcrumbTrailButtons from "@graphite/components/widgets/buttons/BreadcrumbTrailButtons.svelte";
|
||||
@@ -137,7 +136,7 @@
|
||||
getProps: (props, index) => ({
|
||||
...props,
|
||||
$$events: {
|
||||
value: (e: CustomEvent) => debouncer((value: unknown) => widgetValueCommitAndUpdate(index, value, false), { debounceTime: 120 }).debounceUpdateValue(e.detail),
|
||||
value: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, false),
|
||||
},
|
||||
}),
|
||||
},
|
||||
@@ -202,7 +201,7 @@
|
||||
incrementCallbackIncrease: () => widgetValueCommitAndUpdate(index, "Increment", false),
|
||||
incrementCallbackDecrease: () => widgetValueCommitAndUpdate(index, "Decrement", false),
|
||||
$$events: {
|
||||
value: (e: CustomEvent) => debouncer((value: unknown) => widgetValueUpdate(index, value, true)).debounceUpdateValue(e.detail),
|
||||
value: (e: CustomEvent) => widgetValueUpdate(index, e.detail, true),
|
||||
startHistoryTransaction: () => widgetValueCommit(index, props.value),
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import type { Curve, CurveManipulatorGroup, ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import { clamp } from "@graphite/utility-functions/math";
|
||||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
|
||||
@@ -185,6 +184,10 @@
|
||||
dAttribute = recalculateSvgPath();
|
||||
updateCurve();
|
||||
}
|
||||
|
||||
function clamp(value: number, min = 0, max = 1): number {
|
||||
return Math.max(min, Math.min(value, max));
|
||||
}
|
||||
</script>
|
||||
|
||||
<LayoutRow class="curve-input" {tooltipLabel} {tooltipDescription} {tooltipShortcut}>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { evaluateMathExpression, isPlatformNative } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { NumberInputMode, NumberInputIncrementBehavior, ActionShortcut } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import { PRESS_REPEAT_DELAY_MS, PRESS_REPEAT_INTERVAL_MS } from "@graphite/io-managers/input";
|
||||
import { PRESS_REPEAT_DELAY_MS, PRESS_REPEAT_INTERVAL_MS } from "@graphite/managers/input";
|
||||
import { browserVersion } from "@graphite/utility-functions/platform";
|
||||
|
||||
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
@@ -87,6 +87,12 @@
|
||||
let shiftKeyDown = false;
|
||||
// Track whether the Ctrl key is currently held down.
|
||||
let ctrlKeyDown = false;
|
||||
// Cleanup function for active drag interactions, called on destroy to prevent leaked listeners
|
||||
let activeDragCleanup: (() => void) | undefined;
|
||||
// Track the slider abort state for cleanup on destroy
|
||||
let sliderResetAbortHandler: (() => void) | undefined;
|
||||
let sliderAbortTimeout1: ReturnType<typeof setTimeout> | undefined;
|
||||
let sliderAbortTimeout2: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
$: watchValue(value, unit);
|
||||
$: sliderStepValue = isInteger ? (step === undefined ? 1 : step) : "any";
|
||||
@@ -107,10 +113,31 @@
|
||||
addEventListener("mousemove", trackShiftAndCtrl);
|
||||
});
|
||||
onDestroy(() => {
|
||||
clearTimeout(repeatTimeout);
|
||||
clearTimeout(sliderAbortTimeout1);
|
||||
clearTimeout(sliderAbortTimeout2);
|
||||
|
||||
activeDragCleanup?.();
|
||||
|
||||
// Exit pointer lock if active (non-Safari path)
|
||||
if (document.pointerLockElement) document.exitPointerLock();
|
||||
|
||||
// Remove Safari cursor-hidden workaround class if present
|
||||
const isSafari = browserVersion().toLowerCase().includes("safari");
|
||||
if (isSafari) document.body.classList.remove("cursor-hidden");
|
||||
|
||||
// Clean up any listeners related to tracking the Shift and Ctrl keys
|
||||
removeEventListener("keydown", trackShiftAndCtrl);
|
||||
removeEventListener("keyup", trackShiftAndCtrl);
|
||||
removeEventListener("mousemove", trackShiftAndCtrl);
|
||||
clearTimeout(repeatTimeout);
|
||||
|
||||
// Clean up any slider-related listeners that may be active
|
||||
removeEventListener("mousedown", sliderAbortFromMousedown);
|
||||
removeEventListener("keydown", sliderAbortFromMousedown);
|
||||
removeEventListener("pointermove", sliderAbortFromDragging);
|
||||
removeEventListener("keydown", sliderAbortFromDragging);
|
||||
removeEventListener("keydown", incrementPressAbort);
|
||||
if (sliderResetAbortHandler) removeEventListener("pointerup", sliderResetAbortHandler);
|
||||
});
|
||||
|
||||
// ===============================
|
||||
@@ -297,7 +324,7 @@
|
||||
pressingArrow = false;
|
||||
clearTimeout(repeatTimeout);
|
||||
updateValue(initialValueBeforeDragging);
|
||||
removeEventListener("keydown", onIncrementPointerUp);
|
||||
removeEventListener("keydown", incrementPressAbort);
|
||||
}
|
||||
|
||||
// =======================================
|
||||
@@ -333,10 +360,9 @@
|
||||
alreadyActedGuard = true;
|
||||
|
||||
isDragging = true;
|
||||
beginDrag(e);
|
||||
|
||||
removeEventListener("pointermove", onMove);
|
||||
removeEventListener("pointerup", onUp);
|
||||
activeDragCleanup?.();
|
||||
beginDrag(e);
|
||||
};
|
||||
// If it's a mouseup, we'll begin editing the text field.
|
||||
const onUp = () => {
|
||||
@@ -346,11 +372,15 @@
|
||||
isDragging = false;
|
||||
self?.focus();
|
||||
|
||||
removeEventListener("pointermove", onMove);
|
||||
removeEventListener("pointerup", onUp);
|
||||
activeDragCleanup?.();
|
||||
};
|
||||
addEventListener("pointermove", onMove);
|
||||
addEventListener("pointerup", onUp);
|
||||
activeDragCleanup = () => {
|
||||
removeEventListener("pointermove", onMove);
|
||||
removeEventListener("pointerup", onUp);
|
||||
activeDragCleanup = undefined;
|
||||
};
|
||||
}
|
||||
|
||||
function beginDrag(e: PointerEvent) {
|
||||
@@ -449,16 +479,20 @@
|
||||
cumulativeDragDelta = 0;
|
||||
|
||||
// Clean up the event listeners.
|
||||
removeEventListener("pointerup", pointerUp);
|
||||
removeEventListener("pointermove", pointerMove);
|
||||
removeEventListener("pointerlockmove", pointerLockMove);
|
||||
if (usePointerLock) document.removeEventListener("pointerlockchange", pointerLockChange);
|
||||
activeDragCleanup?.();
|
||||
};
|
||||
|
||||
addEventListener("pointerup", pointerUp);
|
||||
addEventListener("pointermove", pointerMove);
|
||||
addEventListener("pointerlockmove", pointerLockMove);
|
||||
if (usePointerLock) document.addEventListener("pointerlockchange", pointerLockChange);
|
||||
activeDragCleanup = () => {
|
||||
removeEventListener("pointerup", pointerUp);
|
||||
removeEventListener("pointermove", pointerMove);
|
||||
removeEventListener("pointerlockmove", pointerLockMove);
|
||||
if (usePointerLock) document.removeEventListener("pointerlockchange", pointerLockChange);
|
||||
activeDragCleanup = undefined;
|
||||
};
|
||||
}
|
||||
|
||||
function pointerLockMoveUpdate(delta: number, slow: boolean, snapping: boolean, initialValue: number) {
|
||||
@@ -657,7 +691,7 @@
|
||||
|
||||
// End the user's drag by instantaneously disabling and re-enabling the range input element
|
||||
if (inputRangeElement) inputRangeElement.disabled = true;
|
||||
setTimeout(() => {
|
||||
sliderAbortTimeout1 = setTimeout(() => {
|
||||
if (inputRangeElement) inputRangeElement.disabled = false;
|
||||
}, 0);
|
||||
|
||||
@@ -680,11 +714,13 @@
|
||||
// dragging the slider, hitting Escape, then releasing the mouse button. This results in being transferred by `onSliderInput()` to the
|
||||
// "Deciding" state when we should remain in the "Ready" state as set here. (For debugging, this can be visualized in CSS by
|
||||
// recoloring the fake slider handle, which is shown in the "Deciding" state.)
|
||||
setTimeout(() => (rangeSliderClickDragState = "Ready"), 0);
|
||||
sliderAbortTimeout2 = setTimeout(() => (rangeSliderClickDragState = "Ready"), 0);
|
||||
|
||||
// Clean up the event listener that was used to call this function.
|
||||
removeEventListener("pointerup", sliderResetAbort);
|
||||
sliderResetAbortHandler = undefined;
|
||||
};
|
||||
sliderResetAbortHandler = sliderResetAbort;
|
||||
addEventListener("pointerup", sliderResetAbort);
|
||||
|
||||
// Clean up the event listeners that were for tracking an abort while dragging the slider, now that we're no longer dragging it.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { createEventDispatcher, onDestroy } from "svelte";
|
||||
|
||||
import { PRESS_REPEAT_DELAY_MS, PRESS_REPEAT_INTERVAL_MS, PRESS_REPEAT_INTERVAL_RAPID_MS } from "@graphite/io-managers/input";
|
||||
import { PRESS_REPEAT_DELAY_MS, PRESS_REPEAT_INTERVAL_MS, PRESS_REPEAT_INTERVAL_RAPID_MS } from "@graphite/managers/input";
|
||||
|
||||
const ARROW_CLICK_DISTANCE = 0.05;
|
||||
const ARROW_REPEAT_DISTANCE = 0.01;
|
||||
@@ -187,6 +187,10 @@
|
||||
if (e.key === "Escape") abortInteraction();
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
removeEvents();
|
||||
});
|
||||
|
||||
function addEvents() {
|
||||
window.addEventListener("pointerup", onPointerUp);
|
||||
window.addEventListener("pointermove", onPointerMove);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onDestroy } from "svelte";
|
||||
import { createEventDispatcher, onMount, onDestroy } from "svelte";
|
||||
|
||||
import { evaluateGradientAtPosition } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { Color, GradientStops } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
@@ -338,7 +338,9 @@
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", deleteStop);
|
||||
onMount(() => {
|
||||
document.addEventListener("keydown", deleteStop);
|
||||
});
|
||||
onDestroy(() => {
|
||||
removeEvents();
|
||||
document.removeEventListener("keydown", deleteStop);
|
||||
|
||||
@@ -62,7 +62,10 @@
|
||||
}
|
||||
|
||||
onMount(() => watchForCheckbox(forCheckbox));
|
||||
onDestroy(() => watchForCheckbox(undefined));
|
||||
onDestroy(() => {
|
||||
handlePointerLeave();
|
||||
watchForCheckbox(undefined);
|
||||
});
|
||||
</script>
|
||||
|
||||
<label
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
import { getContext } from "svelte";
|
||||
|
||||
import { isPlatformNative } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { AppWindowState } from "@graphite/state-providers/app-window";
|
||||
import type { DialogState } from "@graphite/state-providers/dialog";
|
||||
import type { TooltipState } from "@graphite/state-providers/tooltip";
|
||||
import type { AppWindowStore } from "@graphite/stores/app-window";
|
||||
import type { DialogStore } from "@graphite/stores/dialog";
|
||||
import type { TooltipStore } from "@graphite/stores/tooltip";
|
||||
|
||||
import Dialog from "@graphite/components/floating-menus/Dialog.svelte";
|
||||
import Tooltip from "@graphite/components/floating-menus/Tooltip.svelte";
|
||||
@@ -14,9 +14,9 @@
|
||||
import TitleBar from "@graphite/components/window/TitleBar.svelte";
|
||||
import Workspace from "@graphite/components/window/Workspace.svelte";
|
||||
|
||||
const dialog = getContext<DialogState>("dialog");
|
||||
const tooltip = getContext<TooltipState>("tooltip");
|
||||
const appWindow = getContext<AppWindowState>("appWindow");
|
||||
const dialog = getContext<DialogStore>("dialog");
|
||||
const tooltip = getContext<TooltipStore>("tooltip");
|
||||
const appWindow = getContext<AppWindowStore>("appWindow");
|
||||
</script>
|
||||
|
||||
<LayoutCol class="main-window" classes={{ "viewport-hole-punch": $appWindow.viewportHolePunch }}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from "svelte";
|
||||
import { getContext, onMount, onDestroy } from "svelte";
|
||||
|
||||
import type { Layout } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
@@ -24,6 +24,11 @@
|
||||
statusBarInfoLayout = statusBarInfoLayout;
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
editor.subscriptions.unsubscribeLayoutUpdate("StatusBarHints");
|
||||
editor.subscriptions.unsubscribeLayoutUpdate("StatusBarInfo");
|
||||
});
|
||||
</script>
|
||||
|
||||
<LayoutRow class="status-bar">
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from "svelte";
|
||||
import { getContext, onMount, onDestroy } from "svelte";
|
||||
|
||||
import { isPlatformNative } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { Layout } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { AppWindowState } from "@graphite/state-providers/app-window";
|
||||
import type { FullscreenState } from "@graphite/state-providers/fullscreen";
|
||||
import type { TooltipState } from "@graphite/state-providers/tooltip";
|
||||
import type { AppWindowStore } from "@graphite/stores/app-window";
|
||||
import { enterFullscreen, exitFullscreen } from "@graphite/stores/fullscreen";
|
||||
import type { FullscreenStore } from "@graphite/stores/fullscreen";
|
||||
import type { TooltipStore } from "@graphite/stores/tooltip";
|
||||
import { patchLayout } from "@graphite/utility-functions/widgets";
|
||||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
|
||||
|
||||
const appWindow = getContext<AppWindowState>("appWindow");
|
||||
const keyboardLockApiSupported = navigator.keyboard !== undefined && "lock" in navigator.keyboard;
|
||||
|
||||
const appWindow = getContext<AppWindowStore>("appWindow");
|
||||
const editor = getContext<Editor>("editor");
|
||||
const fullscreen = getContext<FullscreenState>("fullscreen");
|
||||
const tooltip = getContext<TooltipState>("tooltip");
|
||||
const fullscreen = getContext<FullscreenStore>("fullscreen");
|
||||
const tooltip = getContext<TooltipStore>("tooltip");
|
||||
|
||||
let menuBarLayout: Layout = [];
|
||||
|
||||
@@ -31,6 +34,10 @@
|
||||
menuBarLayout = menuBarLayout;
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
editor.subscriptions.unsubscribeLayoutUpdate("MenuBar");
|
||||
});
|
||||
</script>
|
||||
|
||||
<LayoutRow class="title-bar" styles={{ height: height + "px" }}>
|
||||
@@ -48,13 +55,13 @@
|
||||
{#if showFullscreenButton}
|
||||
<LayoutRow
|
||||
tooltipLabel={isFullscreen ? "Exit Fullscreen" : "Enter Fullscreen"}
|
||||
tooltipDescription={$appWindow.platform === "Web" && $fullscreen.keyboardLockApiSupported
|
||||
tooltipDescription={$appWindow.platform === "Web" && keyboardLockApiSupported
|
||||
? "While fullscreen, keyboard shortcuts normally reserved by the browser become available."
|
||||
: undefined}
|
||||
tooltipShortcut={$tooltip.fullscreenShortcut}
|
||||
on:click={() => {
|
||||
if (isPlatformNative()) editor.handle.appWindowFullscreen();
|
||||
else ($fullscreen.windowFullscreen ? fullscreen.exitFullscreen : fullscreen.enterFullscreen)();
|
||||
else ($fullscreen.windowFullscreen ? exitFullscreen : enterFullscreen)();
|
||||
}}
|
||||
>
|
||||
<IconLabel icon={isFullscreen ? "FullscreenExit" : "FullscreenEnter"} />
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
import { getContext, onDestroy } from "svelte";
|
||||
|
||||
import type { OpenDocument } from "@graphite/../wasm/pkg/graphite_wasm";
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { PortfolioState } from "@graphite/state-providers/portfolio";
|
||||
import type { PortfolioStore } from "@graphite/stores/portfolio";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
@@ -24,6 +24,11 @@
|
||||
let documentPanel: Panel | undefined;
|
||||
let gutterResizeRestore: [number, number] | undefined = undefined;
|
||||
let pointerCaptureId: number | undefined = undefined;
|
||||
let activeResizeCleanup: (() => void) | undefined = undefined;
|
||||
|
||||
onDestroy(() => {
|
||||
activeResizeCleanup?.();
|
||||
});
|
||||
|
||||
$: documentPanel?.scrollTabIntoView($portfolio.activeDocumentIndex);
|
||||
|
||||
@@ -37,7 +42,7 @@
|
||||
});
|
||||
|
||||
const editor = getContext<Editor>("editor");
|
||||
const portfolio = getContext<PortfolioState>("portfolio");
|
||||
const portfolio = getContext<PortfolioStore>("portfolio");
|
||||
|
||||
function resizePanel(e: PointerEvent) {
|
||||
const gutter = e.target;
|
||||
@@ -76,6 +81,7 @@
|
||||
const abortResize = () => {
|
||||
if (pointerCaptureId) gutter.releasePointerCapture(pointerCaptureId);
|
||||
removeListeners();
|
||||
activeResizeCleanup = undefined;
|
||||
|
||||
pointerCaptureId = e.pointerId;
|
||||
gutter.setPointerCapture(pointerCaptureId);
|
||||
@@ -104,6 +110,7 @@
|
||||
gutterResizeRestore = undefined;
|
||||
if (pointerCaptureId) gutter.releasePointerCapture(pointerCaptureId);
|
||||
removeListeners();
|
||||
activeResizeCleanup = undefined;
|
||||
};
|
||||
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
@@ -130,6 +137,7 @@
|
||||
};
|
||||
|
||||
addListeners();
|
||||
activeResizeCleanup = removeListeners;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user