mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-24 06:58:12 +08:00
Dialog redesign and content revamp (#1409)
* Revamp the content and design of dialogs * Add the Licenses dialog
This commit is contained in:
@@ -44,7 +44,7 @@
|
||||
createPanicManager(editor, dialog);
|
||||
createPersistenceManager(editor, portfolio);
|
||||
let dragManagerDestructor = createDragManager();
|
||||
let inputManagerDestructor = createInputManager(editor, dialog, portfolio, fullscreen);
|
||||
let inputManagerDestructor = createInputManager(editor, dialog, portfolio, document, fullscreen);
|
||||
|
||||
onMount(() => {
|
||||
// Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from "svelte";
|
||||
import { githubUrl } from "@graphite/io-managers/panic";
|
||||
import { wipeDocuments } from "@graphite/io-managers/persistence";
|
||||
|
||||
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
|
||||
import type { DialogState } from "@graphite/state-providers/dialog";
|
||||
|
||||
@@ -20,24 +23,52 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- TODO: Use https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog for improved accessibility -->
|
||||
<FloatingMenu open={true} class="dialog-modal" type="Dialog" direction="Center" bind:this={self} data-dialog-modal>
|
||||
<LayoutRow>
|
||||
<LayoutCol class="icon-column">
|
||||
<!-- `$dialog.icon` class exists to provide special sizing in CSS to specific icons -->
|
||||
<IconLabel icon={$dialog.icon} class={$dialog.icon.toLowerCase()} />
|
||||
</LayoutCol>
|
||||
<LayoutCol class="main-column">
|
||||
{#if $dialog.widgets.layout.length > 0}
|
||||
<WidgetLayout layout={$dialog.widgets} class="details" />
|
||||
<LayoutRow class="header-area">
|
||||
<!-- `$dialog.icon` class exists to provide special sizing in CSS to specific icons -->
|
||||
<IconLabel icon={$dialog.icon} class={$dialog.icon.toLowerCase()} />
|
||||
<TextLabel>{$dialog.title}</TextLabel>
|
||||
</LayoutRow>
|
||||
<LayoutRow class="content">
|
||||
<LayoutCol class="column-1">
|
||||
{#if $dialog.column1.layout.length > 0}
|
||||
<WidgetLayout layout={$dialog.column1} class="details" />
|
||||
{/if}
|
||||
{#if ($dialog.crashDialogButtons?.length || NaN) > 0}
|
||||
<LayoutRow class="panic-buttons-row">
|
||||
{#each $dialog.crashDialogButtons || [] as button, index (index)}
|
||||
<TextButton action={() => button.callback?.()} {...button.props} />
|
||||
{/each}
|
||||
</LayoutRow>
|
||||
{#if $dialog.panicDetails}
|
||||
<div class="widget-layout details">
|
||||
<div class="widget-row"><TextLabel bold={true}>The editor crashed — sorry about that</TextLabel></div>
|
||||
<div class="widget-row"><TextLabel>Please report this by filing an issue on GitHub:</TextLabel></div>
|
||||
<div class="widget-row"><TextButton label="Report Bug" icon="Warning" noBackground={true} action={() => window.open(githubUrl($dialog.panicDetails), "_blank")} /></div>
|
||||
<div class="widget-row"><TextLabel multiline={true}>Reload the editor to continue. If this occurs<br />immediately on repeated reloads, clear storage:</TextLabel></div>
|
||||
<div class="widget-row">
|
||||
<TextButton
|
||||
label="Clear Saved Documents"
|
||||
icon="Trash"
|
||||
noBackground={true}
|
||||
action={async () => {
|
||||
await wipeDocuments();
|
||||
window.location.reload();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
{#if $dialog.column2.layout.length > 0}
|
||||
<LayoutCol class="column-2">
|
||||
<WidgetLayout layout={$dialog.column2} class="details" />
|
||||
</LayoutCol>
|
||||
{/if}
|
||||
</LayoutRow>
|
||||
<LayoutRow class="footer-area">
|
||||
{#if $dialog.buttons.layout.length > 0}
|
||||
<WidgetLayout layout={$dialog.buttons} class="details" />
|
||||
{/if}
|
||||
{#if $dialog.panicDetails}
|
||||
<TextButton label="Copy Error Log" action={() => navigator.clipboard.writeText($dialog.panicDetails)} />
|
||||
<TextButton label="Reload" emphasized={true} action={() => window.location.reload()} />
|
||||
{/if}
|
||||
</LayoutRow>
|
||||
</FloatingMenu>
|
||||
|
||||
@@ -50,32 +81,45 @@
|
||||
|
||||
> .floating-menu-container > .floating-menu-content {
|
||||
pointer-events: auto;
|
||||
padding: 24px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.icon-column {
|
||||
margin-right: 24px;
|
||||
.header-area,
|
||||
.footer-area {
|
||||
background: var(--color-1-nearblack);
|
||||
}
|
||||
|
||||
.header-area,
|
||||
.footer-area,
|
||||
.content {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.header-area {
|
||||
border-radius: 4px 4px 0 0;
|
||||
|
||||
.icon-label {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
&.file,
|
||||
&.copy {
|
||||
width: 60px;
|
||||
|
||||
svg {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 -10px;
|
||||
}
|
||||
}
|
||||
.text-label {
|
||||
margin-left: 12px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.main-column {
|
||||
.content {
|
||||
margin: -4px 0;
|
||||
|
||||
.column-1 + .column-2 {
|
||||
margin-left: 48px;
|
||||
|
||||
.text-button {
|
||||
justify-content: left;
|
||||
}
|
||||
}
|
||||
|
||||
.details.text-label {
|
||||
-webkit-user-select: text; // Required as of Safari 15.0 (Graphite's minimum version) through the latest release
|
||||
user-select: text;
|
||||
@@ -84,13 +128,22 @@
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.radio-input button {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
// Used by the "Open Demo Artwork" dialog
|
||||
.image-label {
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.panic-buttons-row {
|
||||
height: 32px;
|
||||
align-items: center;
|
||||
.footer-area {
|
||||
border-radius: 0 0 4px 4px;
|
||||
justify-content: right;
|
||||
|
||||
.text-button {
|
||||
min-width: 96px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { afterUpdate, createEventDispatcher, tick } from "svelte";
|
||||
import { onMount, afterUpdate, createEventDispatcher, tick } from "svelte";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
|
||||
@@ -103,9 +103,42 @@
|
||||
wasOpen = isOpen;
|
||||
}
|
||||
|
||||
// Gets the client bounds of the elements and apply relevant styles to them
|
||||
// TODO: Use DOM attribute bindings more whilst not causing recursive updates
|
||||
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) {
|
||||
// TODO: Also use https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver to detect any changes which may affect the size of the content.
|
||||
// TODO: The current method only notices when the dialog size increases but can't detect when it decreases.
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
let { width, height } = entry.contentRect;
|
||||
|
||||
width = Math.ceil(width);
|
||||
if (width % 2 === 1) width += 1;
|
||||
height = Math.ceil(height);
|
||||
if (height % 2 === 1) height += 1;
|
||||
|
||||
// We have to set the style properties directly because attempting to do it through a Svelte bound property results in `afterUpdate()` being triggered
|
||||
floatingMenuContentDiv.style.setProperty("min-width", width === 0 ? "unset" : `${width}px`);
|
||||
floatingMenuContentDiv.style.setProperty("min-height", height === 0 ? "unset" : `${height}px`);
|
||||
});
|
||||
});
|
||||
resizeObserver.observe(floatingMenuContentDiv);
|
||||
}
|
||||
});
|
||||
|
||||
afterUpdate(() => {
|
||||
// Remove the size constraint after the content updates so the resize observer can measure the content and reapply a newly calculated one
|
||||
const floatingMenuContentDiv = floatingMenuContent?.div();
|
||||
if (type === "Dialog" && floatingMenuContentDiv) {
|
||||
// We have to set the style properties directly because attempting to do it through a Svelte bound property results in `afterUpdate()` being triggered
|
||||
floatingMenuContentDiv.style.setProperty("min-width", "unset");
|
||||
floatingMenuContentDiv.style.setProperty("min-height", "unset");
|
||||
}
|
||||
|
||||
// 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, which causes the `afterUpdate()` Svelte event to fire extraneous times (hurting performance and sometimes causing an infinite loop)
|
||||
if (!measuringOngoingGuard) positionAndStyleFloatingMenu();
|
||||
});
|
||||
|
||||
@@ -69,13 +69,6 @@
|
||||
editor.instance.updateLayout(layoutTarget, widgets[index].widgetId, value);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// function exclude<T extends Record<string, any>>(props: T, additional?: (keyof T)[]): Pick<T, Exclude<keyof T, "kind" | (typeof additional extends Array<infer K> ? K : never)>> {
|
||||
// const exclusions = ["kind", ...(additional || [])];
|
||||
|
||||
// return Object.fromEntries(Object.entries(props).filter((entry) => !exclusions.includes(entry[0]))) as any;
|
||||
// }
|
||||
|
||||
// TODO: This seems to work, but verify the correctness and terseness of this, it's adapted from https://stackoverflow.com/a/67434028/775283
|
||||
function exclude<T extends object>(props: T, additional?: (keyof T)[]): Omit<T, typeof additional extends Array<infer K> ? K : never> {
|
||||
const exclusions = ["kind", ...(additional || [])];
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
export let label: string;
|
||||
export let icon: IconName | undefined = undefined;
|
||||
export let emphasized = false;
|
||||
export let noBackground = false;
|
||||
export let minWidth = 0;
|
||||
export let disabled = false;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
@@ -21,6 +22,7 @@
|
||||
class="text-button"
|
||||
class:emphasized
|
||||
class:disabled
|
||||
class:no-background={noBackground}
|
||||
class:sharp-right-corners={sharpRightCorners}
|
||||
style:min-width={minWidth > 0 ? `${minWidth}px` : ""}
|
||||
title={tooltip}
|
||||
@@ -76,10 +78,26 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.text-button + .text-button {
|
||||
&.no-background {
|
||||
&:not(:hover) {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.icon-label {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.widget-row > & + .text-button,
|
||||
.layout-row > & + .text-button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.widget-column > & + .text-button,
|
||||
.layout-column > & + .text-button {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.icon-label {
|
||||
position: relative;
|
||||
left: -4px;
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<TextButton label="New Document" icon="File" action={() => editor.instance.newDocumentDialog()} />
|
||||
<TextButton label="New Document" icon="File" noBackground={true} action={() => editor.instance.newDocumentDialog()} />
|
||||
</td>
|
||||
<td>
|
||||
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(true), { key: "KeyN", label: "N" }]]} />
|
||||
@@ -128,7 +128,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<TextButton label="Open Document" icon="Folder" action={() => editor.instance.openDocument()} />
|
||||
<TextButton label="Open Document" icon="Folder" noBackground={true} action={() => editor.instance.openDocument()} />
|
||||
</td>
|
||||
<td>
|
||||
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(false), { key: "KeyO", label: "O" }]]} />
|
||||
@@ -136,7 +136,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<TextButton label="Open Demo Artwork" icon="Image" action={() => editor.instance.demoArtworkDialog()} />
|
||||
<TextButton label="Open Demo Artwork" icon="Image" noBackground={true} action={() => editor.instance.demoArtworkDialog()} />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -289,10 +289,6 @@
|
||||
td {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.text-button:not(:hover) {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { get } from "svelte/store";
|
||||
|
||||
import { type DialogState } from "@graphite/state-providers/dialog";
|
||||
import { type DocumentState } from "@graphite/state-providers/document";
|
||||
import { type FullscreenState } from "@graphite/state-providers/fullscreen";
|
||||
import { type PortfolioState } from "@graphite/state-providers/portfolio";
|
||||
import { makeKeyboardModifiersBitfield, textInputCleanup, getLocalizedScanCode } from "@graphite/utility-functions/keyboard-entry";
|
||||
@@ -16,7 +17,7 @@ type EventListenerTarget = {
|
||||
removeEventListener: typeof window.removeEventListener;
|
||||
};
|
||||
|
||||
export function createInputManager(editor: Editor, dialog: DialogState, document: PortfolioState, fullscreen: FullscreenState): () => void {
|
||||
export function createInputManager(editor: Editor, dialog: DialogState, portfolio: PortfolioState, document: DocumentState, fullscreen: FullscreenState): () => void {
|
||||
const app = window.document.querySelector("[data-app-container]") as HTMLElement | undefined;
|
||||
app?.focus();
|
||||
|
||||
@@ -58,8 +59,8 @@ export function createInputManager(editor: Editor, dialog: DialogState, document
|
||||
// Keyboard events
|
||||
|
||||
async function shouldRedirectKeyboardEventToBackend(e: KeyboardEvent): Promise<boolean> {
|
||||
// Don't redirect when a modal is covering the workspace
|
||||
if (get(dialog).visible) return false;
|
||||
// Don't redirect when a modal, or the overlaid graph, is covering the workspace
|
||||
if (get(dialog).visible || get(document).graphViewOverlayOpen) return false;
|
||||
|
||||
const key = await getLocalizedScanCode(e);
|
||||
|
||||
@@ -239,7 +240,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, document
|
||||
}
|
||||
|
||||
async function onBeforeUnload(e: BeforeUnloadEvent): Promise<void> {
|
||||
const activeDocument = get(document).documents[get(document).activeDocumentIndex];
|
||||
const activeDocument = get(portfolio).documents[get(portfolio).activeDocumentIndex];
|
||||
if (activeDocument && !activeDocument.isAutoSaved) editor.instance.triggerAutoSave(activeDocument.id);
|
||||
|
||||
// Skip the message if the editor crashed, since work is already lost
|
||||
@@ -248,7 +249,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, document
|
||||
// Skip the message during development, since it's annoying when testing
|
||||
if (await editor.instance.inDevelopmentMode()) return;
|
||||
|
||||
const allDocumentsSaved = get(document).documents.reduce((acc, doc) => acc && doc.isSaved, true);
|
||||
const allDocumentsSaved = get(portfolio).documents.reduce((acc, doc) => acc && doc.isSaved, true);
|
||||
if (!allDocumentsSaved) {
|
||||
e.returnValue = "Unsaved work will be lost if the web browser tab is closed. Close anyway?";
|
||||
e.preventDefault();
|
||||
|
||||
@@ -2,10 +2,10 @@ import { type Editor } from "@graphite/wasm-communication/editor";
|
||||
import { TriggerAboutGraphiteLocalizedCommitDate } from "@graphite/wasm-communication/messages";
|
||||
|
||||
export function createLocalizationManager(editor: Editor): void {
|
||||
function localizeTimestamp(utc: string): string {
|
||||
function localizeTimestamp(utc: string): { timestamp: string, year: string } {
|
||||
// Timestamp
|
||||
const date = new Date(utc);
|
||||
if (Number.isNaN(date.getTime())) return utc;
|
||||
if (Number.isNaN(date.getTime())) return { timestamp: utc, year: `${new Date().getFullYear()}` };
|
||||
|
||||
const timezoneName = Intl.DateTimeFormat(undefined, { timeZoneName: "long" })
|
||||
.formatToParts(new Date())
|
||||
@@ -14,12 +14,12 @@ export function createLocalizationManager(editor: Editor): void {
|
||||
const dateString = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
||||
const timeString = `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
||||
const timezoneNameString = timezoneName?.value;
|
||||
return `${dateString} ${timeString} ${timezoneNameString}`;
|
||||
return { timestamp: `${dateString} ${timeString} ${timezoneNameString}`, year: `${date.getFullYear()}` };
|
||||
}
|
||||
|
||||
// Subscribe to process backend event
|
||||
editor.subscriptions.subscribeJsMessage(TriggerAboutGraphiteLocalizedCommitDate, (triggerAboutGraphiteLocalizedCommitDate) => {
|
||||
const localized = localizeTimestamp(triggerAboutGraphiteLocalizedCommitDate.commitDate);
|
||||
editor.instance.requestAboutGraphiteDialogWithLocalizedCommitDate(localized);
|
||||
editor.instance.requestAboutGraphiteDialogWithLocalizedCommitDate(localized.timestamp, localized.year);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { wipeDocuments } from "@graphite/io-managers/persistence";
|
||||
import { type DialogState } from "@graphite/state-providers/dialog";
|
||||
import { type IconName } from "@graphite/utility-functions/icons";
|
||||
import { browserVersion, operatingSystem } from "@graphite/utility-functions/platform";
|
||||
import { stripIndents } from "@graphite/utility-functions/strip-indents";
|
||||
import { type Editor } from "@graphite/wasm-communication/editor";
|
||||
import type { TextLabel } from "@graphite/wasm-communication/messages";
|
||||
import { type TextButtonWidget, type WidgetLayout, Widget, DisplayDialogPanic } from "@graphite/wasm-communication/messages";
|
||||
import { DisplayDialogPanic } from "@graphite/wasm-communication/messages";
|
||||
|
||||
export function createPanicManager(editor: Editor, dialogState: DialogState): void {
|
||||
// Code panic dialog and console error
|
||||
@@ -19,45 +16,11 @@ export function createPanicManager(editor: Editor, dialogState: DialogState): vo
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(panicDetails);
|
||||
|
||||
const crashDialog = prepareCrashDialog(displayDialogPanic.header, displayDialogPanic.description, panicDetails);
|
||||
dialogState.createCrashDialog(...crashDialog);
|
||||
dialogState.createCrashDialog(panicDetails);
|
||||
});
|
||||
}
|
||||
|
||||
function prepareCrashDialog(header: string, details: string, panicDetails: string): [IconName, WidgetLayout, TextButtonWidget[]] {
|
||||
const headerLabel: TextLabel = { kind: "TextLabel", value: header, disabled: false, bold: true, italic: false, tableAlign: false, minWidth: 0, multiline: false, tooltip: "" };
|
||||
const detailsLabel: TextLabel = { kind: "TextLabel", value: details, disabled: false, bold: false, italic: false, tableAlign: false, minWidth: 0, multiline: true, tooltip: "" };
|
||||
|
||||
const widgets: WidgetLayout = {
|
||||
layout: [{ rowWidgets: [new Widget(headerLabel, 0n)] }, { rowWidgets: [new Widget(detailsLabel, 1n)] }],
|
||||
layoutTarget: undefined,
|
||||
};
|
||||
|
||||
const reloadButton: TextButtonWidget = {
|
||||
callback: async () => window.location.reload(),
|
||||
props: { kind: "TextButton", label: "Reload", emphasized: true, minWidth: 96 },
|
||||
};
|
||||
const copyErrorLogButton: TextButtonWidget = {
|
||||
callback: async () => navigator.clipboard.writeText(panicDetails),
|
||||
props: { kind: "TextButton", label: "Copy Error Log", emphasized: false, minWidth: 96 },
|
||||
};
|
||||
const reportOnGithubButton: TextButtonWidget = {
|
||||
callback: async () => window.open(githubUrl(panicDetails), "_blank"),
|
||||
props: { kind: "TextButton", label: "Report Bug", emphasized: false, minWidth: 96 },
|
||||
};
|
||||
const clearPersistedDataButton: TextButtonWidget = {
|
||||
callback: async () => {
|
||||
await wipeDocuments();
|
||||
window.location.reload();
|
||||
},
|
||||
props: { kind: "TextButton", label: "Clear Saved Data", emphasized: false, minWidth: 96 },
|
||||
};
|
||||
const crashDialogButtons = [reloadButton, copyErrorLogButton, reportOnGithubButton, clearPersistedDataButton];
|
||||
|
||||
return ["Warning", widgets, crashDialogButtons];
|
||||
}
|
||||
|
||||
function githubUrl(panicDetails: string): string {
|
||||
export function githubUrl(panicDetails: string): string {
|
||||
const url = new URL("https://github.com/GraphiteEditor/Graphite/issues/new");
|
||||
|
||||
let body = stripIndents`
|
||||
|
||||
@@ -2,23 +2,25 @@ import {writable} from "svelte/store";
|
||||
|
||||
import { type IconName } from "@graphite/utility-functions/icons";
|
||||
import { type Editor } from "@graphite/wasm-communication/editor";
|
||||
import { type TextButtonWidget, type WidgetLayout, defaultWidgetLayout, DisplayDialog, DisplayDialogDismiss, UpdateDialogDetails, patchWidgetLayout } from "@graphite/wasm-communication/messages";
|
||||
import { defaultWidgetLayout, DisplayDialog, DisplayDialogDismiss, UpdateDialogButtons, UpdateDialogColumn1, UpdateDialogColumn2, patchWidgetLayout } from "@graphite/wasm-communication/messages";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export function createDialogState(editor: Editor) {
|
||||
const { subscribe, update } = writable({
|
||||
visible: false,
|
||||
title: "",
|
||||
icon: "" as IconName,
|
||||
widgets: defaultWidgetLayout(),
|
||||
buttons: defaultWidgetLayout(),
|
||||
column1: defaultWidgetLayout(),
|
||||
column2: defaultWidgetLayout(),
|
||||
// Special case for the crash dialog because we cannot handle button widget callbacks from Rust once the editor instance has panicked
|
||||
crashDialogButtons: undefined as undefined | TextButtonWidget[],
|
||||
panicDetails: "",
|
||||
});
|
||||
|
||||
function dismissDialog(): void {
|
||||
|
||||
update((state) => {
|
||||
// Disallow dismissing the crash dialog since it can confuse users why the app stopped responding if they dismiss it without realizing what it means
|
||||
if (!state.crashDialogButtons) state.visible = false;
|
||||
if (state.panicDetails === "") state.visible = false;
|
||||
|
||||
return state;
|
||||
});
|
||||
@@ -26,12 +28,18 @@ export function createDialogState(editor: Editor) {
|
||||
|
||||
// Creates a crash dialog from JS once the editor has panicked.
|
||||
// Normal dialogs are created in the Rust backend, but for the crash dialog, the editor instance has panicked so it cannot respond to widget callbacks.
|
||||
function createCrashDialog(icon: IconName, widgets: WidgetLayout, crashDialogButtons: TextButtonWidget[]): void {
|
||||
function createCrashDialog(panicDetails: string): void {
|
||||
update((state) => {
|
||||
state.visible = true;
|
||||
state.icon = icon;
|
||||
state.widgets = widgets;
|
||||
state.crashDialogButtons = crashDialogButtons;
|
||||
|
||||
state.icon = "Failure";
|
||||
state.title = "Crash";
|
||||
state.panicDetails = panicDetails;
|
||||
|
||||
state.column1 = defaultWidgetLayout();
|
||||
state.column2 = defaultWidgetLayout();
|
||||
state.buttons = defaultWidgetLayout();
|
||||
|
||||
return state;
|
||||
});
|
||||
}
|
||||
@@ -40,15 +48,31 @@ export function createDialogState(editor: Editor) {
|
||||
editor.subscriptions.subscribeJsMessage(DisplayDialog, (displayDialog) => {
|
||||
update((state) => {
|
||||
state.visible = true;
|
||||
|
||||
state.title = displayDialog.title;
|
||||
state.icon = displayDialog.icon;
|
||||
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDialogDetails, (updateDialogDetails) => {
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDialogButtons, (updateDialogButtons) => {
|
||||
update((state) => {
|
||||
patchWidgetLayout(state.widgets, updateDialogDetails);
|
||||
patchWidgetLayout(state.buttons, updateDialogButtons);
|
||||
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDialogColumn1, (updateDialogColumn1) => {
|
||||
update((state) => {
|
||||
patchWidgetLayout(state.column1, updateDialogColumn1);
|
||||
|
||||
return state;
|
||||
});
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDialogColumn2, (updateDialogColumn2) => {
|
||||
update((state) => {
|
||||
patchWidgetLayout(state.column2, updateDialogColumn2);
|
||||
|
||||
state.crashDialogButtons = undefined;
|
||||
return state;
|
||||
});
|
||||
});
|
||||
@@ -57,7 +81,7 @@ export function createDialogState(editor: Editor) {
|
||||
return {
|
||||
subscribe,
|
||||
dismissDialog,
|
||||
createCrashDialog: createCrashDialog,
|
||||
createCrashDialog,
|
||||
};
|
||||
}
|
||||
export type DialogState = ReturnType<typeof createDialogState>;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import {tick} from "svelte";
|
||||
import {writable} from "svelte/store";
|
||||
|
||||
import { type Editor } from "@graphite/wasm-communication/editor";
|
||||
|
||||
@@ -90,10 +90,15 @@ export function createPortfolioState(editor: Editor) {
|
||||
const backgroundColor = mime.endsWith("jpeg") ? "white" : undefined;
|
||||
|
||||
// Rasterize the SVG to an image file
|
||||
const blob = await rasterizeSVG(svg, size.x, size.y, mime, backgroundColor);
|
||||
try {
|
||||
const blob = await rasterizeSVG(svg, size.x, size.y, mime, backgroundColor);
|
||||
|
||||
// Have the browser download the file to the user's disk
|
||||
downloadFileBlob(name, blob);
|
||||
} catch {
|
||||
// Fail silently if there's an error rasterizing the SVG, such as a zero-sized image
|
||||
}
|
||||
|
||||
// Have the browser download the file to the user's disk
|
||||
downloadFileBlob(name, blob);
|
||||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateImageData, (updateImageData) => {
|
||||
updateImageData.imageData.forEach(async (element) => {
|
||||
|
||||
@@ -11,9 +11,11 @@ const GRAPHICS = {
|
||||
import Add from "@graphite-frontend/assets/icon-12px-solid/add.svg";
|
||||
import Checkmark from "@graphite-frontend/assets/icon-12px-solid/checkmark.svg";
|
||||
import CloseX from "@graphite-frontend/assets/icon-12px-solid/close-x.svg";
|
||||
import Delay from "@graphite-frontend/assets/icon-12px-solid/delay.svg";
|
||||
import DropdownArrow from "@graphite-frontend/assets/icon-12px-solid/dropdown-arrow.svg";
|
||||
import Edit12px from "@graphite-frontend/assets/icon-12px-solid/edit-12px.svg";
|
||||
import Empty12px from "@graphite-frontend/assets/icon-12px-solid/empty-12px.svg";
|
||||
import Failure from "@graphite-frontend/assets/icon-12px-solid/failure.svg";
|
||||
import FullscreenEnter from "@graphite-frontend/assets/icon-12px-solid/fullscreen-enter.svg";
|
||||
import FullscreenExit from "@graphite-frontend/assets/icon-12px-solid/fullscreen-exit.svg";
|
||||
import Grid from "@graphite-frontend/assets/icon-12px-solid/grid.svg";
|
||||
@@ -30,6 +32,7 @@ import KeyboardOption from "@graphite-frontend/assets/icon-12px-solid/keyboard-o
|
||||
import KeyboardShift from "@graphite-frontend/assets/icon-12px-solid/keyboard-shift.svg";
|
||||
import KeyboardSpace from "@graphite-frontend/assets/icon-12px-solid/keyboard-space.svg";
|
||||
import KeyboardTab from "@graphite-frontend/assets/icon-12px-solid/keyboard-tab.svg";
|
||||
import License12px from "@graphite-frontend/assets/icon-12px-solid/license-12px.svg";
|
||||
import Link from "@graphite-frontend/assets/icon-12px-solid/link.svg";
|
||||
import Overlays from "@graphite-frontend/assets/icon-12px-solid/overlays.svg";
|
||||
import Remove from "@graphite-frontend/assets/icon-12px-solid/remove.svg";
|
||||
@@ -47,9 +50,11 @@ const SOLID_12PX = {
|
||||
Add: { svg: Add, size: 12 },
|
||||
Checkmark: { svg: Checkmark, size: 12 },
|
||||
CloseX: { svg: CloseX, size: 12 },
|
||||
Delay: { svg: Delay, size: 12 },
|
||||
DropdownArrow: { svg: DropdownArrow, size: 12 },
|
||||
Edit12px: { svg: Edit12px, size: 12 },
|
||||
Empty12px: { svg: Empty12px, size: 12 },
|
||||
Failure: { svg: Failure, size: 12 },
|
||||
FullscreenEnter: { svg: FullscreenEnter, size: 12 },
|
||||
FullscreenExit: { svg: FullscreenExit, size: 12 },
|
||||
Grid: { svg: Grid, size: 12 },
|
||||
@@ -66,6 +71,7 @@ const SOLID_12PX = {
|
||||
KeyboardShift: { svg: KeyboardShift, size: 12 },
|
||||
KeyboardSpace: { svg: KeyboardSpace, size: 12 },
|
||||
KeyboardTab: { svg: KeyboardTab, size: 12 },
|
||||
License12px: { svg: License12px, size: 12 },
|
||||
Link: { svg: Link, size: 12 },
|
||||
Overlays: { svg: Overlays, size: 12 },
|
||||
Remove: { svg: Remove, size: 12 },
|
||||
@@ -95,6 +101,7 @@ import BooleanUnion from "@graphite-frontend/assets/icon-16px-solid/boolean-unio
|
||||
import CheckboxChecked from "@graphite-frontend/assets/icon-16px-solid/checkbox-checked.svg";
|
||||
import CheckboxUnchecked from "@graphite-frontend/assets/icon-16px-solid/checkbox-unchecked.svg";
|
||||
import Copy from "@graphite-frontend/assets/icon-16px-solid/copy.svg";
|
||||
import Credits from "@graphite-frontend/assets/icon-16px-solid/credits.svg";
|
||||
import CustomColor from "@graphite-frontend/assets/icon-16px-solid/custom-color.svg";
|
||||
import Edit from "@graphite-frontend/assets/icon-16px-solid/edit.svg";
|
||||
import Eyedropper from "@graphite-frontend/assets/icon-16px-solid/eyedropper.svg";
|
||||
@@ -107,13 +114,15 @@ import Folder from "@graphite-frontend/assets/icon-16px-solid/folder.svg";
|
||||
import GraphiteLogo from "@graphite-frontend/assets/icon-16px-solid/graphite-logo.svg";
|
||||
import GraphViewClosed from "@graphite-frontend/assets/icon-16px-solid/graph-view-closed.svg";
|
||||
import GraphViewOpen from "@graphite-frontend/assets/icon-16px-solid/graph-view-open.svg";
|
||||
import IconsGrid from "@graphite-frontend/assets/icon-16px-solid/icons-grid.svg";
|
||||
import Image from "@graphite-frontend/assets/icon-16px-solid/image.svg";
|
||||
import Layer from "@graphite-frontend/assets/icon-16px-solid/layer.svg";
|
||||
import License from "@graphite-frontend/assets/icon-16px-solid/license.svg";
|
||||
import NodeArtboard from "@graphite-frontend/assets/icon-16px-solid/node-artboard.svg";
|
||||
import NodeBlur from "@graphite-frontend/assets/icon-16px-solid/node-blur.svg";
|
||||
import NodeBrushwork from "@graphite-frontend/assets/icon-16px-solid/node-brushwork.svg";
|
||||
import NodeColorCorrection from "@graphite-frontend/assets/icon-16px-solid/node-color-correction.svg";
|
||||
import NodeGradient from "@graphite-frontend/assets/icon-16px-solid/node-gradient.svg";
|
||||
import Image from "@graphite-frontend/assets/icon-16px-solid/image.svg";
|
||||
import NodeImaginate from "@graphite-frontend/assets/icon-16px-solid/node-imaginate.svg";
|
||||
import NodeMagicWand from "@graphite-frontend/assets/icon-16px-solid/node-magic-wand.svg";
|
||||
import NodeMask from "@graphite-frontend/assets/icon-16px-solid/node-mask.svg";
|
||||
@@ -137,6 +146,8 @@ import ViewModePixels from "@graphite-frontend/assets/icon-16px-solid/view-mode-
|
||||
import ViewportDesignMode from "@graphite-frontend/assets/icon-16px-solid/viewport-design-mode.svg";
|
||||
import ViewportGuideMode from "@graphite-frontend/assets/icon-16px-solid/viewport-guide-mode.svg";
|
||||
import ViewportSelectMode from "@graphite-frontend/assets/icon-16px-solid/viewport-select-mode.svg";
|
||||
import Volunteer from "@graphite-frontend/assets/icon-16px-solid/volunteer.svg";
|
||||
import Website from "@graphite-frontend/assets/icon-16px-solid/website.svg";
|
||||
import WorkingColorsPrimary from "@graphite-frontend/assets/icon-16px-solid/working-colors-primary.svg";
|
||||
import WorkingColorsSecondary from "@graphite-frontend/assets/icon-16px-solid/working-colors-secondary.svg";
|
||||
import ZoomIn from "@graphite-frontend/assets/icon-16px-solid/zoom-in.svg";
|
||||
@@ -158,6 +169,7 @@ const SOLID_16PX = {
|
||||
CheckboxChecked: { svg: CheckboxChecked, size: 16 },
|
||||
CheckboxUnchecked: { svg: CheckboxUnchecked, size: 16 },
|
||||
Copy: { svg: Copy, size: 16 },
|
||||
Credits: { svg: Credits, size: 16 },
|
||||
CustomColor: { svg: CustomColor, size: 16 },
|
||||
Edit: { svg: Edit, size: 16 },
|
||||
Eyedropper: { svg: Eyedropper, size: 16 },
|
||||
@@ -170,13 +182,15 @@ const SOLID_16PX = {
|
||||
GraphiteLogo: { svg: GraphiteLogo, size: 16 },
|
||||
GraphViewClosed: { svg: GraphViewClosed, size: 16 },
|
||||
GraphViewOpen: { svg: GraphViewOpen, size: 16 },
|
||||
IconsGrid: { svg: IconsGrid, size: 16 },
|
||||
Image: { svg: Image, size: 16 },
|
||||
Layer: { svg: Layer, size: 16 },
|
||||
License: { svg: License, size: 16 },
|
||||
NodeArtboard: { svg: NodeArtboard, size: 16 },
|
||||
NodeBlur: { svg: NodeBlur, size: 16 },
|
||||
NodeBrushwork: { svg: NodeBrushwork, size: 16 },
|
||||
NodeColorCorrection: { svg: NodeColorCorrection, size: 16 },
|
||||
NodeGradient: { svg: NodeGradient, size: 16 },
|
||||
Image: { svg: Image, size: 16 },
|
||||
NodeImaginate: { svg: NodeImaginate, size: 16 },
|
||||
NodeMagicWand: { svg: NodeMagicWand, size: 16 },
|
||||
NodeMask: { svg: NodeMask, size: 16 },
|
||||
@@ -200,6 +214,8 @@ const SOLID_16PX = {
|
||||
ViewportDesignMode: { svg: ViewportDesignMode, size: 16 },
|
||||
ViewportGuideMode: { svg: ViewportGuideMode, size: 16 },
|
||||
ViewportSelectMode: { svg: ViewportSelectMode, size: 16 },
|
||||
Volunteer: { svg: Volunteer, size: 16 },
|
||||
Website: { svg: Website, size: 16 },
|
||||
WorkingColorsPrimary: { svg: WorkingColorsPrimary, size: 16 },
|
||||
WorkingColorsSecondary: { svg: WorkingColorsSecondary, size: 16 },
|
||||
ZoomIn: { svg: ZoomIn, size: 16 },
|
||||
|
||||
@@ -40,6 +40,8 @@ export async function rasterizeSVGCanvas(svg: string, width: number, height: num
|
||||
|
||||
// Rasterize the string of an SVG document at a given width and height and turn it into the blob data of an image file matching the given MIME type
|
||||
export async function rasterizeSVG(svg: string, width: number, height: number, mime: string, backgroundColor?: string): Promise<Blob> {
|
||||
if (!width || !height) throw new Error("Width and height must be nonzero when given to rasterizeSVG()");
|
||||
|
||||
const canvas = await rasterizeSVGCanvas(svg, width, height, backgroundColor);
|
||||
|
||||
// Convert the canvas to an image of the correct MIME type
|
||||
|
||||
@@ -103,7 +103,7 @@ export function createEditor() {
|
||||
instance.openDocumentFile(filename, content);
|
||||
|
||||
// Remove the hash fragment from the URL
|
||||
window.location.hash = "";
|
||||
history.replaceState("", "", `${window.location.pathname}${window.location.search}`);
|
||||
} catch {}
|
||||
})();
|
||||
|
||||
|
||||
@@ -425,13 +425,10 @@ export class UpdateActiveDocument extends JsMessage {
|
||||
|
||||
export class DisplayDialogPanic extends JsMessage {
|
||||
readonly panicInfo!: string;
|
||||
|
||||
readonly header!: string;
|
||||
|
||||
readonly description!: string;
|
||||
}
|
||||
|
||||
export class DisplayDialog extends JsMessage {
|
||||
readonly title!: string;
|
||||
readonly icon!: IconName;
|
||||
}
|
||||
|
||||
@@ -1049,6 +1046,8 @@ export class TextButton extends WidgetProps {
|
||||
|
||||
emphasized!: boolean;
|
||||
|
||||
noBackground!: boolean;
|
||||
|
||||
minWidth!: number;
|
||||
|
||||
disabled!: boolean;
|
||||
@@ -1066,6 +1065,7 @@ export type TextButtonWidget = {
|
||||
label: string;
|
||||
icon?: IconName;
|
||||
emphasized?: boolean;
|
||||
noBackground?: boolean;
|
||||
minWidth?: number;
|
||||
disabled?: boolean;
|
||||
tooltip?: string;
|
||||
@@ -1222,7 +1222,7 @@ export function defaultWidgetLayout(): WidgetLayout {
|
||||
}
|
||||
|
||||
// Updates a widget layout based on a list of updates, giving the new layout by mutating the `layout` argument
|
||||
export function patchWidgetLayout(/* mut */ layout: WidgetLayout, updates: WidgetDiffUpdate): void {
|
||||
export function patchWidgetLayout(layout: /* &mut */ WidgetLayout, updates: WidgetDiffUpdate): void {
|
||||
layout.layoutTarget = updates.layoutTarget;
|
||||
|
||||
updates.diff.forEach((update) => {
|
||||
@@ -1320,7 +1320,11 @@ function createLayoutGroup(layoutGroup: any): LayoutGroup {
|
||||
}
|
||||
|
||||
// WIDGET LAYOUTS
|
||||
export class UpdateDialogDetails extends WidgetDiffUpdate { }
|
||||
export class UpdateDialogButtons extends WidgetDiffUpdate { }
|
||||
|
||||
export class UpdateDialogColumn1 extends WidgetDiffUpdate { }
|
||||
|
||||
export class UpdateDialogColumn2 extends WidgetDiffUpdate { }
|
||||
|
||||
export class UpdateDocumentBarLayout extends WidgetDiffUpdate { }
|
||||
|
||||
@@ -1407,7 +1411,9 @@ export const messageMakers: Record<string, MessageMaker> = {
|
||||
TriggerViewportResize,
|
||||
TriggerVisitLink,
|
||||
UpdateActiveDocument,
|
||||
UpdateDialogDetails,
|
||||
UpdateDialogButtons,
|
||||
UpdateDialogColumn1,
|
||||
UpdateDialogColumn2,
|
||||
UpdateDocumentArtboards,
|
||||
UpdateDocumentArtwork,
|
||||
UpdateDocumentBarLayout,
|
||||
|
||||
Reference in New Issue
Block a user