mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-20 11:28:30 +08:00
Migrate dialogs to Rust and add a New File dialog (#623)
* Migrate coming soon and about dialog to Rust * Migrate confirm close and close all * Migrate dialog error * Improve keyboard navigation throughout UI * Cleanup and fix panic dialog * Reduce css spacing to better match old dialogs * Add new document modal * Fix crash when generating default name * Populate rust about graphite data on startup * Code review changes * Move one more :focus CSS rule into App.vue * Add a dialog message and move dialogs * Split out keyboard input navigation from this branch * Improvements including simplifying panic dialog code Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
parent
7729b6219e
commit
f177f63217
@@ -295,7 +295,7 @@ export default defineComponent({
|
||||
|
||||
// Initialize other stateful Vue systems
|
||||
const dialog = createDialogState(editor);
|
||||
const documents = createDocumentsState(editor, dialog);
|
||||
const documents = createDocumentsState(editor);
|
||||
const fullscreen = createFullscreenState();
|
||||
initErrorHandling(editor, dialog);
|
||||
createAutoSaveManager(editor, documents);
|
||||
|
||||
@@ -288,7 +288,8 @@ import {
|
||||
DisplayRemoveEditableTextbox,
|
||||
DisplayEditableTextbox,
|
||||
TriggerFontLoad,
|
||||
TriggerDefaultFontLoad,
|
||||
TriggerFontLoadDefault,
|
||||
TriggerVisitLink,
|
||||
} from "@/dispatcher/js-messages";
|
||||
|
||||
import { textInputCleanup } from "@/lifetime/input";
|
||||
@@ -466,7 +467,10 @@ export default defineComponent({
|
||||
const responseBuffer = await response.arrayBuffer();
|
||||
this.editor.instance.on_font_load(triggerFontLoad.font, new Uint8Array(responseBuffer), false);
|
||||
});
|
||||
this.editor.dispatcher.subscribeJsMessage(TriggerDefaultFontLoad, loadDefaultFont);
|
||||
this.editor.dispatcher.subscribeJsMessage(TriggerFontLoadDefault, loadDefaultFont);
|
||||
this.editor.dispatcher.subscribeJsMessage(TriggerVisitLink, async (triggerOpenLink) => {
|
||||
window.open(triggerOpenLink.url, "_blank");
|
||||
});
|
||||
this.editor.dispatcher.subscribeJsMessage(TriggerTextCopy, (triggerTextCopy) => {
|
||||
// If the Clipboard API is supported in the browser, copy text to the clipboard
|
||||
navigator.clipboard?.writeText?.(triggerTextCopy.copy_text);
|
||||
@@ -520,10 +524,31 @@ export default defineComponent({
|
||||
});
|
||||
});
|
||||
|
||||
// Gets metadat populated in `frontend/vue.config.js`. We could potentially move this functionality in a build.rs file.
|
||||
const loadBuildMetadata = (): void => {
|
||||
const release = process.env.VUE_APP_RELEASE_SERIES;
|
||||
let timestamp = "";
|
||||
const hash = (process.env.VUE_APP_COMMIT_HASH || "").substring(0, 8);
|
||||
const branch = process.env.VUE_APP_COMMIT_BRANCH;
|
||||
{
|
||||
const date = new Date(process.env.VUE_APP_COMMIT_DATE || "");
|
||||
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 timezoneName = Intl.DateTimeFormat(undefined, { timeZoneName: "long" })
|
||||
.formatToParts(new Date())
|
||||
.find((part) => part.type === "timeZoneName");
|
||||
const timezoneNameString = timezoneName?.value;
|
||||
timestamp = `${dateString} ${timeString} ${timezoneNameString}`;
|
||||
}
|
||||
|
||||
this.editor.instance.populate_build_metadata(release || "", timestamp, hash, branch || "");
|
||||
};
|
||||
|
||||
// TODO(mfish33): Replace with initialization system Issue:#524
|
||||
// Get initial Document Bar
|
||||
this.editor.instance.init_document_bar();
|
||||
setLoadDefaultFontCallback((font: string, data: Uint8Array) => this.editor.instance.on_font_load(font, data, true));
|
||||
loadBuildMetadata();
|
||||
},
|
||||
data() {
|
||||
const documentModeEntries: SectionsOfMenuListEntries = [
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
:data-index="index"
|
||||
:draggable="draggable"
|
||||
@dragstart="(e) => draggable && dragStart(e, listing.entry)"
|
||||
:title="`${listing.entry.name}\n${devMode ? 'Layer Path: ' + listing.entry.path.join(' / ') : ''}`"
|
||||
:title="`${listing.entry.name}\n${devMode ? 'Layer Path: ' + listing.entry.path.join(' / ') : ''}`.trim() || null"
|
||||
>
|
||||
<LayoutRow class="layer-type-icon">
|
||||
<IconLabel v-if="listing.entry.layer_type === 'Folder'" :icon="'NodeFolder'" title="Folder" />
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<LayoutCol class="properties">
|
||||
<LayoutRow class="options-bar">
|
||||
<WidgetLayout :layout="propertiesOptionsLayout"></WidgetLayout>
|
||||
<WidgetLayout :layout="propertiesOptionsLayout" />
|
||||
</LayoutRow>
|
||||
<LayoutRow class="sections" :scrollableY="true">
|
||||
<WidgetLayout :layout="propertiesSectionsLayout"></WidgetLayout>
|
||||
<WidgetLayout :layout="propertiesSectionsLayout" />
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
</template>
|
||||
|
||||
@@ -2,19 +2,7 @@
|
||||
<div class="widget-row">
|
||||
<template v-for="(component, index) in widgetData.widgets" :key="index">
|
||||
<!-- TODO: Use `<component :is="" v-bind="attributesObject"></component>` to avoid all the separate components with `v-if` -->
|
||||
<PopoverButton v-if="component.kind === 'PopoverButton'">
|
||||
<h3>{{ component.props.title }}</h3>
|
||||
<p>{{ component.props.text }}</p>
|
||||
</PopoverButton>
|
||||
<NumberInput
|
||||
v-if="component.kind === 'NumberInput'"
|
||||
v-bind="component.props"
|
||||
@update:value="(value: number) => updateLayout(component.widget_id, value)"
|
||||
:incrementCallbackIncrease="() => updateLayout(component.widget_id, 'Increment')"
|
||||
:incrementCallbackDecrease="() => updateLayout(component.widget_id, 'Decrement')"
|
||||
/>
|
||||
<TextInput v-if="component.kind === 'TextInput'" v-bind="component.props" @commitText="(value: string) => updateLayout(component.widget_id, value)" />
|
||||
<TextAreaInput v-if="component.kind === 'TextAreaInput'" v-bind="component.props" @commitText="(value: string) => updateLayout(component.widget_id, value)" />
|
||||
<CheckboxInput v-if="component.kind === 'CheckboxInput'" v-bind="component.props" @update:checked="(value: boolean) => updateLayout(component.widget_id, value)" />
|
||||
<ColorInput v-if="component.kind === 'ColorInput'" v-bind="component.props" @update:value="(value: string) => updateLayout(component.widget_id, value)" />
|
||||
<FontInput
|
||||
v-if="component.kind === 'FontInput'"
|
||||
@@ -22,26 +10,43 @@
|
||||
@changeFont="(value: { name: string, style: string, file: string }) => updateLayout(component.widget_id, value)"
|
||||
/>
|
||||
<IconButton v-if="component.kind === 'IconButton'" v-bind="component.props" :action="() => updateLayout(component.widget_id, null)" />
|
||||
<IconLabel v-if="component.kind === 'IconLabel'" v-bind="component.props" />
|
||||
<NumberInput
|
||||
v-if="component.kind === 'NumberInput'"
|
||||
v-bind="component.props"
|
||||
@update:value="(value: number) => updateLayout(component.widget_id, value)"
|
||||
:incrementCallbackIncrease="() => updateLayout(component.widget_id, 'Increment')"
|
||||
:incrementCallbackDecrease="() => updateLayout(component.widget_id, 'Decrement')"
|
||||
/>
|
||||
<OptionalInput v-if="component.kind === 'OptionalInput'" v-bind="component.props" @update:checked="(value: boolean) => updateLayout(component.widget_id, value)" />
|
||||
<PopoverButton v-if="component.kind === 'PopoverButton'">
|
||||
<h3>{{ component.props.title }}</h3>
|
||||
<p>{{ component.props.text }}</p>
|
||||
</PopoverButton>
|
||||
<RadioInput v-if="component.kind === 'RadioInput'" v-bind="component.props" @update:selectedIndex="(value: number) => updateLayout(component.widget_id, value)" />
|
||||
<Separator v-if="component.kind === 'Separator'" v-bind="component.props" />
|
||||
<TextLabel v-if="component.kind === 'TextLabel'" v-bind="component.props">{{ component.props.value }}</TextLabel>
|
||||
<IconLabel v-if="component.kind === 'IconLabel'" v-bind="component.props" />
|
||||
<TextAreaInput v-if="component.kind === 'TextAreaInput'" v-bind="component.props" @commitText="(value: string) => updateLayout(component.widget_id, value)" />
|
||||
<TextButton v-if="component.kind === 'TextButton'" v-bind="component.props" :action="() => updateLayout(component.widget_id, null)" />
|
||||
<TextInput v-if="component.kind === 'TextInput'" v-bind="component.props" @commitText="(value: string) => updateLayout(component.widget_id, value)" />
|
||||
<TextLabel v-if="component.kind === 'TextLabel'" v-bind="withoutValue(component.props)">{{ component.props.value }}</TextLabel>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.widget-row {
|
||||
min-height: 32px;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
min-height: 32px;
|
||||
|
||||
> * {
|
||||
--widget-height: 24px;
|
||||
min-height: var(--widget-height);
|
||||
line-height: var(--widget-height);
|
||||
margin: calc((24px - var(--widget-height)) / 2 + 4px) 0;
|
||||
min-height: var(--widget-height);
|
||||
|
||||
&:not(.multiline) {
|
||||
line-height: var(--widget-height);
|
||||
}
|
||||
|
||||
&.icon-label.size-12 {
|
||||
--widget-height: 12px;
|
||||
@@ -61,6 +66,8 @@ import { WidgetRow } from "@/dispatcher/js-messages";
|
||||
|
||||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
||||
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
|
||||
import TextButton from "@/components/widgets/buttons/TextButton.vue";
|
||||
import CheckboxInput from "@/components/widgets/inputs/CheckboxInput.vue";
|
||||
import ColorInput from "@/components/widgets/inputs/ColorInput.vue";
|
||||
import FontInput from "@/components/widgets/inputs/FontInput.vue";
|
||||
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
|
||||
@@ -82,10 +89,16 @@ export default defineComponent({
|
||||
updateLayout(widgetId: BigInt, value: unknown) {
|
||||
this.editor.instance.update_layout(this.layoutTarget, widgetId, value);
|
||||
},
|
||||
withoutValue(props: Record<string, unknown>): Record<string, unknown> {
|
||||
const { value: _, ...rest } = props;
|
||||
return rest;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Separator,
|
||||
PopoverButton,
|
||||
TextButton,
|
||||
CheckboxInput,
|
||||
NumberInput,
|
||||
TextInput,
|
||||
IconButton,
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<template>
|
||||
<button class="text-button" :class="{ emphasized, disabled }" :style="minWidth > 0 ? `min-width: ${minWidth}px` : ''" @click="(e: MouseEvent) => action(e)">
|
||||
<button
|
||||
class="text-button"
|
||||
:class="{ emphasized, disabled }"
|
||||
:data-emphasized="emphasized || null"
|
||||
:data-disabled="disabled || null"
|
||||
:style="minWidth > 0 ? `min-width: ${minWidth}px` : ''"
|
||||
@click="(e: MouseEvent) => action(e)"
|
||||
>
|
||||
<TextLabel>{{ label }}</TextLabel>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
<template>
|
||||
<FloatingMenu class="dialog-modal" :type="'Dialog'" :direction="'Center'" data-dialog-modal>
|
||||
<LayoutRow>
|
||||
<LayoutRow ref="main">
|
||||
<LayoutCol class="icon-column">
|
||||
<!-- `dialog.state.icon` class exists to provide special sizing in CSS to specific icons -->
|
||||
<IconLabel :icon="dialog.state.icon" :class="dialog.state.icon.toLowerCase()" />
|
||||
</LayoutCol>
|
||||
<LayoutCol class="main-column">
|
||||
<TextLabel :bold="true" class="heading">{{ dialog.state.heading }}</TextLabel>
|
||||
<TextLabel class="details">{{ dialog.state.details }}</TextLabel>
|
||||
<LayoutRow class="buttons-row" v-if="dialog.state.buttons.length > 0">
|
||||
<TextButton v-for="(button, index) in dialog.state.buttons" :key="index" :title="button.tooltip" :action="() => button.callback?.()" v-bind="button.props" />
|
||||
<WidgetLayout v-if="dialog.state.widgets.layout.length > 0" :layout="dialog.state.widgets" class="details" />
|
||||
<LayoutRow v-if="dialog.state.jsCallbackBasedButtons?.length > 0" class="panic-buttons-row">
|
||||
<TextButton v-for="(button, index) in dialog.state.jsCallbackBasedButtons" :key="index" :action="() => button.callback?.()" v-bind="button.props" />
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
</LayoutRow>
|
||||
@@ -49,21 +48,18 @@
|
||||
}
|
||||
|
||||
.main-column {
|
||||
.heading {
|
||||
user-select: text;
|
||||
white-space: pre-wrap;
|
||||
max-width: 400px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
margin: -4px 0;
|
||||
|
||||
.details {
|
||||
user-select: text;
|
||||
white-space: pre-wrap;
|
||||
max-width: 400px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.buttons-row {
|
||||
margin-top: 16px;
|
||||
.panic-buttons-row {
|
||||
height: 32px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +73,7 @@ import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import TextButton from "@/components/widgets/buttons/TextButton.vue";
|
||||
import FloatingMenu from "@/components/widgets/floating-menus/FloatingMenu.vue";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||
import WidgetLayout from "@/components/widgets/WidgetLayout.vue";
|
||||
|
||||
export default defineComponent({
|
||||
inject: ["dialog"],
|
||||
@@ -86,8 +82,8 @@ export default defineComponent({
|
||||
LayoutCol,
|
||||
FloatingMenu,
|
||||
IconLabel,
|
||||
TextLabel,
|
||||
TextButton,
|
||||
WidgetLayout,
|
||||
},
|
||||
methods: {
|
||||
dismiss() {
|
||||
|
||||
@@ -224,6 +224,8 @@ const MenuList = defineComponent({
|
||||
|
||||
const floatingMenu = this.$refs.floatingMenu as typeof FloatingMenu;
|
||||
|
||||
if (!floatingMenu) return;
|
||||
|
||||
// Save open/closed state before forcing open, if necessary, for measurement
|
||||
const initiallyOpen = floatingMenu.isOpen();
|
||||
if (!initiallyOpen) floatingMenu.setOpen();
|
||||
@@ -265,7 +267,9 @@ const MenuList = defineComponent({
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return { keyboardLockInfoMessage: this.fullscreen.keyboardLockApiSupported ? KEYBOARD_LOCK_USE_FULLSCREEN : KEYBOARD_LOCK_SWITCH_BROWSER };
|
||||
return {
|
||||
keyboardLockInfoMessage: this.fullscreen.keyboardLockApiSupported ? KEYBOARD_LOCK_USE_FULLSCREEN : KEYBOARD_LOCK_SWITCH_BROWSER,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
FloatingMenu,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<LayoutRow class="checkbox-input" :class="{ 'outline-style': outlineStyle }">
|
||||
<input type="checkbox" :id="`checkbox-input-${id}`" :checked="checked" @input="(e) => $emit('update:checked', (e.target as HTMLInputElement).checked)" />
|
||||
<input type="checkbox" :id="`checkbox-input-${id}`" :checked="checked" @change="(e) => $emit('update:checked', (e.target as HTMLInputElement).checked)" />
|
||||
<label :for="`checkbox-input-${id}`">
|
||||
<LayoutRow class="checkbox-box">
|
||||
<IconLabel :icon="icon" />
|
||||
@@ -12,6 +12,7 @@
|
||||
<style lang="scss">
|
||||
.checkbox-input {
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
display: none;
|
||||
@@ -19,6 +20,7 @@
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
height: 16px;
|
||||
|
||||
.checkbox-box {
|
||||
flex: 0 0 auto;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<LayoutRow class="dropdown-input">
|
||||
<LayoutRow class="font-input">
|
||||
<LayoutRow class="dropdown-box" :class="{ disabled }" :style="{ minWidth: `${minWidth}px` }" @click="() => clickDropdownBox()" data-hover-menu-spawner>
|
||||
<span>{{ activeEntry.label }}</span>
|
||||
<IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" />
|
||||
@@ -16,7 +16,7 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.dropdown-input {
|
||||
.font-input {
|
||||
position: relative;
|
||||
|
||||
.dropdown-box {
|
||||
|
||||
@@ -65,15 +65,7 @@ function makeMenuEntries(editor: EditorState): MenuListEntries {
|
||||
ref: undefined,
|
||||
children: [
|
||||
[
|
||||
{ label: "New", icon: "File", shortcut: ["KeyControl", "KeyN"], shortcutRequiresLock: true, action: (): void => editor.instance.new_document() },
|
||||
{
|
||||
label: "New 1920x1080",
|
||||
icon: "File",
|
||||
action: (): void => {
|
||||
editor.instance.new_document();
|
||||
editor.instance.create_artboard_and_fit_to_viewport(0, 0, 1920, 1080);
|
||||
},
|
||||
},
|
||||
{ label: "New…", icon: "File", shortcut: ["KeyControl", "KeyN"], shortcutRequiresLock: true, action: (): void => editor.instance.request_new_document_dialog() },
|
||||
{ label: "Open…", shortcut: ["KeyControl", "KeyO"], action: (): void => editor.instance.open_document() },
|
||||
{
|
||||
label: "Open Recent",
|
||||
@@ -168,7 +160,12 @@ function makeMenuEntries(editor: EditorState): MenuListEntries {
|
||||
label: "Help",
|
||||
ref: undefined,
|
||||
children: [
|
||||
[{ label: "About Graphite", action: async (): Promise<void> => editor.instance.request_about_graphite_dialog() }],
|
||||
[
|
||||
{
|
||||
label: "About Graphite",
|
||||
action: async (): Promise<void> => editor.instance.request_about_graphite_dialog(),
|
||||
},
|
||||
],
|
||||
[
|
||||
{ label: "Report a Bug", action: (): unknown => window.open("https://github.com/GraphiteEditor/Graphite/issues/new", "_blank") },
|
||||
{ label: "Visit on GitHub", action: (): unknown => window.open("https://github.com/GraphiteEditor/Graphite", "_blank") },
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<span class="text-label" :class="{ bold, italic }">
|
||||
<span class="text-label" :class="{ bold, italic, multiline, 'table-align': tableAlign }">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.text-label {
|
||||
white-space: nowrap;
|
||||
line-height: 18px;
|
||||
white-space: nowrap;
|
||||
|
||||
&.bold {
|
||||
font-weight: 700;
|
||||
@@ -16,6 +16,16 @@
|
||||
&.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&.multiline {
|
||||
white-space: pre-wrap;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
&.table-align {
|
||||
flex: 0 0 30%;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -26,6 +36,8 @@ export default defineComponent({
|
||||
props: {
|
||||
bold: { type: Boolean as PropType<boolean>, default: false },
|
||||
italic: { type: Boolean as PropType<boolean>, default: false },
|
||||
tableAlign: { type: Boolean as PropType<boolean>, default: false },
|
||||
multiline: { type: Boolean as PropType<boolean>, default: false },
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<MenuBarInput v-if="platform !== 'Mac'" />
|
||||
</LayoutRow>
|
||||
<LayoutRow class="header-part">
|
||||
<WindowTitle :title="`${activeDocumentDisplayName} - Graphite`" />
|
||||
<WindowTitle :text="`${activeDocumentDisplayName} - Graphite`" />
|
||||
</LayoutRow>
|
||||
<LayoutRow class="header-part">
|
||||
<WindowButtonsWindows :maximized="maximized" v-if="platform === 'Windows' || platform === 'Linux'" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<LayoutRow class="window-title">
|
||||
<span>{{ title }}</span>
|
||||
<span>{{ text }}</span>
|
||||
</LayoutRow>
|
||||
</template>
|
||||
|
||||
@@ -20,7 +20,7 @@ import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
title: { type: String as PropType<string>, required: true },
|
||||
text: { type: String as PropType<string>, required: true },
|
||||
},
|
||||
components: { LayoutRow },
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { Transform, Type } from "class-transformer";
|
||||
|
||||
import type { RustEditorInstance, WasmInstance } from "@/state/wasm-loader";
|
||||
import { IconName } from "@/utilities/icons";
|
||||
|
||||
export class JsMessage {
|
||||
// The marker provides a way to check if an object is a sub-class constructor for a jsMessage.
|
||||
@@ -143,12 +144,6 @@ export class UpdateActiveDocument extends JsMessage {
|
||||
readonly document_id!: BigInt;
|
||||
}
|
||||
|
||||
export class DisplayDialogError extends JsMessage {
|
||||
readonly title!: string;
|
||||
|
||||
readonly description!: string;
|
||||
}
|
||||
|
||||
export class DisplayDialogPanic extends JsMessage {
|
||||
readonly panic_info!: string;
|
||||
|
||||
@@ -157,14 +152,10 @@ export class DisplayDialogPanic extends JsMessage {
|
||||
readonly description!: string;
|
||||
}
|
||||
|
||||
export class DisplayConfirmationToCloseDocument extends JsMessage {
|
||||
readonly document_id!: BigInt;
|
||||
export class DisplayDialog extends JsMessage {
|
||||
readonly icon!: IconName;
|
||||
}
|
||||
|
||||
export class DisplayConfirmationToCloseAllDocuments extends JsMessage {}
|
||||
|
||||
export class DisplayDialogAboutGraphite extends JsMessage {}
|
||||
|
||||
export class UpdateDocumentArtwork extends JsMessage {
|
||||
readonly svg!: string;
|
||||
}
|
||||
@@ -388,7 +379,9 @@ export class IndexedDbDocumentDetails extends DocumentDetails {
|
||||
id!: string;
|
||||
}
|
||||
|
||||
export class TriggerDefaultFontLoad extends JsMessage {}
|
||||
export class TriggerFontLoadDefault extends JsMessage {}
|
||||
|
||||
export class DisplayDialogDismiss extends JsMessage {}
|
||||
|
||||
export class TriggerIndexedDbWriteDocument extends JsMessage {
|
||||
document!: string;
|
||||
@@ -409,9 +402,13 @@ export class TriggerFontLoad extends JsMessage {
|
||||
font!: string;
|
||||
}
|
||||
|
||||
export class TriggerVisitLink extends JsMessage {
|
||||
url!: string;
|
||||
}
|
||||
|
||||
export interface WidgetLayout {
|
||||
layout_target: unknown;
|
||||
layout: LayoutRow[];
|
||||
layout_target: unknown;
|
||||
}
|
||||
|
||||
export function defaultWidgetLayout(): WidgetLayout {
|
||||
@@ -434,18 +431,20 @@ export function isWidgetSection(layoutRow: WidgetRow | WidgetSection): layoutRow
|
||||
}
|
||||
|
||||
export type WidgetKind =
|
||||
| "NumberInput"
|
||||
| "Separator"
|
||||
| "IconButton"
|
||||
| "PopoverButton"
|
||||
| "OptionalInput"
|
||||
| "RadioInput"
|
||||
| "TextInput"
|
||||
| "TextAreaInput"
|
||||
| "TextLabel"
|
||||
| "IconLabel"
|
||||
| "CheckboxInput"
|
||||
| "ColorInput"
|
||||
| "FontInput";
|
||||
| "FontInput"
|
||||
| "IconButton"
|
||||
| "IconLabel"
|
||||
| "NumberInput"
|
||||
| "OptionalInput"
|
||||
| "PopoverButton"
|
||||
| "RadioInput"
|
||||
| "Separator"
|
||||
| "TextAreaInput"
|
||||
| "TextButton"
|
||||
| "TextInput"
|
||||
| "TextLabel";
|
||||
|
||||
export interface Widget {
|
||||
kind: WidgetKind;
|
||||
@@ -454,6 +453,13 @@ export interface Widget {
|
||||
props: any;
|
||||
}
|
||||
|
||||
export class UpdateDialogDetails extends JsMessage implements WidgetLayout {
|
||||
layout_target!: unknown;
|
||||
|
||||
@Transform(({ value }) => createWidgetLayout(value))
|
||||
layout!: LayoutRow[];
|
||||
}
|
||||
|
||||
export class UpdateToolOptionsLayout extends JsMessage implements WidgetLayout {
|
||||
layout_target!: unknown;
|
||||
|
||||
@@ -512,10 +518,6 @@ function createWidgetLayout(widgetLayout: any[]): LayoutRow[] {
|
||||
});
|
||||
}
|
||||
|
||||
export class DisplayDialogComingSoon extends JsMessage {
|
||||
issue: number | undefined;
|
||||
}
|
||||
|
||||
export class TriggerTextCommit extends JsMessage {}
|
||||
|
||||
export class TriggerTextCopy extends JsMessage {
|
||||
@@ -530,17 +532,14 @@ type JSMessageFactory = (data: any, wasm: WasmInstance, instance: RustEditorInst
|
||||
type MessageMaker = typeof JsMessage | JSMessageFactory;
|
||||
|
||||
export const messageMakers: Record<string, MessageMaker> = {
|
||||
DisplayConfirmationToCloseAllDocuments,
|
||||
DisplayConfirmationToCloseDocument,
|
||||
DisplayDialogAboutGraphite,
|
||||
DisplayDialogComingSoon,
|
||||
DisplayDialogError,
|
||||
DisplayDialog,
|
||||
DisplayDialogPanic,
|
||||
DisplayDocumentLayerTreeStructure: newDisplayDocumentLayerTreeStructure,
|
||||
DisplayEditableTextbox,
|
||||
UpdateImageData,
|
||||
DisplayRemoveEditableTextbox,
|
||||
TriggerDefaultFontLoad,
|
||||
TriggerFontLoadDefault,
|
||||
DisplayDialogDismiss,
|
||||
TriggerFileDownload,
|
||||
TriggerFileUpload,
|
||||
TriggerIndexedDbRemoveDocument,
|
||||
@@ -549,10 +548,12 @@ export const messageMakers: Record<string, MessageMaker> = {
|
||||
TriggerTextCommit,
|
||||
TriggerTextCopy,
|
||||
TriggerViewportResize,
|
||||
TriggerVisitLink,
|
||||
UpdateActiveDocument,
|
||||
UpdateActiveTool,
|
||||
UpdateCanvasRotation,
|
||||
UpdateCanvasZoom,
|
||||
UpdateDialogDetails,
|
||||
UpdateDocumentArtboards,
|
||||
UpdateDocumentArtwork,
|
||||
UpdateDocumentBarLayout,
|
||||
|
||||
@@ -1,54 +1,73 @@
|
||||
import { DisplayDialogError, DisplayDialogPanic } from "@/dispatcher/js-messages";
|
||||
import { DisplayDialogPanic, WidgetLayout } from "@/dispatcher/js-messages";
|
||||
import { DialogState } from "@/state/dialog";
|
||||
import { EditorState } from "@/state/wasm-loader";
|
||||
import { stripIndents } from "@/utilities/strip-indents";
|
||||
import { TextButtonWidget } from "@/utilities/widgets";
|
||||
|
||||
export function initErrorHandling(editor: EditorState, dialogState: DialogState): void {
|
||||
// Graphite error dialog
|
||||
editor.dispatcher.subscribeJsMessage(DisplayDialogError, (displayDialogError) => {
|
||||
const okButton: TextButtonWidget = {
|
||||
kind: "TextButton",
|
||||
callback: async () => dialogState.dismissDialog(),
|
||||
props: { label: "OK", emphasized: true, minWidth: 96 },
|
||||
};
|
||||
const buttons = [okButton];
|
||||
|
||||
dialogState.createDialog("Warning", displayDialogError.title, displayDialogError.description, buttons);
|
||||
});
|
||||
|
||||
// Code panic dialog and console error
|
||||
editor.dispatcher.subscribeJsMessage(DisplayDialogPanic, (displayDialogPanic) => {
|
||||
// `Error.stackTraceLimit` is only available in V8/Chromium
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(Error as any).stackTraceLimit = Infinity;
|
||||
const stackTrace = new Error().stack || "";
|
||||
const panicDetails = `${displayDialogPanic.panic_info}\n\n${stackTrace}`;
|
||||
const panicDetails = `${displayDialogPanic.panic_info}${stackTrace ? `\n\n${stackTrace}` : ""}`;
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(panicDetails);
|
||||
|
||||
const reloadButton: TextButtonWidget = {
|
||||
kind: "TextButton",
|
||||
callback: async () => window.location.reload(),
|
||||
props: { label: "Reload", emphasized: true, minWidth: 96 },
|
||||
};
|
||||
const copyErrorLogButton: TextButtonWidget = {
|
||||
kind: "TextButton",
|
||||
callback: async () => navigator.clipboard.writeText(panicDetails),
|
||||
props: { label: "Copy Error Log", emphasized: false, minWidth: 96 },
|
||||
};
|
||||
const reportOnGithubButton: TextButtonWidget = {
|
||||
kind: "TextButton",
|
||||
callback: async () => window.open(githubUrl(panicDetails), "_blank"),
|
||||
props: { label: "Report Bug", emphasized: false, minWidth: 96 },
|
||||
};
|
||||
const buttons = [reloadButton, copyErrorLogButton, reportOnGithubButton];
|
||||
|
||||
dialogState.createDialog("Warning", displayDialogPanic.title, displayDialogPanic.description, buttons);
|
||||
preparePanicDialog(dialogState, displayDialogPanic.title, displayDialogPanic.description, panicDetails);
|
||||
});
|
||||
}
|
||||
|
||||
function preparePanicDialog(dialogState: DialogState, title: string, details: string, panicDetails: string): void {
|
||||
const widgets: WidgetLayout = {
|
||||
layout: [
|
||||
{
|
||||
widgets: [
|
||||
{
|
||||
kind: "TextLabel",
|
||||
props: { value: title, bold: true },
|
||||
// eslint-disable-next-line camelcase
|
||||
widget_id: 0n,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
widgets: [
|
||||
{
|
||||
kind: "TextLabel",
|
||||
props: { value: details, multiline: true },
|
||||
// eslint-disable-next-line camelcase
|
||||
widget_id: 0n,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
// eslint-disable-next-line camelcase
|
||||
layout_target: null,
|
||||
};
|
||||
|
||||
const reloadButton: TextButtonWidget = {
|
||||
kind: "TextButton",
|
||||
callback: async () => window.location.reload(),
|
||||
props: { label: "Reload", emphasized: true, minWidth: 96 },
|
||||
};
|
||||
const copyErrorLogButton: TextButtonWidget = {
|
||||
kind: "TextButton",
|
||||
callback: async () => navigator.clipboard.writeText(panicDetails),
|
||||
props: { label: "Copy Error Log", emphasized: false, minWidth: 96 },
|
||||
};
|
||||
const reportOnGithubButton: TextButtonWidget = {
|
||||
kind: "TextButton",
|
||||
callback: async () => window.open(githubUrl(panicDetails), "_blank"),
|
||||
props: { label: "Report Bug", emphasized: false, minWidth: 96 },
|
||||
};
|
||||
const jsCallbackBasedButtons = [reloadButton, copyErrorLogButton, reportOnGithubButton];
|
||||
|
||||
dialogState.createPanicDialog(widgets, jsCallbackBasedButtons);
|
||||
}
|
||||
|
||||
function githubUrl(panicDetails: string): string {
|
||||
const url = new URL("https://github.com/GraphiteEditor/Graphite/issues/new");
|
||||
|
||||
|
||||
@@ -70,6 +70,10 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
|
||||
if (e.ctrlKey && e.shiftKey && key === "i") return false;
|
||||
if (e.ctrlKey && e.shiftKey && key === "j") return false;
|
||||
|
||||
// Don't redirect tab or enter if not in canvas (to allow navigating elements)
|
||||
const inCanvas = e.target instanceof Element && e.target.closest("[data-canvas]");
|
||||
if (!inCanvas && (key === "tab" || key === "enter")) return false;
|
||||
|
||||
// Redirect to the backend
|
||||
return true;
|
||||
};
|
||||
@@ -87,12 +91,6 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
|
||||
|
||||
if (dialog.dialogIsVisible()) {
|
||||
if (key === "escape") dialog.dismissDialog();
|
||||
if (key === "enter") {
|
||||
dialog.submitDialog();
|
||||
|
||||
// Prevent the Enter key from acting like a click on the last clicked button, which might reopen the dialog
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -113,6 +111,13 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
|
||||
const onPointerMove = (e: PointerEvent): void => {
|
||||
if (!e.buttons) viewportPointerInteractionOngoing = false;
|
||||
|
||||
// Don't redirect pointer movement to the backend if there's no ongoing interaction and it's over a floating menu on top of the canvas
|
||||
// TODO: A better approach is to pass along a boolean to the backend's input preprocessor so it can know if it's being occluded by the GUI.
|
||||
// TODO: This would allow it to properly decide to act on removing hover focus from something that was hovered in the canvas before moving over the GUI.
|
||||
// TODO: Further explanation: https://github.com/GraphiteEditor/Graphite/pull/623#discussion_r866436197
|
||||
const inFloatingMenu = e.target instanceof Element && e.target.closest("[data-floating-menu-content]");
|
||||
if (!viewportPointerInteractionOngoing && inFloatingMenu) return;
|
||||
|
||||
const modifiers = makeModifiersBitfield(e);
|
||||
editor.instance.on_mouse_move(e.clientX, e.clientY, e.buttons, modifiers);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { reactive, readonly } from "vue";
|
||||
|
||||
import { DisplayDialogAboutGraphite, DisplayDialogComingSoon } from "@/dispatcher/js-messages";
|
||||
import { defaultWidgetLayout, DisplayDialog, DisplayDialogDismiss, UpdateDialogDetails, WidgetLayout } from "@/dispatcher/js-messages";
|
||||
import { EditorState } from "@/state/wasm-loader";
|
||||
import { IconName } from "@/utilities/icons";
|
||||
import { stripIndents } from "@/utilities/strip-indents";
|
||||
import { TextButtonWidget } from "@/utilities/widgets";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
@@ -11,103 +10,47 @@ export function createDialogState(editor: EditorState) {
|
||||
const state = reactive({
|
||||
visible: false,
|
||||
icon: "" as IconName,
|
||||
heading: "",
|
||||
details: "",
|
||||
buttons: [] as TextButtonWidget[],
|
||||
widgets: defaultWidgetLayout(),
|
||||
// Special case for the crash dialog because we cannot handle button widget callbacks from Rust once the editor instance has panicked
|
||||
jsCallbackBasedButtons: undefined as undefined | TextButtonWidget[],
|
||||
});
|
||||
|
||||
const createDialog = (icon: IconName, heading: string, details: string, buttons: TextButtonWidget[]): void => {
|
||||
// Creates a panic dialog from JS.
|
||||
// Normal dialogs are created in the Rust backend, however for the crash dialog, the editor instance has panicked so it cannot respond to widget callbacks.
|
||||
const createPanicDialog = (widgets: WidgetLayout, jsCallbackBasedButtons: TextButtonWidget[]): void => {
|
||||
state.visible = true;
|
||||
state.icon = icon;
|
||||
state.heading = heading;
|
||||
state.details = details;
|
||||
state.buttons = buttons;
|
||||
state.icon = "Warning";
|
||||
state.widgets = widgets;
|
||||
state.jsCallbackBasedButtons = jsCallbackBasedButtons;
|
||||
};
|
||||
|
||||
const dismissDialog = (): void => {
|
||||
state.visible = false;
|
||||
};
|
||||
|
||||
const submitDialog = (): void => {
|
||||
const firstEmphasizedButton = state.buttons.find((button) => button.props.emphasized && button.callback);
|
||||
firstEmphasizedButton?.callback?.();
|
||||
};
|
||||
|
||||
const dialogIsVisible = (): boolean => state.visible;
|
||||
|
||||
const comingSoon = (issueNumber?: number): void => {
|
||||
const bugMessage = `— but you can help add it!\nSee issue #${issueNumber} on GitHub.`;
|
||||
const details = `This feature is not implemented yet${issueNumber ? bugMessage : ""}`;
|
||||
|
||||
const okButton: TextButtonWidget = {
|
||||
kind: "TextButton",
|
||||
callback: async () => dismissDialog(),
|
||||
props: { label: "OK", emphasized: true, minWidth: 96 },
|
||||
};
|
||||
const issueButton: TextButtonWidget = {
|
||||
kind: "TextButton",
|
||||
callback: async () => window.open(`https://github.com/GraphiteEditor/Graphite/issues/${issueNumber}`, "_blank"),
|
||||
props: { label: `Issue #${issueNumber}`, minWidth: 96 },
|
||||
};
|
||||
const buttons = issueNumber ? [okButton, issueButton] : [okButton];
|
||||
|
||||
createDialog("Warning", "Coming soon", details, buttons);
|
||||
};
|
||||
|
||||
const onAboutHandler = (): void => {
|
||||
const date = new Date(process.env.VUE_APP_COMMIT_DATE || "");
|
||||
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 timezoneName = Intl.DateTimeFormat(undefined, { timeZoneName: "long" })
|
||||
.formatToParts(new Date())
|
||||
.find((part) => part.type === "timeZoneName");
|
||||
const timezoneNameString = timezoneName?.value;
|
||||
|
||||
const hash = (process.env.VUE_APP_COMMIT_HASH || "").substring(0, 12);
|
||||
|
||||
const details = stripIndents`
|
||||
Release Series: ${process.env.VUE_APP_RELEASE_SERIES}
|
||||
|
||||
Date: ${dateString} ${timeString} ${timezoneNameString}
|
||||
Hash: ${hash}
|
||||
Branch: ${process.env.VUE_APP_COMMIT_BRANCH}
|
||||
`;
|
||||
|
||||
const buttons: TextButtonWidget[] = [
|
||||
{
|
||||
kind: "TextButton",
|
||||
callback: (): unknown => window.open("https://graphite.rs", "_blank"),
|
||||
props: { label: "Website", emphasized: false, minWidth: 0 },
|
||||
},
|
||||
{
|
||||
kind: "TextButton",
|
||||
callback: (): unknown => window.open("https://github.com/GraphiteEditor/Graphite/graphs/contributors", "_blank"),
|
||||
props: { label: "Credits", emphasized: false, minWidth: 0 },
|
||||
},
|
||||
{
|
||||
kind: "TextButton",
|
||||
callback: (): unknown => window.open("https://raw.githubusercontent.com/GraphiteEditor/Graphite/master/LICENSE.txt", "_blank"),
|
||||
props: { label: "License", emphasized: false, minWidth: 0 },
|
||||
},
|
||||
{
|
||||
kind: "TextButton",
|
||||
callback: (): unknown => window.open("/third-party-licenses.txt", "_blank"),
|
||||
props: { label: "Third-Party Licenses", emphasized: false, minWidth: 0 },
|
||||
},
|
||||
];
|
||||
|
||||
createDialog("GraphiteLogo", "Graphite", details, buttons);
|
||||
editor.instance.request_coming_soon_dialog(issueNumber);
|
||||
};
|
||||
|
||||
// Run on creation
|
||||
editor.dispatcher.subscribeJsMessage(DisplayDialogAboutGraphite, () => onAboutHandler());
|
||||
editor.dispatcher.subscribeJsMessage(DisplayDialogComingSoon, (displayDialogComingSoon) => comingSoon(displayDialogComingSoon.issue));
|
||||
editor.dispatcher.subscribeJsMessage(DisplayDialog, (displayDialog) => {
|
||||
state.visible = true;
|
||||
state.icon = displayDialog.icon;
|
||||
});
|
||||
|
||||
editor.dispatcher.subscribeJsMessage(DisplayDialogDismiss, dismissDialog);
|
||||
|
||||
editor.dispatcher.subscribeJsMessage(UpdateDialogDetails, (updateDialogDetails) => {
|
||||
state.widgets = updateDialogDetails;
|
||||
state.jsCallbackBasedButtons = undefined;
|
||||
});
|
||||
|
||||
return {
|
||||
state: readonly(state),
|
||||
createDialog,
|
||||
createPanicDialog,
|
||||
dismissDialog,
|
||||
submitDialog,
|
||||
dialogIsVisible,
|
||||
comingSoon,
|
||||
};
|
||||
|
||||
@@ -1,80 +1,18 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import { reactive, readonly } from "vue";
|
||||
|
||||
import {
|
||||
DisplayConfirmationToCloseAllDocuments,
|
||||
DisplayConfirmationToCloseDocument,
|
||||
TriggerFileDownload,
|
||||
FrontendDocumentDetails,
|
||||
TriggerFileUpload,
|
||||
UpdateActiveDocument,
|
||||
UpdateOpenDocumentsList,
|
||||
} from "@/dispatcher/js-messages";
|
||||
import { DialogState } from "@/state/dialog";
|
||||
import { TriggerFileDownload, FrontendDocumentDetails, TriggerFileUpload, UpdateActiveDocument, UpdateOpenDocumentsList } from "@/dispatcher/js-messages";
|
||||
import { EditorState } from "@/state/wasm-loader";
|
||||
import { download, upload } from "@/utilities/files";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export function createDocumentsState(editor: EditorState, dialogState: DialogState) {
|
||||
export function createDocumentsState(editor: EditorState) {
|
||||
const state = reactive({
|
||||
unsaved: false,
|
||||
documents: [] as FrontendDocumentDetails[],
|
||||
activeDocumentIndex: 0,
|
||||
});
|
||||
|
||||
const closeDocumentWithConfirmation = async (documentId: BigInt): Promise<void> => {
|
||||
// Assume we receive a correct document_id
|
||||
const targetDocument = state.documents.find((doc) => doc.id === documentId) as FrontendDocumentDetails;
|
||||
const tabLabel = targetDocument.displayName;
|
||||
|
||||
// Show the close confirmation prompt
|
||||
dialogState.createDialog("File", "Save changes before closing?", tabLabel, [
|
||||
{
|
||||
kind: "TextButton",
|
||||
callback: async (): Promise<void> => {
|
||||
editor.instance.save_document();
|
||||
dialogState.dismissDialog();
|
||||
},
|
||||
props: { label: "Save", emphasized: true, minWidth: 96 },
|
||||
},
|
||||
{
|
||||
kind: "TextButton",
|
||||
callback: async (): Promise<void> => {
|
||||
editor.instance.close_document(targetDocument.id);
|
||||
dialogState.dismissDialog();
|
||||
},
|
||||
props: { label: "Discard", minWidth: 96 },
|
||||
},
|
||||
{
|
||||
kind: "TextButton",
|
||||
callback: async (): Promise<void> => {
|
||||
dialogState.dismissDialog();
|
||||
},
|
||||
props: { label: "Cancel", minWidth: 96 },
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const closeAllDocumentsWithConfirmation = (): void => {
|
||||
dialogState.createDialog("Copy", "Close all documents?", "Unsaved work will be lost!", [
|
||||
{
|
||||
kind: "TextButton",
|
||||
callback: (): void => {
|
||||
editor.instance.close_all_documents();
|
||||
dialogState.dismissDialog();
|
||||
},
|
||||
props: { label: "Discard All", minWidth: 96 },
|
||||
},
|
||||
{
|
||||
kind: "TextButton",
|
||||
callback: (): void => {
|
||||
dialogState.dismissDialog();
|
||||
},
|
||||
props: { label: "Cancel", minWidth: 96 },
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
// Set up message subscriptions on creation
|
||||
editor.dispatcher.subscribeJsMessage(UpdateOpenDocumentsList, (updateOpenDocumentList) => {
|
||||
state.documents = updateOpenDocumentList.open_documents;
|
||||
@@ -86,14 +24,6 @@ export function createDocumentsState(editor: EditorState, dialogState: DialogSta
|
||||
state.activeDocumentIndex = activeId;
|
||||
});
|
||||
|
||||
editor.dispatcher.subscribeJsMessage(DisplayConfirmationToCloseDocument, (displayConfirmationToCloseDocument) => {
|
||||
closeDocumentWithConfirmation(displayConfirmationToCloseDocument.document_id);
|
||||
});
|
||||
|
||||
editor.dispatcher.subscribeJsMessage(DisplayConfirmationToCloseAllDocuments, () => {
|
||||
closeAllDocumentsWithConfirmation();
|
||||
});
|
||||
|
||||
editor.dispatcher.subscribeJsMessage(TriggerFileUpload, async () => {
|
||||
const extension = editor.rawWasm.file_save_suffix();
|
||||
const data = await upload(extension);
|
||||
@@ -110,7 +40,6 @@ export function createDocumentsState(editor: EditorState, dialogState: DialogSta
|
||||
|
||||
return {
|
||||
state: readonly(state),
|
||||
closeAllDocumentsWithConfirmation,
|
||||
};
|
||||
}
|
||||
export type DocumentsState = ReturnType<typeof createDocumentsState>;
|
||||
|
||||
@@ -66,11 +66,11 @@ export function getWasmInstance(): WasmInstance {
|
||||
}
|
||||
|
||||
type CreateEditorStateType = {
|
||||
/// Allows subscribing to messages from the WASM backend
|
||||
// Allows subscribing to messages from the WASM backend
|
||||
rawWasm: WasmInstance;
|
||||
/// Bindings to WASM wrapper declarations (generated by wasm-bindgen)
|
||||
// Bindings to WASM wrapper declarations (generated by wasm-bindgen)
|
||||
dispatcher: ReturnType<typeof createJsDispatcher>;
|
||||
/// WASM wrapper's exported functions (generated by wasm-bindgen)
|
||||
// WASM wrapper's exported functions (generated by wasm-bindgen)
|
||||
instance: RustEditorInstance;
|
||||
};
|
||||
export function createEditorState(): CreateEditorStateType {
|
||||
|
||||
Reference in New Issue
Block a user