Rework wasm initialization and reduce global state (#379)

* wasm: do the async initialization only once

This allows the rest of the app to access wasm synchronously.

This allows removing of a global.

* provide the wasm via vue provide/inject.

There's still code directly accessing the wasm. That will be changed later.

* MenuBarInput: use injected wasm instead of the global instance

* Let the App handle event listeners

* move stateful modules into state/

* state/fullscreen: create per instance

* App: load the initial document list on mount.
This got lost a few commits ago. Now it's back.

* state/dialog: create per instance

* util/input: remove dependency on global dialog instance

* state/documents: create per instance

* reponse-handler: move into EditorWasm

* comingSoon: move into dialog

* wasm: allow instantiating multiple editors

* input handlers: do not look at canvases outside the mounted App

* input: listen on the container instead of the window when possible

* - removed proxy from wasm-loader
- integrated with js-dispatcher
- state functions to classes
- integrated some upstream changes

* fix errors caused by merge

* Getting closer:
- added global state to track all instances
- fix fullscreen close trigger
- wasm-loader is statefull
- panic across instanes

* - fix outline while using editor
- removed circular import rule
- added editorInstance to js message constructor

* - changed input handler to a class
- still need a better way of handeling it in App.vue

* - fixed single instance of inputManager to weakmap

* - fix no-explicit-any in a few places
- removed global state from input.ts

* simplified two long lines

* removed global state

* removed $data from App

* add mut self to functions in api.rs

* Update Workspace.vue

remove outdated import

* fixed missing import

* Changes throughout code review; note this causes some bugs to be fixed in a later commit

* PR review round 1

* - fix coming soon bugs
- changed folder structure

* moved declaration to .d.ts

* - changed from classes to functions
- moved decs back to app.vue

* removed need to export js function to rust

* changed folder structure

* fixed indentation breaking multiline strings

* Fix eslint rule to whitelist @/../

* Simplify strip-indents implementation

* replace type assertions with better annotations or proper runtime checks

* Small tweaks and code rearranging improvements after second code review pass

* maybe fix mouse events

* Add back preventDefault for mouse scroll

* code review round 2

* Comment improvements

* -removed runtime checks
- fixed layers not showing

* - extened proxy to cover classes
- stopped multiple panics from logging
- Stop wasm-bindgen from mut ref counting our struct

* cleaned up messageConstructors exports

* Fix input and fullscreen regressions

Co-authored-by: Max Fisher <maxmfishernj@gmail.com>
Co-authored-by: mfish33 <32677537+mfish33@users.noreply.github.com>
Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Christian Authmann
2021-12-20 07:37:19 +01:00
committed by Keavon Chambers
parent 6d82672a95
commit 5ec8aaa31d
39 changed files with 1524 additions and 1412 deletions

View File

@@ -18,7 +18,7 @@
<Separator :type="SeparatorType.Unrelated" />
<OptionalInput v-model:checked="gridEnabled" @update:checked="comingSoon(318)" :icon="'Grid'" title="Grid" />
<OptionalInput v-model:checked="gridEnabled" @update:checked="dialog.comingSoon(318)" :icon="'Grid'" title="Grid" />
<PopoverButton>
<h3>Grid</h3>
<p>The contents of this popover menu are coming soon</p>
@@ -26,7 +26,7 @@
<Separator :type="SeparatorType.Unrelated" />
<OptionalInput v-model:checked="overlaysEnabled" @update:checked="comingSoon(99)" :icon="'Overlays'" title="Overlays" />
<OptionalInput v-model:checked="overlaysEnabled" @update:checked="dialog.comingSoon(99)" :icon="'Overlays'" title="Overlays" />
<PopoverButton>
<h3>Overlays</h3>
<p>The contents of this popover menu are coming soon</p>
@@ -70,31 +70,31 @@
<LayoutCol :class="'shelf'">
<div class="tools scrollable-y">
<ShelfItemInput icon="LayoutSelectTool" title="Select Tool (V)" :active="activeTool === 'Select'" :action="() => selectTool('Select')" />
<ShelfItemInput icon="LayoutCropTool" title="Crop Tool" :active="activeTool === 'Crop'" :action="() => comingSoon(289) && selectTool('Crop')" />
<ShelfItemInput icon="LayoutNavigateTool" title="Navigate Tool (Z)" :active="activeTool === 'Navigate'" :action="() => comingSoon(155) && selectTool('Navigate')" />
<ShelfItemInput icon="LayoutCropTool" title="Crop Tool" :active="activeTool === 'Crop'" :action="() => dialog.comingSoon(289) && selectTool('Crop')" />
<ShelfItemInput icon="LayoutNavigateTool" title="Navigate Tool (Z)" :active="activeTool === 'Navigate'" :action="() => dialog.comingSoon(155) && selectTool('Navigate')" />
<ShelfItemInput icon="LayoutEyedropperTool" title="Eyedropper Tool (I)" :active="activeTool === 'Eyedropper'" :action="() => selectTool('Eyedropper')" />
<Separator :type="SeparatorType.Section" :direction="SeparatorDirection.Vertical" />
<ShelfItemInput icon="ParametricTextTool" title="Text Tool (T)" :active="activeTool === 'Text'" :action="() => comingSoon(153) && selectTool('Text')" />
<ShelfItemInput icon="ParametricTextTool" title="Text Tool (T)" :active="activeTool === 'Text'" :action="() => dialog.comingSoon(153) && selectTool('Text')" />
<ShelfItemInput icon="ParametricFillTool" title="Fill Tool (F)" :active="activeTool === 'Fill'" :action="() => selectTool('Fill')" />
<ShelfItemInput icon="ParametricGradientTool" title="Gradient Tool (H)" :active="activeTool === 'Gradient'" :action="() => comingSoon() && selectTool('Gradient')" />
<ShelfItemInput icon="ParametricGradientTool" title="Gradient Tool (H)" :active="activeTool === 'Gradient'" :action="() => dialog.comingSoon() && selectTool('Gradient')" />
<Separator :type="SeparatorType.Section" :direction="SeparatorDirection.Vertical" />
<ShelfItemInput icon="RasterBrushTool" title="Brush Tool (B)" :active="activeTool === 'Brush'" :action="() => comingSoon() && selectTool('Brush')" />
<ShelfItemInput icon="RasterHealTool" title="Heal Tool (J)" :active="activeTool === 'Heal'" :action="() => comingSoon() && selectTool('Heal')" />
<ShelfItemInput icon="RasterCloneTool" title="Clone Tool (C)" :active="activeTool === 'Clone'" :action="() => comingSoon() && selectTool('Clone')" />
<ShelfItemInput icon="RasterPatchTool" title="Patch Tool" :active="activeTool === 'Patch'" :action="() => comingSoon() && selectTool('Patch')" />
<ShelfItemInput icon="RasterBlurSharpenTool" title="Detail Tool (D)" :active="activeTool === 'BlurSharpen'" :action="() => comingSoon() && selectTool('BlurSharpen')" />
<ShelfItemInput icon="RasterRelightTool" title="Relight Tool (O)" :active="activeTool === 'Relight'" :action="() => comingSoon() && selectTool('Relight')" />
<ShelfItemInput icon="RasterBrushTool" title="Brush Tool (B)" :active="activeTool === 'Brush'" :action="() => dialog.comingSoon() && selectTool('Brush')" />
<ShelfItemInput icon="RasterHealTool" title="Heal Tool (J)" :active="activeTool === 'Heal'" :action="() => dialog.comingSoon() && selectTool('Heal')" />
<ShelfItemInput icon="RasterCloneTool" title="Clone Tool (C)" :active="activeTool === 'Clone'" :action="() => dialog.comingSoon() && selectTool('Clone')" />
<ShelfItemInput icon="RasterPatchTool" title="Patch Tool" :active="activeTool === 'Patch'" :action="() => dialog.comingSoon() && selectTool('Patch')" />
<ShelfItemInput icon="RasterBlurSharpenTool" title="Detail Tool (D)" :active="activeTool === 'BlurSharpen'" :action="() => dialog.comingSoon() && selectTool('BlurSharpen')" />
<ShelfItemInput icon="RasterRelightTool" title="Relight Tool (O)" :active="activeTool === 'Relight'" :action="() => dialog.comingSoon() && selectTool('Relight')" />
<Separator :type="SeparatorType.Section" :direction="SeparatorDirection.Vertical" />
<ShelfItemInput icon="VectorPathTool" title="Path Tool (A)" :active="activeTool === 'Path'" :action="() => selectTool('Path')" />
<ShelfItemInput icon="VectorPenTool" title="Pen Tool (P)" :active="activeTool === 'Pen'" :action="() => selectTool('Pen')" />
<ShelfItemInput icon="VectorFreehandTool" title="Freehand Tool (N)" :active="activeTool === 'Freehand'" :action="() => comingSoon() && selectTool('Freehand')" />
<ShelfItemInput icon="VectorSplineTool" title="Spline Tool" :active="activeTool === 'Spline'" :action="() => comingSoon() && selectTool('Spline')" />
<ShelfItemInput icon="VectorFreehandTool" title="Freehand Tool (N)" :active="activeTool === 'Freehand'" :action="() => dialog.comingSoon() && selectTool('Freehand')" />
<ShelfItemInput icon="VectorSplineTool" title="Spline Tool" :active="activeTool === 'Spline'" :action="() => dialog.comingSoon() && selectTool('Spline')" />
<ShelfItemInput icon="VectorLineTool" title="Line Tool (L)" :active="activeTool === 'Line'" :action="() => selectTool('Line')" />
<ShelfItemInput icon="VectorRectangleTool" title="Rectangle Tool (M)" :active="activeTool === 'Rectangle'" :action="() => selectTool('Rectangle')" />
<ShelfItemInput icon="VectorEllipseTool" title="Ellipse Tool (E)" :active="activeTool === 'Ellipse'" :action="() => selectTool('Ellipse')" />
@@ -238,11 +238,8 @@
<script lang="ts">
import { defineComponent } from "vue";
import { subscribeJsMessage } from "@/utilities/js-message-dispatcher";
import { UpdateCanvas, UpdateScrollbars, UpdateRulers, SetActiveTool, SetCanvasZoom, SetCanvasRotation } from "@/utilities/js-messages";
import { UpdateCanvas, UpdateScrollbars, UpdateRulers, SetActiveTool, SetCanvasZoom, SetCanvasRotation } from "@/dispatcher/js-messages";
import { SeparatorDirection, SeparatorType } from "@/components/widgets/widgets";
import { comingSoon } from "@/utilities/errors";
import { panicProxy } from "@/utilities/panic-proxy";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import LayoutCol from "@/components/layout/LayoutCol.vue";
@@ -261,27 +258,13 @@ import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue";
import ToolOptions from "@/components/widgets/options/ToolOptions.vue";
import { SectionsOfMenuListEntries } from "@/components/widgets/floating-menus/MenuList.vue";
const wasm = import("@/../wasm/pkg").then(panicProxy);
const documentModeEntries: SectionsOfMenuListEntries = [
[
{ label: "Design Mode", icon: "ViewportDesignMode" },
{ label: "Select Mode", icon: "ViewportSelectMode", action: () => comingSoon(330) },
{ label: "Guide Mode", icon: "ViewportGuideMode", action: () => comingSoon(331) },
],
];
const viewModeEntries: RadioEntries = [
{ value: "normal", icon: "ViewModeNormal", tooltip: "View Mode: Normal" },
{ value: "outline", icon: "ViewModeOutline", tooltip: "View Mode: Outline", action: () => comingSoon(319) },
{ value: "pixels", icon: "ViewModePixels", tooltip: "View Mode: Pixels", action: () => comingSoon(320) },
];
export default defineComponent({
inject: ["editor", "dialog"],
methods: {
async setSnap(newStatus: boolean) {
(await wasm).set_snapping(newStatus);
setSnap(newStatus: boolean) {
this.editor.instance.set_snapping(newStatus);
},
async viewportResize() {
viewportResize() {
const canvas = this.$refs.canvas as HTMLElement;
// Get the width and height rounded up to the nearest even number because resizing is centered and dividing an odd number by 2 for centering causes antialiasing
let width = Math.ceil(parseFloat(getComputedStyle(canvas).width));
@@ -292,73 +275,73 @@ export default defineComponent({
this.canvasSvgWidth = `${width}px`;
this.canvasSvgHeight = `${height}px`;
},
async setCanvasZoom(newZoom: number) {
(await wasm).set_canvas_zoom(newZoom / 100);
setCanvasZoom(newZoom: number) {
this.editor.instance.set_canvas_zoom(newZoom / 100);
},
async increaseCanvasZoom() {
(await wasm).increase_canvas_zoom();
increaseCanvasZoom() {
this.editor.instance.increase_canvas_zoom();
},
async decreaseCanvasZoom() {
(await wasm).decrease_canvas_zoom();
decreaseCanvasZoom() {
this.editor.instance.decrease_canvas_zoom();
},
async setRotation(newRotation: number) {
(await wasm).set_rotation(newRotation * (Math.PI / 180));
setRotation(newRotation: number) {
this.editor.instance.set_rotation(newRotation * (Math.PI / 180));
},
async translateCanvasX(newValue: number) {
translateCanvasX(newValue: number) {
const delta = newValue - this.scrollbarPos.x;
this.scrollbarPos.x = newValue;
(await wasm).translate_canvas(-delta * this.scrollbarMultiplier.x, 0);
this.editor.instance.translate_canvas(-delta * this.scrollbarMultiplier.x, 0);
},
async translateCanvasY(newValue: number) {
translateCanvasY(newValue: number) {
const delta = newValue - this.scrollbarPos.y;
this.scrollbarPos.y = newValue;
(await wasm).translate_canvas(0, -delta * this.scrollbarMultiplier.y);
this.editor.instance.translate_canvas(0, -delta * this.scrollbarMultiplier.y);
},
async pageX(delta: number) {
pageX(delta: number) {
const move = delta < 0 ? 1 : -1;
(await wasm).translate_canvas_by_fraction(move, 0);
this.editor.instance.translate_canvas_by_fraction(move, 0);
},
async pageY(delta: number) {
pageY(delta: number) {
const move = delta < 0 ? 1 : -1;
(await wasm).translate_canvas_by_fraction(0, move);
this.editor.instance.translate_canvas_by_fraction(0, move);
},
async selectTool(toolName: string) {
(await wasm).select_tool(toolName);
selectTool(toolName: string) {
this.editor.instance.select_tool(toolName);
},
async swapWorkingColors() {
(await wasm).swap_colors();
swapWorkingColors() {
this.editor.instance.swap_colors();
},
async resetWorkingColors() {
(await wasm).reset_colors();
resetWorkingColors() {
this.editor.instance.reset_colors();
},
},
mounted() {
subscribeJsMessage(UpdateCanvas, (updateCanvas) => {
this.editor.dispatcher.subscribeJsMessage(UpdateCanvas, (updateCanvas) => {
this.viewportSvg = updateCanvas.document;
});
subscribeJsMessage(UpdateScrollbars, (updateScrollbars) => {
this.editor.dispatcher.subscribeJsMessage(UpdateScrollbars, (updateScrollbars) => {
this.scrollbarPos = updateScrollbars.position;
this.scrollbarSize = updateScrollbars.size;
this.scrollbarMultiplier = updateScrollbars.multiplier;
});
subscribeJsMessage(UpdateRulers, (updateRulers) => {
this.editor.dispatcher.subscribeJsMessage(UpdateRulers, (updateRulers) => {
this.rulerOrigin = updateRulers.origin;
this.rulerSpacing = updateRulers.spacing;
this.rulerInterval = updateRulers.interval;
});
subscribeJsMessage(SetActiveTool, (setActiveTool) => {
this.editor.dispatcher.subscribeJsMessage(SetActiveTool, (setActiveTool) => {
this.activeTool = setActiveTool.tool_name;
this.activeToolOptions = setActiveTool.tool_options;
});
subscribeJsMessage(SetCanvasZoom, (setCanvasZoom) => {
this.editor.dispatcher.subscribeJsMessage(SetCanvasZoom, (setCanvasZoom) => {
this.documentZoom = setCanvasZoom.new_zoom * 100;
});
subscribeJsMessage(SetCanvasRotation, (setCanvasRotation) => {
this.editor.dispatcher.subscribeJsMessage(SetCanvasRotation, (setCanvasRotation) => {
const newRotation = setCanvasRotation.new_radians * (180 / Math.PI);
this.documentRotation = (360 + (newRotation % 360)) % 360;
});
@@ -367,6 +350,19 @@ export default defineComponent({
window.addEventListener("DOMContentLoaded", this.viewportResize);
},
data() {
const documentModeEntries: SectionsOfMenuListEntries = [
[
{ label: "Design Mode", icon: "ViewportDesignMode" },
{ label: "Select Mode", icon: "ViewportSelectMode", action: () => this.dialog.comingSoon(330) },
{ label: "Guide Mode", icon: "ViewportGuideMode", action: () => this.dialog.comingSoon(331) },
],
];
const viewModeEntries: RadioEntries = [
{ value: "normal", icon: "ViewModeNormal", tooltip: "View Mode: Normal" },
{ value: "outline", icon: "ViewModeOutline", tooltip: "View Mode: Outline", action: () => this.dialog.comingSoon(319) },
{ value: "pixels", icon: "ViewModePixels", tooltip: "View Mode: Pixels", action: () => this.dialog.comingSoon(320) },
];
return {
viewportSvg: "",
canvasSvgWidth: "100%",
@@ -395,7 +391,6 @@ export default defineComponent({
ScrollbarDirection,
RulerDirection,
SeparatorType,
comingSoon,
};
},
components: {

View File

@@ -197,9 +197,7 @@
<script lang="ts">
import { defineComponent } from "vue";
import { subscribeJsMessage } from "@/utilities/js-message-dispatcher";
import { BlendMode, DisplayFolderTreeStructure, UpdateLayer, LayerPanelEntry, LayerTypeOptions } from "@/utilities/js-messages";
import { panicProxy } from "@/utilities/panic-proxy";
import { BlendMode, DisplayFolderTreeStructure, UpdateLayer, LayerPanelEntry, LayerTypeOptions } from "@/dispatcher/js-messages";
import { SeparatorType } from "@/components/widgets/widgets";
import LayoutRow from "@/components/layout/LayoutRow.vue";
@@ -213,8 +211,6 @@ import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import DropdownInput from "@/components/widgets/inputs/DropdownInput.vue";
import { SectionsOfMenuListEntries } from "@/components/widgets/floating-menus/MenuList.vue";
const wasm = import("@/../wasm/pkg").then(panicProxy);
const blendModeEntries: SectionsOfMenuListEntries<BlendMode> = [
[{ label: "Normal", value: "Normal" }],
[
@@ -255,6 +251,7 @@ const blendModeEntries: SectionsOfMenuListEntries<BlendMode> = [
];
export default defineComponent({
inject: ["editor"],
data() {
return {
blendModeEntries,
@@ -278,19 +275,19 @@ export default defineComponent({
return `${(layer.path.length - 1) * 16}px`;
},
async toggleLayerVisibility(path: BigUint64Array) {
(await wasm).toggle_layer_visibility(path);
this.editor.instance.toggle_layer_visibility(path);
},
async handleNodeConnectorClick(path: BigUint64Array) {
(await wasm).toggle_layer_expansion(path);
this.editor.instance.toggle_layer_expansion(path);
},
async setLayerBlendMode() {
const blendMode = this.blendModeEntries.flat()[this.blendModeSelectedIndex].value;
if (blendMode) {
(await wasm).set_blend_mode_for_selected_layers(blendMode);
this.editor.instance.set_blend_mode_for_selected_layers(blendMode);
}
},
async setLayerOpacity() {
(await wasm).set_opacity_for_selected_layers(this.opacity);
this.editor.instance.set_opacity_for_selected_layers(this.opacity);
},
async handleControlClick(clickedLayer: LayerPanelEntry) {
const index = this.layers.indexOf(clickedLayer);
@@ -330,7 +327,7 @@ export default defineComponent({
this.selectionRangeStartLayer = undefined;
this.selectionRangeEndLayer = undefined;
(await wasm).deselect_all_layers();
this.editor.instance.deselect_all_layers();
},
async fillSelectionRange(start: LayerPanelEntry, end: LayerPanelEntry, selected = true) {
const startIndex = this.layers.findIndex((layer) => layer.path.join() === start.path.join());
@@ -363,7 +360,7 @@ export default defineComponent({
}
i += 1;
});
(await wasm).select_layers(output);
this.editor.instance.select_layers(output);
},
setBlendModeForSelectedLayers() {
const selected = this.layers.filter((layer) => layer.layer_data.selected);
@@ -407,7 +404,7 @@ export default defineComponent({
},
},
mounted() {
subscribeJsMessage(DisplayFolderTreeStructure, (displayFolderTreeStructure) => {
this.editor.dispatcher.subscribeJsMessage(DisplayFolderTreeStructure, (displayFolderTreeStructure) => {
const path = [] as bigint[];
this.layers = [] as LayerPanelEntry[];
function recurse(folder: DisplayFolderTreeStructure, layers: LayerPanelEntry[], cache: Map<string, LayerPanelEntry>) {
@@ -423,7 +420,7 @@ export default defineComponent({
recurse(displayFolderTreeStructure, this.layers, this.layerCache);
});
subscribeJsMessage(UpdateLayer, (updateLayer) => {
this.editor.dispatcher.subscribeJsMessage(UpdateLayer, (updateLayer) => {
const targetPath = updateLayer.data.path;
const targetLayer = updateLayer.data;

View File

@@ -3,14 +3,14 @@
<FloatingMenu :type="MenuType.Dialog" :direction="MenuDirection.Center">
<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()" />
<!-- `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.heading }}</TextLabel>
<TextLabel :class="'details'">{{ dialog.details }}</TextLabel>
<LayoutRow :class="'buttons-row'" v-if="dialog.buttons.length > 0">
<TextButton v-for="(button, index) in dialog.buttons" :key="index" :title="button.tooltip" :action="button.callback" v-bind="button.props" />
<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" />
</LayoutRow>
</LayoutCol>
</LayoutRow>
@@ -79,8 +79,6 @@
<script lang="ts">
import { defineComponent } from "vue";
import { dismissDialog } from "@/utilities/dialog";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import LayoutCol from "@/components/layout/LayoutCol.vue";
import FloatingMenu, { MenuDirection, MenuType } from "@/components/widgets/floating-menus/FloatingMenu.vue";
@@ -100,7 +98,7 @@ export default defineComponent({
},
methods: {
dismiss() {
dismissDialog();
this.dialog.dismissDialog();
},
},
data() {

View File

@@ -18,7 +18,7 @@
<span class="entry-label">{{ entry.label }}</span>
<IconLabel v-if="entry.shortcutRequiresLock && !fullscreen.keyboardLocked" :icon="'Info'" :title="keyboardLockInfoMessage" />
<IconLabel v-if="entry.shortcutRequiresLock && !fullscreen.state.keyboardLocked" :icon="'Info'" :title="keyboardLockInfoMessage" />
<UserInputLabel v-else-if="entry.shortcut && entry.shortcut.length" :inputKeys="[entry.shortcut]" />
<div class="submenu-arrow" v-if="entry.children && entry.children.length"></div>
@@ -132,7 +132,6 @@
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { keyboardLockApiSupported } from "@/utilities/fullscreen";
import { SeparatorDirection, SeparatorType } from "@/components/widgets/widgets";
import FloatingMenu, { MenuDirection, MenuType } from "@/components/widgets/floating-menus/FloatingMenu.vue";
@@ -265,7 +264,7 @@ const MenuList = defineComponent({
},
data() {
return {
keyboardLockInfoMessage: keyboardLockApiSupported() ? KEYBOARD_LOCK_USE_FULLSCREEN : KEYBOARD_LOCK_SWITCH_BROWSER,
keyboardLockInfoMessage: this.fullscreen.keyboardLockApiSupported ? KEYBOARD_LOCK_USE_FULLSCREEN : KEYBOARD_LOCK_SWITCH_BROWSER,
SeparatorDirection,
SeparatorType,
MenuDirection,

View File

@@ -53,117 +53,116 @@
<script lang="ts">
import { defineComponent } from "vue";
import { comingSoon } from "@/utilities/errors";
import { panicProxy } from "@/utilities/panic-proxy";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import { ApplicationPlatform } from "@/components/window/MainWindow.vue";
import MenuList, { MenuListEntry, MenuListEntries } from "@/components/widgets/floating-menus/MenuList.vue";
import { MenuDirection } from "@/components/widgets/floating-menus/FloatingMenu.vue";
import { EditorState } from "@/state/wasm-loader";
const wasm = import("@/../wasm/pkg").then(panicProxy);
const menuEntries: MenuListEntries = [
{
label: "File",
ref: undefined,
children: [
[
{ label: "New", icon: "File", shortcut: ["Ctrl", "N"], shortcutRequiresLock: true, action: async () => (await wasm).new_document() },
{ label: "Open…", shortcut: ["Ctrl", "O"], action: async () => (await wasm).open_document() },
{
label: "Open Recent",
shortcut: ["Ctrl", "⇧", "O"],
children: [
[{ label: "Reopen Last Closed", shortcut: ["Ctrl", "⇧", "T"], shortcutRequiresLock: true }, { label: "Clear Recently Opened" }],
[
{ label: "Some Recent File.gdd" },
{ label: "Another Recent File.gdd" },
{ label: "An Older File.gdd" },
{ label: "Some Other Older File.gdd" },
{ label: "Yet Another Older File.gdd" },
function makeMenuEntries(editor: EditorState): MenuListEntries {
return [
{
label: "File",
ref: undefined,
children: [
[
{ label: "New", icon: "File", shortcut: ["Ctrl", "N"], shortcutRequiresLock: true, action: async () => editor.instance.new_document() },
{ label: "Open…", shortcut: ["Ctrl", "O"], action: async () => editor.instance.open_document() },
{
label: "Open Recent",
shortcut: ["Ctrl", "⇧", "O"],
children: [
[{ label: "Reopen Last Closed", shortcut: ["Ctrl", "⇧", "T"], shortcutRequiresLock: true }, { label: "Clear Recently Opened" }],
[
{ label: "Some Recent File.gdd" },
{ label: "Another Recent File.gdd" },
{ label: "An Older File.gdd" },
{ label: "Some Other Older File.gdd" },
{ label: "Yet Another Older File.gdd" },
],
],
],
},
},
],
[
{ label: "Close", shortcut: ["Ctrl", "W"], shortcutRequiresLock: true, action: async () => editor.instance.close_active_document_with_confirmation() },
{ label: "Close All", shortcut: ["Ctrl", "Alt", "W"], action: async () => editor.instance.close_all_documents_with_confirmation() },
],
[
{ label: "Save", shortcut: ["Ctrl", "S"], action: async () => editor.instance.save_document() },
{ label: "Save As…", shortcut: ["Ctrl", "⇧", "S"], action: async () => editor.instance.save_document() },
{ label: "Save All", shortcut: ["Ctrl", "Alt", "S"] },
{ label: "Auto-Save", checkbox: true, checked: true },
],
[
{ label: "Import…", shortcut: ["Ctrl", "I"] },
{ label: "Export…", shortcut: ["Ctrl", "E"], action: async () => editor.instance.export_document() },
],
[{ label: "Quit", shortcut: ["Ctrl", "Q"] }],
],
[
{ label: "Close", shortcut: ["Ctrl", "W"], shortcutRequiresLock: true, action: async () => (await wasm).close_active_document_with_confirmation() },
{ label: "Close All", shortcut: ["Ctrl", "Alt", "W"], action: async () => (await wasm).close_all_documents_with_confirmation() },
},
{
label: "Edit",
ref: undefined,
children: [
[
{ label: "Undo", shortcut: ["Ctrl", "Z"], action: async () => editor.instance.undo() },
{ label: "Redo", shortcut: ["Ctrl", "⇧", "Z"], action: async () => editor.instance.redo() },
],
[
{ label: "Cut", shortcut: ["Ctrl", "X"] },
{ label: "Copy", icon: "Copy", shortcut: ["Ctrl", "C"] },
{ label: "Paste", icon: "Paste", shortcut: ["Ctrl", "V"] },
],
],
[
{ label: "Save", shortcut: ["Ctrl", "S"], action: async () => (await wasm).save_document() },
{ label: "Save As…", shortcut: ["Ctrl", "⇧", "S"], action: async () => (await wasm).save_document() },
{ label: "Save All", shortcut: ["Ctrl", "Alt", "S"] },
{ label: "Auto-Save", checkbox: true, checked: true },
],
[
{ label: "Import…", shortcut: ["Ctrl", "I"] },
{ label: "Export…", shortcut: ["Ctrl", "E"], action: async () => (await wasm).export_document() },
],
[{ label: "Quit", shortcut: ["Ctrl", "Q"] }],
],
},
{
label: "Edit",
ref: undefined,
children: [
[
{ label: "Undo", shortcut: ["Ctrl", "Z"], action: async () => (await wasm).undo() },
{ label: "Redo", shortcut: ["Ctrl", "⇧", "Z"], action: async () => (await wasm).redo() },
],
[
{ label: "Cut", shortcut: ["Ctrl", "X"] },
{ label: "Copy", icon: "Copy", shortcut: ["Ctrl", "C"] },
{ label: "Paste", icon: "Paste", shortcut: ["Ctrl", "V"] },
],
],
},
{
label: "Layer",
ref: undefined,
children: [
[
{ label: "Select All", shortcut: ["Ctrl", "A"], action: async () => (await wasm).select_all_layers() },
{ label: "Deselect All", shortcut: ["Ctrl", "Alt", "A"], action: async () => (await wasm).deselect_all_layers() },
{
label: "Order",
children: [
[
{ label: "Raise To Front", shortcut: ["Ctrl", "Shift", "]"], action: async () => (await wasm).reorder_selected_layers((await wasm).i32_max()) },
{ label: "Raise", shortcut: ["Ctrl", "]"], action: async () => (await wasm).reorder_selected_layers(1) },
{ label: "Lower", shortcut: ["Ctrl", "["], action: async () => (await wasm).reorder_selected_layers(-1) },
{ label: "Lower to Back", shortcut: ["Ctrl", "Shift", "["], action: async () => (await wasm).reorder_selected_layers((await wasm).i32_min()) },
},
{
label: "Layer",
ref: undefined,
children: [
[
{ label: "Select All", shortcut: ["Ctrl", "A"], action: async () => editor.instance.select_all_layers() },
{ label: "Deselect All", shortcut: ["Ctrl", "Alt", "A"], action: async () => editor.instance.deselect_all_layers() },
{
label: "Order",
children: [
[
{ label: "Raise To Front", shortcut: ["Ctrl", "Shift", "]"], action: async () => editor.instance.reorder_selected_layers(editor.rawWasm.i32_max()) },
{ label: "Raise", shortcut: ["Ctrl", "]"], action: async () => editor.instance.reorder_selected_layers(1) },
{ label: "Lower", shortcut: ["Ctrl", "["], action: async () => editor.instance.reorder_selected_layers(-1) },
{ label: "Lower to Back", shortcut: ["Ctrl", "Shift", "["], action: async () => editor.instance.reorder_selected_layers(editor.rawWasm.i32_min()) },
],
],
],
},
},
],
],
],
},
{
label: "Document",
ref: undefined,
children: [[{ label: "Menu entries coming soon" }]],
},
{
label: "View",
ref: undefined,
children: [[{ label: "Menu entries coming soon" }]],
},
{
label: "Help",
ref: undefined,
children: [
[{ label: "About Graphite", action: async () => (await wasm).request_about_graphite_dialog() }],
[
{ label: "Report a Bug", action: () => window.open("https://github.com/GraphiteEditor/Graphite/issues/new", "_blank") },
{ label: "Visit on GitHub", action: () => window.open("https://github.com/GraphiteEditor/Graphite", "_blank") },
},
{
label: "Document",
ref: undefined,
children: [[{ label: "Menu entries coming soon" }]],
},
{
label: "View",
ref: undefined,
children: [[{ label: "Menu entries coming soon" }]],
},
{
label: "Help",
ref: undefined,
children: [
[{ label: "About Graphite", action: async () => editor.instance.request_about_graphite_dialog() }],
[
{ label: "Report a Bug", action: () => window.open("https://github.com/GraphiteEditor/Graphite/issues/new", "_blank") },
{ label: "Visit on GitHub", action: () => window.open("https://github.com/GraphiteEditor/Graphite", "_blank") },
],
[{ label: "Debug: Panic (DANGER)", action: async () => editor.rawWasm.intentional_panic() }],
],
[{ label: "Debug: Panic (DANGER)", action: async () => (await wasm).intentional_panic() }],
],
},
];
},
];
}
export default defineComponent({
inject: ["editor", "dialog"],
methods: {
setEntryRefs(menuEntry: MenuListEntry, ref: typeof MenuList) {
if (ref) menuEntry.ref = ref;
@@ -180,9 +179,9 @@ export default defineComponent({
data() {
return {
ApplicationPlatform,
menuEntries,
menuEntries: makeMenuEntries(this.editor),
MenuDirection,
comingSoon,
comingSoon: () => this.dialog.comingSoon(),
};
},
components: {

View File

@@ -69,16 +69,13 @@
import { defineComponent } from "vue";
import { rgbToDecimalRgb, RGB } from "@/utilities/color";
import { panicProxy } from "@/utilities/panic-proxy";
import { subscribeJsMessage } from "@/utilities/js-message-dispatcher";
import { UpdateWorkingColors } from "@/utilities/js-messages";
import ColorPicker from "@/components/widgets/floating-menus/ColorPicker.vue";
import FloatingMenu, { MenuDirection, MenuType } from "@/components/widgets/floating-menus/FloatingMenu.vue";
const wasm = import("@/../wasm/pkg").then(panicProxy);
import { UpdateWorkingColors } from "@/dispatcher/js-messages";
export default defineComponent({
inject: ["editor"],
components: {
FloatingMenu,
ColorPicker,
@@ -115,7 +112,7 @@ export default defineComponent({
button.style.setProperty("--swatch-color", `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`);
color = rgbToDecimalRgb(this.primaryColor);
(await wasm).update_primary_color(color.r, color.g, color.b, color.a);
this.editor.instance.update_primary_color(color.r, color.g, color.b, color.a);
},
async updateSecondaryColor() {
@@ -124,7 +121,7 @@ export default defineComponent({
button.style.setProperty("--swatch-color", `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`);
color = rgbToDecimalRgb(this.secondaryColor);
(await wasm).update_secondary_color(color.r, color.g, color.b, color.a);
this.editor.instance.update_secondary_color(color.r, color.g, color.b, color.a);
},
},
data() {
@@ -136,7 +133,7 @@ export default defineComponent({
};
},
mounted() {
subscribeJsMessage(UpdateWorkingColors, (updateWorkingColors) => {
this.editor.dispatcher.subscribeJsMessage(UpdateWorkingColors, (updateWorkingColors) => {
const { primary, secondary } = updateWorkingColors;
this.primaryColor = primary.toRgba();

View File

@@ -31,8 +31,6 @@
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { comingSoon } from "@/utilities/errors";
import { panicProxy } from "@/utilities/panic-proxy";
import { WidgetRow, SeparatorType, IconButtonWidget } from "@/components/widgets/widgets";
import Separator from "@/components/widgets/separators/Separator.vue";
@@ -40,9 +38,8 @@ import IconButton from "@/components/widgets/buttons/IconButton.vue";
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
const wasm = import("@/../wasm/pkg").then(panicProxy);
export default defineComponent({
inject: ["editor", "dialog"],
props: {
activeTool: { type: String },
activeToolOptions: { type: Object as PropType<Record<string, object>> },
@@ -50,10 +47,10 @@ export default defineComponent({
methods: {
async updateToolOptions(path: string[], newValue: number) {
this.setToolOption(path, newValue);
(await wasm).set_tool_options(this.activeTool || "", this.activeToolOptions);
this.editor.instance.set_tool_options(this.activeTool || "", this.activeToolOptions);
},
async sendToolMessage(message: string | object) {
(await wasm).send_tool_message(this.activeTool || "", message);
this.editor.instance.send_tool_message(this.activeTool || "", message);
},
// Traverses the given path and returns the direct parent of the option
getRecordContainingOption(optionPath: string[]): Record<string, number> {
@@ -87,7 +84,7 @@ export default defineComponent({
return;
}
comingSoon();
this.dialog.comingSoon();
},
},
data() {
@@ -132,11 +129,11 @@ export default defineComponent({
{ kind: "Separator", props: { type: SeparatorType.Section } },
{ kind: "IconButton", tooltip: "Boolean Union", callback: () => comingSoon(197), props: { icon: "BooleanUnion", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Subtract Front", callback: () => comingSoon(197), props: { icon: "BooleanSubtractFront", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Subtract Back", callback: () => comingSoon(197), props: { icon: "BooleanSubtractBack", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Intersect", callback: () => comingSoon(197), props: { icon: "BooleanIntersect", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Difference", callback: () => comingSoon(197), props: { icon: "BooleanDifference", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Union", callback: () => this.dialog.comingSoon(197), props: { icon: "BooleanUnion", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Subtract Front", callback: () => this.dialog.comingSoon(197), props: { icon: "BooleanSubtractFront", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Subtract Back", callback: () => this.dialog.comingSoon(197), props: { icon: "BooleanSubtractBack", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Intersect", callback: () => this.dialog.comingSoon(197), props: { icon: "BooleanIntersect", size: 24 } },
{ kind: "IconButton", tooltip: "Boolean Difference", callback: () => this.dialog.comingSoon(197), props: { icon: "BooleanDifference", size: 24 } },
{ kind: "Separator", props: { type: SeparatorType.Related } },
@@ -157,7 +154,6 @@ export default defineComponent({
return {
toolOptionsWidgets,
SeparatorType,
comingSoon,
};
},
components: {

View File

@@ -4,7 +4,7 @@
<MenuBarInput v-if="platform !== ApplicationPlatform.Mac" />
</div>
<div class="header-third">
<WindowTitle :title="`${documents.documents[documents.activeDocumentIndex].displayName} - Graphite`" />
<WindowTitle :title="`${activeDocumentDisplayName} - Graphite`" />
</div>
<div class="header-third">
<WindowButtonsWindows :maximized="maximized" v-if="platform === ApplicationPlatform.Windows || platform === ApplicationPlatform.Linux" />
@@ -52,6 +52,11 @@ export default defineComponent({
ApplicationPlatform,
};
},
computed: {
activeDocumentDisplayName() {
return this.documents.state.documents[this.documents.state.activeDocumentIndex].displayName;
},
},
components: {
MenuBarInput,
WindowTitle,

View File

@@ -1,7 +1,7 @@
<template>
<div class="window-buttons-web" @click="handleClick" :title="fullscreen.windowFullscreen ? 'Exit Fullscreen (F11)' : 'Enter Fullscreen (F11)'">
<div class="window-buttons-web" @click="handleClick" :title="fullscreen.state.windowFullscreen ? 'Exit Fullscreen (F11)' : 'Enter Fullscreen (F11)'">
<TextLabel v-if="requestFullscreenHotkeys" :italic="true">Go fullscreen to access all hotkeys</TextLabel>
<IconLabel :icon="fullscreen.windowFullscreen ? 'FullscreenExit' : 'FullscreenEnter'" />
<IconLabel :icon="fullscreen.state.windowFullscreen ? 'FullscreenExit' : 'FullscreenEnter'" />
</div>
</template>
@@ -33,24 +33,20 @@
<script lang="ts">
import { defineComponent } from "vue";
import fullscreen, { keyboardLockApiSupported, enterFullscreen, exitFullscreen } from "@/utilities/fullscreen";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
const canUseKeyboardLock = keyboardLockApiSupported();
export default defineComponent({
inject: ["fullscreen"],
methods: {
async handleClick() {
if (fullscreen.windowFullscreen) exitFullscreen();
else enterFullscreen();
if (this.fullscreen.state.windowFullscreen) this.fullscreen.exitFullscreen();
else this.fullscreen.enterFullscreen();
},
},
computed: {
requestFullscreenHotkeys() {
return canUseKeyboardLock && !fullscreen.keyboardLocked;
return this.fullscreen.keyboardLockApiSupported && !this.fullscreen.state.keyboardLocked;
},
},
components: {

View File

@@ -10,17 +10,17 @@
@click.middle="
(e) => {
e.stopPropagation();
closeDocumentWithConfirmation(tabIndex);
documents.closeDocumentWithConfirmation(tabIndex);
}
"
@click="panelType === 'Document' && selectDocument(tabIndex)"
@click="panelType === 'Document' && documents.selectDocument(tabIndex)"
>
<span>{{ tabLabel }}</span>
<IconButton
:action="
(e) => {
e.stopPropagation();
closeDocumentWithConfirmation(tabIndex);
documents.closeDocumentWithConfirmation(tabIndex);
}
"
:icon="'CloseX'"
@@ -169,8 +169,6 @@
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { selectDocument, closeDocumentWithConfirmation } from "@/utilities/documents";
import Document from "@/components/panels/Document.vue";
import Properties from "@/components/panels/Properties.vue";
import LayerTree from "@/components/panels/LayerTree.vue";
@@ -180,6 +178,7 @@ import PopoverButton, { PopoverButtonIcon } from "@/components/widgets/buttons/P
import { MenuDirection } from "@/components/widgets/floating-menus/FloatingMenu.vue";
export default defineComponent({
inject: ["documents"],
components: {
Document,
Properties,
@@ -197,8 +196,6 @@ export default defineComponent({
},
data() {
return {
selectDocument,
closeDocumentWithConfirmation,
PopoverButtonIcon,
MenuDirection,
};

View File

@@ -5,8 +5,8 @@
:panelType="'Document'"
:tabCloseButtons="true"
:tabMinWidths="true"
:tabLabels="documents.documents.map((doc) => doc.displayName)"
:tabActiveIndex="documents.activeDocumentIndex"
:tabLabels="documents.state.documents.map((doc) => doc.displayName)"
:tabActiveIndex="documents.state.activeDocumentIndex"
ref="documentsPanel"
/>
</LayoutCol>
@@ -25,7 +25,7 @@
</LayoutRow> -->
</LayoutCol>
</LayoutRow>
<DialogModal v-if="dialog.visible" />
<DialogModal v-if="dialog.state.visible" />
</template>
<style lang="scss">
@@ -55,8 +55,6 @@
<script lang="ts">
import { defineComponent } from "vue";
import "@/utilities/dialogs";
import Panel from "@/components/workspace/Panel.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
import LayoutCol from "@/components/layout/LayoutCol.vue";
@@ -75,8 +73,7 @@ export default defineComponent({
},
computed: {
activeDocumentIndex() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (this as any).documents.activeDocumentIndex;
return this.documents.state.activeDocumentIndex;
},
},
watch: {