Dialog redesign and content revamp (#1409)
* Revamp the content and design of dialogs * Add the Licenses dialog
@@ -1,7 +1,5 @@
|
||||
Copyright (c) 2021-2023 Keavon Chambers
|
||||
Copyright (c) 2021-2023 Graphite Labs, LLC.
|
||||
|
||||
The design assets in this directory (including SVG code for icons and logos) are NOT licensed under the Apache 2.0 license terms applied to other Graphite source code files. This directory and its entire contents are excluded from the Apache 2.0 source code license, and copyrights are held by the author for the creative works contained as files herein.
|
||||
The design assets in this directory (including SVG code for icons and logos) are NOT licensed under the Apache 2.0 license terms applied to other Graphite source code files. This directory and its entire contents are excluded from the Apache 2.0 source code license, and full copyright is held by the rightsholder for the creative works contained as files herein.
|
||||
|
||||
Parties interested in using Graphite source code in a capacity that deploys the Graphite editor reference frontend are advised to substitute all assets and "Graphite" branding or otherwise arrange written permission from the rightsholder. The recommended use case for adopting Graphite open source code is to develop one's own unique frontend user interface implementation that integrates Graphite's backend technology.
|
||||
|
||||
The author and rightsholder, Keavon Chambers, may be reached through the email address listed at https://graphite.rs/contact/ or https://keavon.com.
|
||||
Parties interested in using Graphite source code in a capacity that deploys the Graphite editor reference frontend are advised to substitute all assets and "Graphite" branding or otherwise arrange written permission from the rightsholder (see https://graphite.rs/contact/ for contact info). The recommended use case for adopting Graphite open source code is to develop one's own unique frontend user interface implementation that integrates Graphite's backend technology.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||
<path d="m6,1c2.76,0,5,2.24,5,5s-2.24,5-5,5S1,8.76,1,6,3.24,1,6,1m0-1C2.68,0,0,2.69,0,6s2.68,6,6,6,6-2.68,6-6S9.31,0,6,0h0Zm2.47,8.47c.2-.2.2-.51,0-.71l-1.97-1.97v-2.79c0-.28-.22-.5-.5-.5s-.5.22-.5.5v3c0,.13.05.26.15.35l2.12,2.12c.1.1.23.15.35.15s.26-.05.35-.15z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 336 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||
<path d="m6,1c2.76,0,5,2.24,5,5s-2.24,5-5,5S1,8.76,1,6c0-2.76,2.24-5,5-5m0-1C2.69,0,0,2.69,0,6s2.69,6,6,6,6-2.69,6-6S9.31,0,6,0m2.83,3.88l-.71-.71-2.12,2.12-2.12-2.12-.71.71,2.12,2.12-2.12,2.12.71.71,2.12-2.12,2.12,2.12.71-.71-2.12-2.12,2.12-2.12z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 321 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12">
|
||||
<path d="m12,1.63C11.94.7,11.17,0,10.25,0h-6.25C2.9,0,2,.9,2,2v6.5H0v1.75c0,.92.7,1.69,1.62,1.75h6.12c.95-.05,1.7-.8,1.75-1.75v-6.25h2.5V1.63ZM1.63,11c-.37-.06-.63-.38-.62-.75v-.75h5v.75c0,.26.07.52.19.75H1.63Zm6.12,0c-.39-.05-.7-.36-.75-.75v-1.75h-4V2c0-.55.45-1,1-1h4.71c-.14.31-.21.66-.21,1v8.25c-.05.39-.36.7-.75.75m3.25-8h-1.5v-1c0-.3.07-1,.75-1,.37,0,.69.26.75.62v1.37Zm-3.75-.5h-3c-.14,0-.25.11-.25.25s.11.25.25.25h3c.14,0,.25-.11.25-.25s-.11-.25-.25-.25m0,2h-3c-.14,0-.25.11-.25.25s.11.25.25.25h3c.14,0,.25-.11.25-.25s-.11-.25-.25-.25m0,2h-3c-.14,0-.25.11-.25.25s.11.25.25.25h3c.14,0,.25-.11.25-.25s-.11-.25-.25-.25z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 698 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path d="m2,1h6v2H2V1Zm0,12h7v2H2v-2ZM1,5h3v2H1v-2Zm-1,4h5v2H0v-2Zm7,0h4v2h-4v-2ZM10,1h4v2h-4V1Zm1,12h3v2h-3v-2ZM6,5h9v2H6v-2Zm7,4h3v2h-3v-2z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 215 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path d="m6,7H2c-.55,0-1-.45-1-1V2c0-.55.45-1,1-1h4c.55,0,1,.45,1,1v4c0,.55-.45,1-1,1ZM2,2v4h4V2H2Zm4,13H2c-.55,0-1-.45-1-1v-4c0-.55.45-1,1-1h4c.55,0,1,.45,1,1v4c0,.55-.45,1-1,1Zm-4-5v4h4v-4H2Zm12-3h-4c-.55,0-1-.45-1-1V2c0-.55.45-1,1-1h4c.55,0,1,.45,1,1v4c0,.55-.45,1-1,1Zm-4-5v4h4V2h-4Zm4,13h-4c-.55,0-1-.45-1-1v-4c0-.55.45-1,1-1h4c.55,0,1,.45,1,1v4c0,.55-.45,1-1,1Zm-4-5v4h4v-4h-4z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 457 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path d="m16,2c0-1.1-.9-2-2-2H5.5c-1.38,0-2.5,1.12-2.5,2.5v9.5H0v2c0,1.1.9,2,2,2h9c1.1,0,2-.9,2-2V5h3v-3ZM2,15c-.55,0-1-.45-1-1v-1h8v1c0,.35.09.7.27,1H2Zm9,0c-.55,0-1-.45-1-1v-2h-6V2.5c0-.83.67-1.5,1.5-1.5h6.82c-.22.47-.33.98-.32,1.5v11.5c0,.55-.45,1-1,1m4-11h-2v-1.5c0-1.5.75-1.5,1-1.5.55,0,1,.45,1,1v2Zm-4.5-1h-5c-.28,0-.5.22-.5.5s.22.5.5.5h5c.28,0,.5-.22.5-.5s-.22-.5-.5-.5m0,3h-5c-.28,0-.5.22-.5.5s.22.5.5.5h5c.28,0,.5-.22.5-.5s-.22-.5-.5-.5m0,3h-5c-.28,0-.5.22-.5.5s.22.5.5.5h5c.28,0,.5-.22.5-.5s-.22-.5-.5-.5z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 589 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path d="m4.56,15.92c1.01.06,1.28.08,2.53.08,2.95,0,3.89.02,6.12-.08.23-1.32.23-2.68,0-4-.44-2.54.53-2.46,1.4-4.45,1.54-3.49,1.39-4.97,1.39-4.97.07-.76-.48-1.43-1.24-1.51-.75.08-1.3.75-1.24,1.51-.14,1.73-1.05,3.29-2.49,4.26-2.36,1.66-5.5.45-8.39,2.83C.67,11.21.51,15.02.49,15.51c.82.19,1.65.29,2.49.32.08-1.09.35-2.16.8-3.16,0,0,.75.61.77,3.25ZM7.65,0c1.74,0,3.15,1.41,3.15,3.15s-1.41,3.15-3.15,3.15-3.15-1.41-3.15-3.15S5.91,0,7.65,0z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 508 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path d="m8,0C3.59,0,0,3.59,0,8s3.59,8,8,8,8-3.59,8-8S12.41,0,8,0Zm6.1,5h-2.39c-.26-1.33-.69-2.47-1.24-3.33,1.58.62,2.88,1.82,3.63,3.33Zm.7,3c0,.7-.11,1.37-.31,2h-2.62c.08-.64.12-1.31.12-2s-.04-1.36-.12-2h2.62c.2.63.31,1.3.31,2Zm-13.6,0c0-.7.11-1.37.31-2h2.62c-.08.64-.12,1.31-.12,2s.04,1.36.12,2H1.51c-.2-.63-.31-1.3-.31-2Zm3.9,0c0-.71.06-1.37.14-2h5.52c.08.63.14,1.29.14,2s-.06,1.37-.14,2h-5.52c-.08-.63-.14-1.29-.14-2ZM7.49,1.23c.17-.01.34-.03.51-.03s.34.01.51.03c.84.41,1.65,1.79,2.08,3.77h-5.17c.43-1.98,1.24-3.37,2.08-3.77Zm-1.96.44c-.55.86-.98,2-1.24,3.33H1.9c.75-1.51,2.05-2.71,3.63-3.33ZM1.9,11h2.39c.26,1.33.69,2.47,1.24,3.33-1.58-.62-2.88-1.82-3.63-3.33Zm6.61,3.78c-.17.01-.34.03-.51.03s-.34-.01-.51-.03c-.84-.4-1.65-1.79-2.08-3.78h5.17c-.43,1.98-1.24,3.37-2.08,3.78Zm1.96-.44c.55-.86.98-2,1.24-3.33h2.39c-.75,1.51-2.05,2.71-3.63,3.33z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 920 B |
@@ -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,
|
||||
|
||||
@@ -354,8 +354,11 @@ impl JsEditorHandle {
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = requestAboutGraphiteDialogWithLocalizedCommitDate)]
|
||||
pub fn request_about_graphite_dialog_with_localized_commit_date(&self, localized_commit_date: String) {
|
||||
let message = DialogMessage::RequestAboutGraphiteDialogWithLocalizedCommitDate { localized_commit_date };
|
||||
pub fn request_about_graphite_dialog_with_localized_commit_date(&self, localized_commit_date: String, localized_commit_year: String) {
|
||||
let message = DialogMessage::RequestAboutGraphiteDialogWithLocalizedCommitDate {
|
||||
localized_commit_date,
|
||||
localized_commit_year,
|
||||
};
|
||||
self.dispatch(message);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,24 +8,13 @@ use wasm_bindgen::prelude::*;
|
||||
|
||||
/// When a panic occurs, notify the user and log the error to the JS console before the backend dies
|
||||
pub fn panic_hook(info: &panic::PanicInfo) {
|
||||
let header = "The editor crashed — sorry about that";
|
||||
let description = "
|
||||
An internal error occurred. Please report this by filing an issue on GitHub.\n\
|
||||
\n\
|
||||
Reload the editor to continue. If this happens immediately on repeated reloads, clear saved data.
|
||||
"
|
||||
.trim();
|
||||
|
||||
error!("{}", info);
|
||||
|
||||
JS_EDITOR_HANDLES.with(|instances| {
|
||||
instances.borrow_mut().values_mut().for_each(|instance| {
|
||||
instance.send_frontend_message_to_js_rust_proxy(FrontendMessage::DisplayDialogPanic {
|
||||
panic_info: info.to_string(),
|
||||
header: header.to_string(),
|
||||
description: description.to_string(),
|
||||
})
|
||||
})
|
||||
instances
|
||||
.borrow_mut()
|
||||
.values_mut()
|
||||
.for_each(|instance| instance.send_frontend_message_to_js_rust_proxy(FrontendMessage::DisplayDialogPanic { panic_info: info.to_string() }))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||