Tidy up the full frontend codebase and use optional chaining where possible (#620)

* Tidy up the full frontend codebase and use optional chaining where possible

* Code review changes
This commit is contained in:
Keavon Chambers
2022-04-30 02:52:53 -07:00
parent 92ee3bbad3
commit 07736a9fca
28 changed files with 640 additions and 622 deletions
+2 -1
View File
@@ -59,6 +59,7 @@ module.exports = {
"linebreak-style": ["error", "unix"], "linebreak-style": ["error", "unix"],
"eol-last": ["error", "always"], "eol-last": ["error", "always"],
"max-len": ["error", { code: 200, tabWidth: 4 }], "max-len": ["error", { code: 200, tabWidth: 4 }],
"prefer-destructuring": "off",
"no-console": "warn", "no-console": "warn",
"no-debugger": "warn", "no-debugger": "warn",
"no-param-reassign": ["error", { props: false }], "no-param-reassign": ["error", { props: false }],
@@ -71,7 +72,7 @@ module.exports = {
"@typescript-eslint/indent": "off", "@typescript-eslint/indent": "off",
"@typescript-eslint/camelcase": "off", "@typescript-eslint/camelcase": "off",
"@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", ignoreRestSiblings: true }],
"@typescript-eslint/explicit-function-return-type": ["error"], "@typescript-eslint/explicit-function-return-type": ["error"],
// Import plugin config (used to intelligently validate module import statements) // Import plugin config (used to intelligently validate module import statements)
+2 -5
View File
@@ -318,11 +318,8 @@ export default defineComponent({
this.inputManager = createInputManager(this.editor, this.$el.parentElement, this.dialog, this.documents, this.fullscreen); this.inputManager = createInputManager(this.editor, this.$el.parentElement, this.dialog, this.documents, this.fullscreen);
}, },
beforeUnmount() { beforeUnmount() {
const { inputManager } = this; this.inputManager?.removeListeners();
if (inputManager) inputManager.removeListeners(); this.editor.instance.free();
const { editor } = this;
editor.instance.free();
}, },
components: { components: {
MainWindow, MainWindow,
+24 -27
View File
@@ -329,22 +329,21 @@ export default defineComponent({
const rulerHorizontal = this.$refs.rulerHorizontal as typeof CanvasRuler; const rulerHorizontal = this.$refs.rulerHorizontal as typeof CanvasRuler;
const rulerVertical = this.$refs.rulerVertical as typeof CanvasRuler; const rulerVertical = this.$refs.rulerVertical as typeof CanvasRuler;
if (rulerHorizontal) rulerHorizontal.handleResize(); rulerHorizontal?.handleResize();
if (rulerVertical) rulerVertical.handleResize(); rulerVertical?.handleResize();
}, },
pasteFile(e: DragEvent) { pasteFile(e: DragEvent) {
const { dataTransfer } = e; const { dataTransfer } = e;
if (!dataTransfer) return; if (!dataTransfer) return;
e.preventDefault(); e.preventDefault();
Array.from(dataTransfer.items).forEach((item) => { Array.from(dataTransfer.items).forEach(async (item) => {
const file = item.getAsFile(); const file = item.getAsFile();
if (file && file.type.startsWith("image")) { if (file?.type.startsWith("image")) {
file.arrayBuffer().then((buffer): void => { const buffer = await file.arrayBuffer();
const u8Array = new Uint8Array(buffer); const u8Array = new Uint8Array(buffer);
this.editor.instance.paste_image(file.type, u8Array, e.clientX, e.clientY); this.editor.instance.paste_image(file.type, u8Array, e.clientX, e.clientY);
});
} }
}); });
}, },
@@ -400,11 +399,13 @@ export default defineComponent({
const range = document.createRange(); const range = document.createRange();
range.selectNodeContents(addedInput); range.selectNodeContents(addedInput);
const selection = window.getSelection(); const selection = window.getSelection();
if (selection) { if (selection) {
selection.removeAllRanges(); selection.removeAllRanges();
selection.addRange(range); selection.addRange(range);
} }
addedInput.focus(); addedInput.focus();
addedInput.click(); addedInput.click();
}); });
@@ -455,24 +456,20 @@ export default defineComponent({
this.canvasCursor = updateMouseCursor.cursor; this.canvasCursor = updateMouseCursor.cursor;
}); });
this.editor.dispatcher.subscribeJsMessage(TriggerTextCommit, () => { this.editor.dispatcher.subscribeJsMessage(TriggerTextCommit, () => {
if (this.textInput) this.editor.instance.on_change_text(textInputCleanup(this.textInput.innerText)); if (this.textInput) {
const textCleaned = textInputCleanup(this.textInput.innerText);
this.editor.instance.on_change_text(textCleaned);
}
}); });
this.editor.dispatcher.subscribeJsMessage(TriggerFontLoad, (triggerFontLoad) => { this.editor.dispatcher.subscribeJsMessage(TriggerFontLoad, async (triggerFontLoad) => {
fetch(triggerFontLoad.font) const response = await fetch(triggerFontLoad.font);
.then((response) => response.arrayBuffer()) const responseBuffer = await response.arrayBuffer();
.then((response) => { this.editor.instance.on_font_load(triggerFontLoad.font, new Uint8Array(responseBuffer), false);
this.editor.instance.on_font_load(triggerFontLoad.font, new Uint8Array(response), false);
});
}); });
this.editor.dispatcher.subscribeJsMessage(TriggerDefaultFontLoad, loadDefaultFont); this.editor.dispatcher.subscribeJsMessage(TriggerDefaultFontLoad, loadDefaultFont);
this.editor.dispatcher.subscribeJsMessage(TriggerTextCopy, async (triggerTextCopy) => { this.editor.dispatcher.subscribeJsMessage(TriggerTextCopy, (triggerTextCopy) => {
// Clipboard API supported? // If the Clipboard API is supported in the browser, copy text to the clipboard
if (!navigator.clipboard) return; navigator.clipboard?.writeText?.(triggerTextCopy.copy_text);
// copy text to clipboard
if (navigator.clipboard.writeText) {
await navigator.clipboard.writeText(triggerTextCopy.copy_text);
}
}); });
this.editor.dispatcher.subscribeJsMessage(DisplayEditableTextbox, (displayEditableTextbox) => { this.editor.dispatcher.subscribeJsMessage(DisplayEditableTextbox, (displayEditableTextbox) => {
@@ -511,15 +508,15 @@ export default defineComponent({
this.editor.dispatcher.subscribeJsMessage(TriggerViewportResize, this.viewportResize); this.editor.dispatcher.subscribeJsMessage(TriggerViewportResize, this.viewportResize);
this.editor.dispatcher.subscribeJsMessage(UpdateImageData, (updateImageData) => { this.editor.dispatcher.subscribeJsMessage(UpdateImageData, (updateImageData) => {
updateImageData.image_data.forEach((element) => { updateImageData.image_data.forEach(async (element) => {
// Using updateImageData.image_data.buffer returns undefined for some reason? // Using updateImageData.image_data.buffer returns undefined for some reason?
const blob = new Blob([new Uint8Array(element.image_data.values()).buffer], { type: element.mime }); const blob = new Blob([new Uint8Array(element.image_data.values()).buffer], { type: element.mime });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
createImageBitmap(blob).then((image) => { const image = await createImageBitmap(blob);
this.editor.instance.set_image_blob_url(element.path, url, image.width, image.height);
}); this.editor.instance.set_image_blob_url(element.path, url, image.width, image.height);
}); });
}); });
+23 -14
View File
@@ -42,11 +42,11 @@
class="layer-row" class="layer-row"
v-for="(listing, index) in layers" v-for="(listing, index) in layers"
:key="String(listing.entry.path.slice(-1))" :key="String(listing.entry.path.slice(-1))"
:class="{ 'insert-folder': draggingData && draggingData.highlightFolder && draggingData.insertFolder === listing.entry.path }" :class="{ 'insert-folder': draggingData?.highlightFolder && draggingData?.insertFolder === listing.entry.path }"
> >
<LayoutRow class="visibility"> <LayoutRow class="visibility">
<IconButton <IconButton
:action="(e) => (toggleLayerVisibility(listing.entry.path), e && e.stopPropagation())" :action="(e) => (toggleLayerVisibility(listing.entry.path), e?.stopPropagation())"
:size="24" :size="24"
:icon="listing.entry.visible ? 'EyeVisible' : 'EyeHidden'" :icon="listing.entry.visible ? 'EyeVisible' : 'EyeHidden'"
:title="listing.entry.visible ? 'Visible' : 'Hidden'" :title="listing.entry.visible ? 'Visible' : 'Hidden'"
@@ -396,16 +396,16 @@ export default defineComponent({
markTopOffset(height: number): string { markTopOffset(height: number): string {
return `${height}px`; return `${height}px`;
}, },
async createEmptyFolder() { createEmptyFolder() {
this.editor.instance.create_empty_folder(); this.editor.instance.create_empty_folder();
}, },
async deleteSelectedLayers() { deleteSelectedLayers() {
this.editor.instance.delete_selected_layers(); this.editor.instance.delete_selected_layers();
}, },
async toggleLayerVisibility(path: BigUint64Array) { toggleLayerVisibility(path: BigUint64Array) {
this.editor.instance.toggle_layer_visibility(path); this.editor.instance.toggle_layer_visibility(path);
}, },
async handleExpandArrowClick(path: BigUint64Array) { handleExpandArrowClick(path: BigUint64Array) {
this.editor.instance.toggle_layer_expansion(path); this.editor.instance.toggle_layer_expansion(path);
}, },
onEditLayerName(listing: LayerListingInfo) { onEditLayerName(listing: LayerListingInfo) {
@@ -414,12 +414,12 @@ export default defineComponent({
this.draggable = false; this.draggable = false;
listing.editingName = true; listing.editingName = true;
const tree = (this.$refs.layerTreeList as typeof LayoutCol).$el as HTMLElement; const tree: HTMLElement = (this.$refs.layerTreeList as typeof LayoutCol).$el;
this.$nextTick(() => { this.$nextTick(() => {
(tree.querySelector("[data-text-input]:not([disabled])") as HTMLInputElement).select(); (tree.querySelector("[data-text-input]:not([disabled])") as HTMLInputElement).select();
}); });
}, },
async onEditLayerNameChange(listing: LayerListingInfo, inputElement: EventTarget | null) { onEditLayerNameChange(listing: LayerListingInfo, inputElement: EventTarget | null) {
// Eliminate duplicate events // Eliminate duplicate events
if (!listing.editingName) return; if (!listing.editingName) return;
@@ -434,8 +434,7 @@ export default defineComponent({
listing.editingName = false; listing.editingName = false;
this.$nextTick(() => { this.$nextTick(() => {
const selection = window.getSelection(); window.getSelection()?.removeAllRanges();
if (selection) selection.removeAllRanges();
}); });
}, },
async setLayerBlendMode(newSelectedIndex: number) { async setLayerBlendMode(newSelectedIndex: number) {
@@ -536,7 +535,7 @@ export default defineComponent({
// Stop the drag from being shown as cancelled // Stop the drag from being shown as cancelled
event.preventDefault(); event.preventDefault();
const tree = (this.$refs.layerTreeList as typeof LayoutCol).$el as HTMLElement; const tree: HTMLElement = (this.$refs.layerTreeList as typeof LayoutCol).$el;
this.draggingData = this.calculateDragIndex(tree, event.clientY); this.draggingData = this.calculateDragIndex(tree, event.clientY);
}, },
async drop() { async drop() {
@@ -594,8 +593,8 @@ export default defineComponent({
mounted() { mounted() {
this.editor.dispatcher.subscribeJsMessage(DisplayDocumentLayerTreeStructure, (displayDocumentLayerTreeStructure) => { this.editor.dispatcher.subscribeJsMessage(DisplayDocumentLayerTreeStructure, (displayDocumentLayerTreeStructure) => {
const layerWithNameBeingEdited = this.layers.find((layer: LayerListingInfo) => layer.editingName); const layerWithNameBeingEdited = this.layers.find((layer: LayerListingInfo) => layer.editingName);
const layerPathWithNameBeingEdited = layerWithNameBeingEdited && layerWithNameBeingEdited.entry.path; const layerPathWithNameBeingEdited = layerWithNameBeingEdited?.entry.path;
const layerIdWithNameBeingEdited = layerPathWithNameBeingEdited && layerPathWithNameBeingEdited.slice(-1)[0]; const layerIdWithNameBeingEdited = layerPathWithNameBeingEdited?.slice(-1)[0];
const path = [] as bigint[]; const path = [] as bigint[];
this.layers = [] as LayerListingInfo[]; this.layers = [] as LayerListingInfo[];
@@ -606,9 +605,18 @@ export default defineComponent({
path.push(layerId); path.push(layerId);
const mapping = cache.get(path.toString()); const mapping = cache.get(path.toString());
if (mapping) layers.push({ folderIndex: index, bottomLayer: index === folder.children.length - 1, entry: mapping, editingName: layerIdWithNameBeingEdited === layerId }); if (mapping) {
layers.push({
folderIndex: index,
bottomLayer: index === folder.children.length - 1,
entry: mapping,
editingName: layerIdWithNameBeingEdited === layerId,
});
}
// Call self recursively if there are any children
if (item.children.length >= 1) recurse(item, layers, cache); if (item.children.length >= 1) recurse(item, layers, cache);
path.pop(); path.pop();
}); });
}; };
@@ -626,6 +634,7 @@ export default defineComponent({
} else { } else {
this.layerCache.set(targetPath.toString(), targetLayer); this.layerCache.set(targetPath.toString(), targetLayer);
} }
this.setBlendModeForSelectedLayers(); this.setBlendModeForSelectedLayers();
this.setOpacityForSelectedLayers(); this.setOpacityForSelectedLayers();
}); });
@@ -1,5 +1,4 @@
<template> <template>
<div>{{ widgetData.name }}</div>
<div class="widget-row"> <div class="widget-row">
<template v-for="(component, index) in widgetData.widgets" :key="index"> <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` --> <!-- TODO: Use `<component :is="" v-bind="attributesObject"></component>` to avoid all the separate components with `v-if` -->
@@ -69,7 +69,7 @@ export default defineComponent({
handleClick() { handleClick() {
(this.$refs.floatingMenu as typeof FloatingMenu).setOpen(); (this.$refs.floatingMenu as typeof FloatingMenu).setOpen();
if (this.action) this.action(); this.action?.();
}, },
}, },
}); });
@@ -161,13 +161,13 @@ export default defineComponent({
}, },
onPointerDown(e: PointerEvent) { onPointerDown(e: PointerEvent) {
const saturationPicker = this.$refs.saturationPicker as typeof LayoutCol; const saturationPicker = this.$refs.saturationPicker as typeof LayoutCol;
const saturationPickerElement = saturationPicker && (saturationPicker.$el as HTMLElement); const saturationPickerElement = saturationPicker?.$el as HTMLElement | undefined;
const huePicker = this.$refs.huePicker as typeof LayoutCol; const huePicker = this.$refs.huePicker as typeof LayoutCol;
const huePickerElement = huePicker && (huePicker.$el as HTMLElement); const huePickerElement = huePicker?.$el as HTMLElement | undefined;
const opacityPicker = this.$refs.opacityPicker as typeof LayoutCol; const opacityPicker = this.$refs.opacityPicker as typeof LayoutCol;
const opacityPickerElement = opacityPicker && (opacityPicker.$el as HTMLElement); const opacityPickerElement = opacityPicker?.$el as HTMLElement | undefined;
if (!(e.currentTarget instanceof HTMLElement) || !saturationPickerElement || !huePickerElement || !opacityPickerElement) return; if (!(e.currentTarget instanceof HTMLElement) || !saturationPickerElement || !huePickerElement || !opacityPickerElement) return;
@@ -216,13 +216,13 @@ export default defineComponent({
}, },
updateRects() { updateRects() {
const saturationPicker = this.$refs.saturationPicker as typeof LayoutCol; const saturationPicker = this.$refs.saturationPicker as typeof LayoutCol;
const saturationPickerElement = saturationPicker && (saturationPicker.$el as HTMLElement); const saturationPickerElement = saturationPicker?.$el as HTMLElement | undefined;
const huePicker = this.$refs.huePicker as typeof LayoutCol; const huePicker = this.$refs.huePicker as typeof LayoutCol;
const huePickerElement = huePicker && (huePicker.$el as HTMLElement); const huePickerElement = huePicker?.$el as HTMLElement | undefined;
const opacityPicker = this.$refs.opacityPicker as typeof LayoutCol; const opacityPicker = this.$refs.opacityPicker as typeof LayoutCol;
const opacityPickerElement = opacityPicker && (opacityPicker.$el as HTMLElement); const opacityPickerElement = opacityPicker?.$el as HTMLElement | undefined;
if (!saturationPickerElement || !huePickerElement || !opacityPickerElement) return; if (!saturationPickerElement || !huePickerElement || !opacityPickerElement) return;
@@ -9,7 +9,7 @@
<TextLabel :bold="true" class="heading">{{ dialog.state.heading }}</TextLabel> <TextLabel :bold="true" class="heading">{{ dialog.state.heading }}</TextLabel>
<TextLabel class="details">{{ dialog.state.details }}</TextLabel> <TextLabel class="details">{{ dialog.state.details }}</TextLabel>
<LayoutRow class="buttons-row" v-if="dialog.state.buttons.length > 0"> <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 && button.callback()" v-bind="button.props" /> <TextButton v-for="(button, index) in dialog.state.buttons" :key="index" :title="button.tooltip" :action="() => button.callback?.()" v-bind="button.props" />
</LayoutRow> </LayoutRow>
</LayoutCol> </LayoutCol>
</LayoutRow> </LayoutRow>
@@ -212,7 +212,7 @@ export default defineComponent({
const workspace = document.querySelector("[data-workspace]"); const workspace = document.querySelector("[data-workspace]");
const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLElement; const floatingMenuContainer = this.$refs.floatingMenuContainer as HTMLElement;
const floatingMenuContentComponent = this.$refs.floatingMenuContent as typeof LayoutCol; const floatingMenuContentComponent = this.$refs.floatingMenuContent as typeof LayoutCol;
const floatingMenuContent = floatingMenuContentComponent && (floatingMenuContentComponent.$el as HTMLElement); const floatingMenuContent: HTMLElement | undefined = floatingMenuContentComponent?.$el;
const floatingMenu = this.$refs.floatingMenu as HTMLElement; const floatingMenu = this.$refs.floatingMenu as HTMLElement;
if (!workspace || !floatingMenuContainer || !floatingMenuContentComponent || !floatingMenuContent || !floatingMenu) return; if (!workspace || !floatingMenuContainer || !floatingMenuContentComponent || !floatingMenuContent || !floatingMenu) return;
@@ -298,30 +298,28 @@ export default defineComponent({
}, },
getWidth(callback: (width: number) => void) { getWidth(callback: (width: number) => void) {
this.$nextTick(() => { this.$nextTick(() => {
const floatingMenuContent = (this.$refs.floatingMenuContent as typeof LayoutCol).$el as HTMLElement; const floatingMenuContent: HTMLElement = (this.$refs.floatingMenuContent as typeof LayoutCol).$el;
const width = floatingMenuContent.clientWidth; const width = floatingMenuContent.clientWidth;
callback(width); callback(width);
}); });
}, },
disableMinWidth(callback: (minWidth: string) => void) { disableMinWidth(callback: (minWidth: string) => void) {
this.$nextTick(() => { this.$nextTick(() => {
const floatingMenuContent = (this.$refs.floatingMenuContent as typeof LayoutCol).$el as HTMLElement; const floatingMenuContent: HTMLElement = (this.$refs.floatingMenuContent as typeof LayoutCol).$el;
const initialMinWidth = floatingMenuContent.style.minWidth; const initialMinWidth = floatingMenuContent.style.minWidth;
floatingMenuContent.style.minWidth = "0"; floatingMenuContent.style.minWidth = "0";
callback(initialMinWidth); callback(initialMinWidth);
}); });
}, },
enableMinWidth(minWidth: string) { enableMinWidth(minWidth: string) {
const floatingMenuContent = (this.$refs.floatingMenuContent as typeof LayoutCol).$el as HTMLElement; const floatingMenuContent: HTMLElement = (this.$refs.floatingMenuContent as typeof LayoutCol).$el;
floatingMenuContent.style.minWidth = minWidth; floatingMenuContent.style.minWidth = minWidth;
}, },
pointerMoveHandler(e: PointerEvent) { pointerMoveHandler(e: PointerEvent) {
const target = e.target as HTMLElement; const target = e.target as HTMLElement | undefined;
const pointerOverFloatingMenuKeepOpen = target && (target.closest("[data-hover-menu-keep-open]") as HTMLElement); const pointerOverFloatingMenuKeepOpen = target?.closest("[data-hover-menu-keep-open]") as HTMLElement | undefined;
const pointerOverFloatingMenuSpawner = target && (target.closest("[data-hover-menu-spawner]") as HTMLElement); const pointerOverFloatingMenuSpawner = target?.closest("[data-hover-menu-spawner]") as HTMLElement | undefined;
// TODO: Simplify the following expression when optional chaining is supported by the build system const pointerOverOwnFloatingMenuSpawner = pointerOverFloatingMenuSpawner?.parentElement?.contains(this.$refs.floatingMenu as HTMLElement);
const pointerOverOwnFloatingMenuSpawner =
pointerOverFloatingMenuSpawner && pointerOverFloatingMenuSpawner.parentElement && pointerOverFloatingMenuSpawner.parentElement.contains(this.$refs.floatingMenu as HTMLElement);
// Swap this open floating menu with the one created by the floating menu spawner being hovered over // Swap this open floating menu with the one created by the floating menu spawner being hovered over
if (pointerOverFloatingMenuSpawner && !pointerOverOwnFloatingMenuSpawner) { if (pointerOverFloatingMenuSpawner && !pointerOverOwnFloatingMenuSpawner) {
this.setClosed(); this.setClosed();
@@ -372,10 +370,12 @@ export default defineComponent({
}, },
isPointerEventOutsideMenuElement(e: PointerEvent, element: HTMLElement, extraDistanceAllowed = 0): boolean { isPointerEventOutsideMenuElement(e: PointerEvent, element: HTMLElement, extraDistanceAllowed = 0): boolean {
const floatingMenuBounds = element.getBoundingClientRect(); const floatingMenuBounds = element.getBoundingClientRect();
if (floatingMenuBounds.left - e.clientX >= extraDistanceAllowed) return true; if (floatingMenuBounds.left - e.clientX >= extraDistanceAllowed) return true;
if (e.clientX - floatingMenuBounds.right >= extraDistanceAllowed) return true; if (e.clientX - floatingMenuBounds.right >= extraDistanceAllowed) return true;
if (floatingMenuBounds.top - e.clientY >= extraDistanceAllowed) return true; if (floatingMenuBounds.top - e.clientY >= extraDistanceAllowed) return true;
if (e.clientY - floatingMenuBounds.bottom >= extraDistanceAllowed) return true; if (e.clientY - floatingMenuBounds.bottom >= extraDistanceAllowed) return true;
return false; return false;
}, },
}, },
@@ -19,9 +19,9 @@
<span class="entry-label">{{ entry.label }}</span> <span class="entry-label">{{ entry.label }}</span>
<IconLabel v-if="entry.shortcutRequiresLock && !fullscreen.state.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]" /> <UserInputLabel v-else-if="entry.shortcut?.length" :inputKeys="[entry.shortcut]" />
<div class="submenu-arrow" v-if="entry.children && entry.children.length"></div> <div class="submenu-arrow" v-if="entry.children?.length"></div>
<div class="no-submenu-arrow" v-else></div> <div class="no-submenu-arrow" v-else></div>
<MenuList <MenuList
@@ -175,10 +175,10 @@ const MenuList = defineComponent({
scrollableY: { type: Boolean as PropType<boolean>, default: false }, scrollableY: { type: Boolean as PropType<boolean>, default: false },
}, },
methods: { methods: {
setEntryRefs(menuEntry: MenuListEntry, ref: typeof FloatingMenu) { setEntryRefs(menuEntry: MenuListEntry, ref: typeof FloatingMenu): void {
if (ref) menuEntry.ref = ref; if (ref) menuEntry.ref = ref;
}, },
handleEntryClick(menuEntry: MenuListEntry) { handleEntryClick(menuEntry: MenuListEntry): void {
(this.$refs.floatingMenu as typeof FloatingMenu).setClosed(); (this.$refs.floatingMenu as typeof FloatingMenu).setClosed();
if (menuEntry.checkbox) menuEntry.checked = !menuEntry.checked; if (menuEntry.checkbox) menuEntry.checked = !menuEntry.checked;
@@ -188,20 +188,20 @@ const MenuList = defineComponent({
this.$emit("update:activeEntry", menuEntry); this.$emit("update:activeEntry", menuEntry);
}, },
handleEntryPointerEnter(menuEntry: MenuListEntry) { handleEntryPointerEnter(menuEntry: MenuListEntry): void {
if (!menuEntry.children || !menuEntry.children.length) return; if (!menuEntry.children?.length) return;
if (menuEntry.ref) menuEntry.ref.setOpen(); if (menuEntry.ref) menuEntry.ref.setOpen();
else throw new Error("The menu bar floating menu has no associated ref"); else throw new Error("The menu bar floating menu has no associated ref");
}, },
handleEntryPointerLeave(menuEntry: MenuListEntry) { handleEntryPointerLeave(menuEntry: MenuListEntry): void {
if (!menuEntry.children || !menuEntry.children.length) return; if (!menuEntry.children?.length) return;
if (menuEntry.ref) menuEntry.ref.setClosed(); if (menuEntry.ref) menuEntry.ref.setClosed();
else throw new Error("The menu bar floating menu has no associated ref"); else throw new Error("The menu bar floating menu has no associated ref");
}, },
isMenuEntryOpen(menuEntry: MenuListEntry): boolean { isMenuEntryOpen(menuEntry: MenuListEntry): boolean {
if (!menuEntry.children || !menuEntry.children.length) return false; if (!menuEntry.children?.length) return false;
if (menuEntry.ref) return menuEntry.ref.isOpen(); if (menuEntry.ref) return menuEntry.ref.isOpen();
@@ -215,7 +215,7 @@ const MenuList = defineComponent({
}, },
isOpen(): boolean { isOpen(): boolean {
const floatingMenu = this.$refs.floatingMenu as typeof FloatingMenu; const floatingMenu = this.$refs.floatingMenu as typeof FloatingMenu;
return Boolean(floatingMenu && floatingMenu.isOpen()); return Boolean(floatingMenu?.isOpen());
}, },
async measureAndReportWidth() { async measureAndReportWidth() {
// API is experimental but supported in all browsers - https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready // API is experimental but supported in all browsers - https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready
@@ -242,10 +242,8 @@ const MenuList = defineComponent({
}, },
computed: { computed: {
menuEntriesWithoutRefs(): MenuListEntryData[][] { menuEntriesWithoutRefs(): MenuListEntryData[][] {
const { menuEntries } = this; return this.menuEntries.map((entries) =>
return menuEntries.map((entries) =>
entries.map((entry) => { entries.map((entry) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { ref, ...entryWithoutRef } = entry; const { ref, ...entryWithoutRef } = entry;
return entryWithoutRef; return entryWithoutRef;
}) })
@@ -112,34 +112,45 @@ export default defineComponent({
if (!this.disabled) (this.$refs.menuList as typeof MenuList).setOpen(); if (!this.disabled) (this.$refs.menuList as typeof MenuList).setOpen();
}, },
selectFont(newName: string) { selectFont(newName: string) {
if (this.isStyle) this.$emit("update:fontStyle", newName); let fontFamily;
else this.$emit("update:fontFamily", newName); let fontStyle;
{ if (this.isStyle) {
const fontFamily = this.isStyle ? this.fontFamily : newName; this.$emit("update:fontStyle", newName);
const fontStyle = this.isStyle ? newName : getFontStyles(newName)[0];
const fontFile = getFontFile(fontFamily, fontStyle); fontFamily = this.fontFamily;
this.$emit("changeFont", { fontFamily, fontStyle, fontFile }); fontStyle = newName;
} else {
this.$emit("update:fontFamily", newName);
fontFamily = newName;
fontStyle = getFontStyles(newName)[0];
} }
const fontFile = getFontFile(fontFamily, fontStyle);
this.$emit("changeFont", { fontFamily, fontStyle, fontFile });
}, },
onWidthChanged(newWidth: number) { onWidthChanged(newWidth: number) {
this.minWidth = newWidth; this.minWidth = newWidth;
}, },
updateEntries(): { menuEntries: SectionsOfMenuListEntries; activeEntry: MenuListEntry } { updateEntries(): { menuEntries: SectionsOfMenuListEntries; activeEntry: MenuListEntry } {
let selectedIndex = -1; const choices = this.isStyle ? getFontStyles(this.fontFamily) : fontNames();
const menuEntries: SectionsOfMenuListEntries = [ const selectedChoice = this.isStyle ? this.fontStyle : this.fontFamily;
(this.isStyle ? getFontStyles(this.fontFamily) : fontNames()).map((name, index) => {
if (name === (this.isStyle ? this.fontStyle : this.fontFamily)) selectedIndex = index;
const result: MenuListEntry = { let selectedEntry: MenuListEntry | undefined;
label: name, const entries = choices.map((name) => {
action: (): void => this.selectFont(name), const result: MenuListEntry = {
}; label: name,
return result; action: (): void => this.selectFont(name),
}), };
];
const activeEntry = selectedIndex < 0 ? { label: "-" } : menuEntries.flat()[selectedIndex]; if (name === selectedChoice) selectedEntry = result;
return result;
});
const menuEntries: SectionsOfMenuListEntries = [entries];
const activeEntry = selectedEntry || { label: "-" };
return { menuEntries, activeEntry }; return { menuEntries, activeEntry };
}, },
@@ -6,7 +6,7 @@
</div> </div>
</div> </div>
<div class="entry-container" v-for="(entry, index) in menuEntries" :key="index"> <div class="entry-container" v-for="(entry, index) in menuEntries" :key="index">
<div @click="() => handleEntryClick(entry)" class="entry" :class="{ open: entry.ref && entry.ref.isOpen() }" data-hover-menu-spawner> <div @click="() => handleEntryClick(entry)" class="entry" :class="{ open: entry.ref?.isOpen() }" data-hover-menu-spawner>
<IconLabel :icon="entry.icon" v-if="entry.icon" /> <IconLabel :icon="entry.icon" v-if="entry.icon" />
<span v-if="entry.label">{{ entry.label }}</span> <span v-if="entry.label">{{ entry.label }}</span>
</div> </div>
@@ -152,7 +152,7 @@ export default defineComponent({
onIncrement(direction: IncrementDirection) { onIncrement(direction: IncrementDirection) {
if (Number.isNaN(this.value)) return; if (Number.isNaN(this.value)) return;
({ const actions = {
Add: (): void => { Add: (): void => {
const directionAddend = direction === "Increase" ? this.incrementFactor : -this.incrementFactor; const directionAddend = direction === "Increase" ? this.incrementFactor : -this.incrementFactor;
this.updateValue(this.value + directionAddend); this.updateValue(this.value + directionAddend);
@@ -162,11 +162,13 @@ export default defineComponent({
this.updateValue(this.value * directionMultiplier); this.updateValue(this.value * directionMultiplier);
}, },
Callback: (): void => { Callback: (): void => {
if (direction === "Increase" && this.incrementCallbackIncrease) this.incrementCallbackIncrease(); if (direction === "Increase") this.incrementCallbackIncrease?.();
if (direction === "Decrease" && this.incrementCallbackDecrease) this.incrementCallbackDecrease(); if (direction === "Decrease") this.incrementCallbackDecrease?.();
}, },
None: (): void => undefined, None: (): void => undefined,
}[this.incrementBehavior]()); };
const action = actions[this.incrementBehavior];
action();
}, },
updateValue(newValue: number) { updateValue(newValue: number) {
const invalid = Number.isNaN(newValue); const invalid = Number.isNaN(newValue);
@@ -91,7 +91,7 @@ export default defineComponent({
const index = this.entries.indexOf(menuEntry); const index = this.entries.indexOf(menuEntry);
this.$emit("update:selectedIndex", index); this.$emit("update:selectedIndex", index);
if (menuEntry.action) menuEntry.action(); menuEntry.action?.();
}, },
}, },
components: { components: {
@@ -27,14 +27,12 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import { DefineComponent, defineComponent, PropType } from "vue"; import { defineComponent, PropType } from "vue";
import { IconName, IconSize, ICON_LIST } from "@/utilities/icons"; import { IconName, icons } from "@/utilities/icons";
import LayoutRow from "@/components/layout/LayoutRow.vue"; import LayoutRow from "@/components/layout/LayoutRow.vue";
const icons: Record<IconName, { component: DefineComponent; size: IconSize }> = ICON_LIST;
export default defineComponent({ export default defineComponent({
props: { props: {
icon: { type: String as PropType<IconName>, required: true }, icon: { type: String as PropType<IconName>, required: true },
@@ -47,6 +45,7 @@ export default defineComponent({
}, },
components: { components: {
LayoutRow, LayoutRow,
// Import the components of all the icons
...Object.fromEntries(Object.entries(icons).map(([name, data]) => [name, data.component])), ...Object.fromEntries(Object.entries(icons).map(([name, data]) => [name, data.component])),
}, },
}); });
@@ -52,7 +52,7 @@ const MINOR_MARK_THICKNESS = 3;
export type RulerDirection = "Horizontal" | "Vertical"; export type RulerDirection = "Horizontal" | "Vertical";
// Apparently the modulo operator in js does not work properly. // Modulo function that works for negative numbers, unlike the JS % operator
const mod = (n: number, m: number): number => { const mod = (n: number, m: number): number => {
const remainder = n % m; const remainder = n % m;
return Math.floor(remainder >= 0 ? remainder : remainder + m); return Math.floor(remainder >= 0 ? remainder : remainder + m);
@@ -190,9 +190,7 @@ export default defineComponent({
this.dragging = false; this.dragging = false;
}, },
pointerMove(e: PointerEvent) { pointerMove(e: PointerEvent) {
if (this.dragging) { if (this.dragging) this.updateHandlePosition(e);
this.updateHandlePosition(e);
}
}, },
changePosition(difference: number) { changePosition(difference: number) {
this.clampHandlePosition(this.handlePosition + difference / this.trackLength()); this.clampHandlePosition(this.handlePosition + difference / this.trackLength());
+3 -3
View File
@@ -8,11 +8,11 @@
data-tab data-tab
v-for="(tabLabel, tabIndex) in tabLabels" v-for="(tabLabel, tabIndex) in tabLabels"
:key="tabIndex" :key="tabIndex"
@click="(e) => (e && e.stopPropagation(), clickAction && clickAction(tabIndex))" @click="(e) => (e?.stopPropagation(), clickAction?.(tabIndex))"
@click.middle="(e) => (e && e.stopPropagation(), closeAction && closeAction(tabIndex))" @click.middle="(e) => (e?.stopPropagation(), closeAction?.(tabIndex))"
> >
<span>{{ tabLabel }}</span> <span>{{ tabLabel }}</span>
<IconButton :action="(e) => (e && e.stopPropagation(), closeAction && closeAction(tabIndex))" :icon="'CloseX'" :size="16" v-if="tabCloseButtons" /> <IconButton :action="(e) => (e?.stopPropagation(), closeAction?.(tabIndex))" :icon="'CloseX'" :size="16" v-if="tabCloseButtons" />
</LayoutRow> </LayoutRow>
</LayoutRow> </LayoutRow>
<PopoverButton :icon="'VerticalEllipsis'"> <PopoverButton :icon="'VerticalEllipsis'">
@@ -7,18 +7,8 @@
:tabCloseButtons="true" :tabCloseButtons="true"
:tabMinWidths="true" :tabMinWidths="true"
:tabLabels="documents.state.documents.map((doc) => doc.displayName)" :tabLabels="documents.state.documents.map((doc) => doc.displayName)"
:clickAction=" :clickAction="(tabIndex) => editor.instance.select_document(documents.state.documents[tabIndex].id)"
(tabIndex) => { :closeAction="(tabIndex) => editor.instance.close_document_with_confirmation(documents.state.documents[tabIndex].id)"
const targetId = documents.state.documents[tabIndex].id;
editor.instance.select_document(targetId);
}
"
:closeAction="
(tabIndex) => {
const targetId = documents.state.documents[tabIndex].id;
editor.instance.close_document_with_confirmation(targetId);
}
"
:tabActiveIndex="documents.state.activeDocumentIndex" :tabActiveIndex="documents.state.activeDocumentIndex"
ref="documentsPanel" ref="documentsPanel"
/> />
+20 -15
View File
@@ -1,6 +1,6 @@
import { plainToInstance } from "class-transformer"; import { plainToInstance } from "class-transformer";
import { JsMessageType, messageConstructors, JsMessage } from "@/dispatcher/js-messages"; import { JsMessageType, messageMakers, JsMessage } from "@/dispatcher/js-messages";
import type { RustEditorInstance, WasmInstance } from "@/state/wasm-loader"; import type { RustEditorInstance, WasmInstance } from "@/state/wasm-loader";
type JsMessageCallback<T extends JsMessage> = (messageData: T) => void; type JsMessageCallback<T extends JsMessage> = (messageData: T) => void;
@@ -20,36 +20,41 @@ export function createJsDispatcher() {
}; };
const handleJsMessage = (messageType: JsMessageType, messageData: Record<string, unknown>, wasm: WasmInstance, instance: RustEditorInstance): void => { const handleJsMessage = (messageType: JsMessageType, messageData: Record<string, unknown>, wasm: WasmInstance, instance: RustEditorInstance): void => {
const messageConstructor = messageConstructors[messageType]; // Find the message maker for the message type, which can either be a JS class constructor or a function that returns an instance of the JS class
if (!messageConstructor) { const messageMaker = messageMakers[messageType];
if (!messageMaker) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error( console.error(
`Received a frontend message of type "${messageType}" but was not able to parse the data. ` + `Received a frontend message of type "${messageType}" but was not able to parse the data. ` +
"(Perhaps this message parser isn't exported in `messageConstructors` at the bottom of `js-messages.ts`.)" "(Perhaps this message parser isn't exported in `messageMakers` at the bottom of `js-messages.ts`.)"
); );
return; return;
} }
// Checks if the provided `messageMaker` is a class extending `JsMessage`. All classes inheriting from `JsMessage` will have a static readonly `jsMessageMarker` which is `true`.
const isJsMessageMaker = (fn: typeof messageMaker): fn is typeof JsMessage => "jsMessageMarker" in fn;
const messageIsClass = isJsMessageMaker(messageMaker);
// Messages with non-empty data are provided by wasm-bindgen as an object with one key as the message name, like: { NameOfThisMessage: { ... } } // Messages with non-empty data are provided by wasm-bindgen as an object with one key as the message name, like: { NameOfThisMessage: { ... } }
// Messages with empty data are provided by wasm-bindgen as a string with the message name, like: "NameOfThisMessage" // Messages with empty data are provided by wasm-bindgen as a string with the message name, like: "NameOfThisMessage"
// Here we extract the payload object or use an empty object depending on the situation.
const unwrappedMessageData = messageData[messageType] || {}; const unwrappedMessageData = messageData[messageType] || {};
const isJsMessageConstructor = (fn: typeof messageConstructor): fn is typeof JsMessage => "jsMessageMarker" in fn; // Converts to a `JsMessage` object by turning the JSON message data into an instance of the message class, either automatically or by calling the function that builds it.
let message: JsMessage; // If the `messageMaker` is a `JsMessage` class then we use the class-transformer library's `plainToInstance` function in order to convert the JSON data into the destination class.
if (isJsMessageConstructor(messageConstructor)) { // If it is not a `JsMessage` then it should be a custom function that creates a JsMessage from a JSON, so we call the function itself with the raw JSON as an argument.
message = plainToInstance(messageConstructor, unwrappedMessageData); // The resulting `message` is an instance of a class that extends `JsMessage`.
} else { const message = messageIsClass ? plainToInstance(messageMaker, unwrappedMessageData) : messageMaker(unwrappedMessageData, wasm, instance);
message = messageConstructor(unwrappedMessageData, wasm, instance);
}
// It is ok to use constructor.name even with minification since it is used consistently with registerHandler // It is ok to use constructor.name even with minification since it is used consistently with registerHandler
const callback = subscriptions[message.constructor.name]; const callback = subscriptions[message.constructor.name];
if (callback && message) { // If we have constructed a valid message, then we try and execute the callback that the frontend has associated with this message.
callback(message); // The frontend should always have a callback for all messages, and so we display an error if one was not found.
} else if (message) { if (message) {
if (callback) callback(message);
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(`Received a frontend message of type "${messageType}" but no handler was registered for it from the client.`); else console.error(`Received a frontend message of type "${messageType}" but no handler was registered for it from the client.`);
} }
}; };
+16 -16
View File
@@ -244,9 +244,9 @@ interface DataBuffer {
} }
export function newDisplayDocumentLayerTreeStructure(input: { data_buffer: DataBuffer }, wasm: WasmInstance): DisplayDocumentLayerTreeStructure { export function newDisplayDocumentLayerTreeStructure(input: { data_buffer: DataBuffer }, wasm: WasmInstance): DisplayDocumentLayerTreeStructure {
const { pointer, length } = input.data_buffer; const pointerNum = Number(input.data_buffer.pointer);
const pointerNum = Number(pointer); const lengthNum = Number(input.data_buffer.length);
const lengthNum = Number(length);
const wasmMemoryBuffer = wasm.wasm_memory().buffer; const wasmMemoryBuffer = wasm.wasm_memory().buffer;
// Decode the folder structure encoding // Decode the folder structure encoding
@@ -265,7 +265,7 @@ export function newDisplayDocumentLayerTreeStructure(input: { data_buffer: DataB
for (let i = 0; i < structureSectionLength; i += 1) { for (let i = 0; i < structureSectionLength; i += 1) {
const msbSigned = structureSectionMsbSigned.getBigUint64(i * 8, true); const msbSigned = structureSectionMsbSigned.getBigUint64(i * 8, true);
const msbMask = BigInt(1) << BigInt(63); const msbMask = BigInt(1) << BigInt(64 - 1);
// Set the MSB to 0 to clear the sign and then read the number as usual // Set the MSB to 0 to clear the sign and then read the number as usual
const numberOfLayersAtThisDepth = msbSigned & ~msbMask; const numberOfLayersAtThisDepth = msbSigned & ~msbMask;
@@ -485,7 +485,7 @@ export class UpdatePropertyPanelSectionsLayout extends JsMessage implements Widg
// Unpacking rust types to more usable type in the frontend // Unpacking rust types to more usable type in the frontend
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
function createWidgetLayout(widgetLayout: any[]): LayoutRow[] { function createWidgetLayout(widgetLayout: any[]): LayoutRow[] {
return widgetLayout.map((rowOrSection) => { return widgetLayout.map((rowOrSection): LayoutRow => {
if (rowOrSection.Row) { if (rowOrSection.Row) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const widgets = rowOrSection.Row.widgets.map((widgetHolder: any) => { const widgets = rowOrSection.Row.widgets.map((widgetHolder: any) => {
@@ -496,16 +496,16 @@ function createWidgetLayout(widgetLayout: any[]): LayoutRow[] {
return { widget_id, kind, props }; return { widget_id, kind, props };
}); });
return { const result: WidgetRow = { widgets };
name: rowOrSection.Row.name, return result;
widgets,
};
} }
if (rowOrSection.Section) { if (rowOrSection.Section) {
return { const { name } = rowOrSection.Section;
name: rowOrSection.Section.name, const layout = createWidgetLayout(rowOrSection.Section.layout);
layout: createWidgetLayout(rowOrSection.Section.layout),
}; const result: WidgetSection = { name, layout };
return result;
} }
throw new Error("Layout row type does not exist"); throw new Error("Layout row type does not exist");
@@ -524,12 +524,12 @@ export class TriggerTextCopy extends JsMessage {
export class TriggerViewportResize extends JsMessage {} export class TriggerViewportResize extends JsMessage {}
// Any is used since the type of the object should be known from the rust side // `any` is used since the type of the object should be known from the Rust side
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
type JSMessageFactory = (data: any, wasm: WasmInstance, instance: RustEditorInstance) => JsMessage; type JSMessageFactory = (data: any, wasm: WasmInstance, instance: RustEditorInstance) => JsMessage;
type MessageMaker = typeof JsMessage | JSMessageFactory; type MessageMaker = typeof JsMessage | JSMessageFactory;
export const messageConstructors: Record<string, MessageMaker> = { export const messageMakers: Record<string, MessageMaker> = {
DisplayConfirmationToCloseAllDocuments, DisplayConfirmationToCloseAllDocuments,
DisplayConfirmationToCloseDocument, DisplayConfirmationToCloseDocument,
DisplayDialogAboutGraphite, DisplayDialogAboutGraphite,
@@ -568,4 +568,4 @@ export const messageConstructors: Record<string, MessageMaker> = {
UpdateToolOptionsLayout, UpdateToolOptionsLayout,
UpdateWorkingColors, UpdateWorkingColors,
} as const; } as const;
export type JsMessageType = keyof typeof messageConstructors; export type JsMessageType = keyof typeof messageMakers;
+1 -3
View File
@@ -86,7 +86,5 @@ export function createAutoSaveManager(editor: EditorState, documents: DocumentsS
// On creation // On creation
openAutoSavedDocuments(); openAutoSavedDocuments();
return { return { openAutoSavedDocuments };
openAutoSavedDocuments,
};
} }
+342 -335
View File
@@ -42,9 +42,14 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
if (!key) return false; if (!key) return false;
// Don't redirect user input from text entry into HTML elements // Don't redirect user input from text entry into HTML elements
const { target } = e; if (
if (key !== "escape" && !(key === "enter" && e.ctrlKey) && target instanceof HTMLElement && (target.nodeName === "INPUT" || target.nodeName === "TEXTAREA" || target.isContentEditable)) key !== "escape" &&
!(key === "enter" && e.ctrlKey) &&
e.target instanceof HTMLElement &&
(e.target.nodeName === "INPUT" || e.target.nodeName === "TEXTAREA" || e.target.isContentEditable)
) {
return false; return false;
}
// Don't redirect paste // Don't redirect paste
if (key === "v" && e.ctrlKey) return false; if (key === "v" && e.ctrlKey) return false;
@@ -124,9 +129,10 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
e.stopPropagation(); e.stopPropagation();
} }
if (textInput && !inTextInput) { if (!inTextInput) {
editor.instance.on_change_text(textInputCleanup(textInput.innerText)); if (textInput) editor.instance.on_change_text(textInputCleanup(textInput.innerText));
} else if (inCanvas && !inTextInput) viewportPointerInteractionOngoing = true; else if (inCanvas) viewportPointerInteractionOngoing = true;
}
if (viewportPointerInteractionOngoing) { if (viewportPointerInteractionOngoing) {
const modifiers = makeModifiersBitfield(e); const modifiers = makeModifiersBitfield(e);
@@ -164,6 +170,8 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
const { target } = e; const { target } = e;
const inCanvas = target instanceof Element && target.closest("[data-canvas]"); const inCanvas = target instanceof Element && target.closest("[data-canvas]");
// Redirect vertical scroll wheel movement into a horizontal scroll on a horizontally scrollable element
// There seems to be no possible way to properly employ the browser's smooth scrolling interpolation
const horizontalScrollableElement = target instanceof Element && target.closest("[data-scrollable-x]"); const horizontalScrollableElement = target instanceof Element && target.closest("[data-scrollable-x]");
if (horizontalScrollableElement && e.deltaY !== 0) { if (horizontalScrollableElement && e.deltaY !== 0) {
horizontalScrollableElement.scrollTo(horizontalScrollableElement.scrollLeft + e.deltaY, 0); horizontalScrollableElement.scrollTo(horizontalScrollableElement.scrollLeft + e.deltaY, 0);
@@ -228,7 +236,7 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
} }
const file = item.getAsFile(); const file = item.getAsFile();
if (file && file.type.startsWith("image")) { if (file?.type.startsWith("image")) {
file.arrayBuffer().then((buffer): void => { file.arrayBuffer().then((buffer): void => {
const u8Array = new Uint8Array(buffer); const u8Array = new Uint8Array(buffer);
@@ -252,9 +260,7 @@ export function createInputManager(editor: EditorState, container: HTMLElement,
addListeners(); addListeners();
onWindowResize(container); onWindowResize(container);
return { return { removeListeners };
removeListeners,
};
} }
export type InputManager = ReturnType<typeof createInputManager>; export type InputManager = ReturnType<typeof createInputManager>;
@@ -296,335 +302,336 @@ function keyCodeToKey(code: string): string | null {
if (code.match(/^F[1-9]|F1[0-9]|F20$/)) return code.replace("F", "").toLowerCase(); if (code.match(/^F[1-9]|F1[0-9]|F20$/)) return code.replace("F", "").toLowerCase();
// Other characters // Other characters
const mapping: Record<string, string> = { if (MAPPING[code]) return MAPPING[code];
BracketLeft: "[",
BracketRight: "]",
Backslash: "\\",
Slash: "/",
Period: ".",
Comma: ",",
Equal: "=",
Minus: "-",
Quote: "'",
Semicolon: ";",
NumpadEqual: "=",
NumpadDivide: "/",
NumpadMultiply: "*",
NumpadSubtract: "-",
NumpadAdd: "+",
NumpadDecimal: ".",
};
if (code in mapping) return mapping[code];
return null; return null;
} }
function isKeyPrintable(key: string): boolean { const MAPPING: Record<string, string> = {
const allPrintableKeys: string[] = [ BracketLeft: "[",
// Modifier BracketRight: "]",
"Alt", Backslash: "\\",
"AltGraph", Slash: "/",
"CapsLock", Period: ".",
"Control", Comma: ",",
"Fn", Equal: "=",
"FnLock", Minus: "-",
"Meta", Quote: "'",
"NumLock", Semicolon: ";",
"ScrollLock", NumpadEqual: "=",
"Shift", NumpadDivide: "/",
"Symbol", NumpadMultiply: "*",
"SymbolLock", NumpadSubtract: "-",
// Legacy modifier NumpadAdd: "+",
"Hyper", NumpadDecimal: ".",
"Super", };
// White space
"Enter",
"Tab",
// Navigation
"ArrowDown",
"ArrowLeft",
"ArrowRight",
"ArrowUp",
"End",
"Home",
"PageDown",
"PageUp",
// Editing
"Backspace",
"Clear",
"Copy",
"CrSel",
"Cut",
"Delete",
"EraseEof",
"ExSel",
"Insert",
"Paste",
"Redo",
"Undo",
// UI
"Accept",
"Again",
"Attn",
"Cancel",
"ContextMenu",
"Escape",
"Execute",
"Find",
"Help",
"Pause",
"Play",
"Props",
"Select",
"ZoomIn",
"ZoomOut",
// Device
"BrightnessDown",
"BrightnessUp",
"Eject",
"LogOff",
"Power",
"PowerOff",
"PrintScreen",
"Hibernate",
"Standby",
"WakeUp",
// IME composition keys
"AllCandidates",
"Alphanumeric",
"CodeInput",
"Compose",
"Convert",
"Dead",
"FinalMode",
"GroupFirst",
"GroupLast",
"GroupNext",
"GroupPrevious",
"ModeChange",
"NextCandidate",
"NonConvert",
"PreviousCandidate",
"Process",
"SingleCandidate",
// Korean-specific
"HangulMode",
"HanjaMode",
"JunjaMode",
// Japanese-specific
"Eisu",
"Hankaku",
"Hiragana",
"HiraganaKatakana",
"KanaMode",
"KanjiMode",
"Katakana",
"Romaji",
"Zenkaku",
"ZenkakuHankaku",
// Common function
"F1",
"F2",
"F3",
"F4",
"F5",
"F6",
"F7",
"F8",
"F9",
"F10",
"F11",
"F12",
"Soft1",
"Soft2",
"Soft3",
"Soft4",
// Multimedia
"ChannelDown",
"ChannelUp",
"Close",
"MailForward",
"MailReply",
"MailSend",
"MediaClose",
"MediaFastForward",
"MediaPause",
"MediaPlay",
"MediaPlayPause",
"MediaRecord",
"MediaRewind",
"MediaStop",
"MediaTrackNext",
"MediaTrackPrevious",
"New",
"Open",
"Print",
"Save",
"SpellCheck",
// Multimedia numpad
"Key11",
"Key12",
// Audio
"AudioBalanceLeft",
"AudioBalanceRight",
"AudioBassBoostDown",
"AudioBassBoostToggle",
"AudioBassBoostUp",
"AudioFaderFront",
"AudioFaderRear",
"AudioSurroundModeNext",
"AudioTrebleDown",
"AudioTrebleUp",
"AudioVolumeDown",
"AudioVolumeUp",
"AudioVolumeMute",
"MicrophoneToggle",
"MicrophoneVolumeDown",
"MicrophoneVolumeUp",
"MicrophoneVolumeMute",
// Speech
"SpeechCorrectionList",
"SpeechInputToggle",
// Application
"LaunchApplication1",
"LaunchApplication2",
"LaunchCalendar",
"LaunchContacts",
"LaunchMail",
"LaunchMediaPlayer",
"LaunchMusicPlayer",
"LaunchPhone",
"LaunchScreenSaver",
"LaunchSpreadsheet",
"LaunchWebBrowser",
"LaunchWebCam",
"LaunchWordProcessor",
// Browser
"BrowserBack",
"BrowserFavorites",
"BrowserForward",
"BrowserHome",
"BrowserRefresh",
"BrowserSearch",
"BrowserStop",
// Mobile phone
"AppSwitch",
"Call",
"Camera",
"CameraFocus",
"EndCall",
"GoBack",
"GoHome",
"HeadsetHook",
"LastNumberRedial",
"Notification",
"MannerMode",
"VoiceDial",
// TV
"TV",
"TV3DMode",
"TVAntennaCable",
"TVAudioDescription",
"TVAudioDescriptionMixDown",
"TVAudioDescriptionMixUp",
"TVContentsMenu",
"TVDataService",
"TVInput",
"TVInputComponent1",
"TVInputComponent2",
"TVInputComposite1",
"TVInputComposite2",
"TVInputHDMI1",
"TVInputHDMI2",
"TVInputHDMI3",
"TVInputHDMI4",
"TVInputVGA1",
"TVMediaContext",
"TVNetwork",
"TVNumberEntry",
"TVPower",
"TVRadioService",
"TVSatellite",
"TVSatelliteBS",
"TVSatelliteCS",
"TVSatelliteToggle",
"TVTerrestrialAnalog",
"TVTerrestrialDigital",
"TVTimer",
// Media controls
"AVRInput",
"AVRPower",
"ColorF0Red",
"ColorF1Green",
"ColorF2Yellow",
"ColorF3Blue",
"ColorF4Grey",
"ColorF5Brown",
"ClosedCaptionToggle",
"Dimmer",
"DisplaySwap",
"DVR",
"Exit",
"FavoriteClear0",
"FavoriteClear1",
"FavoriteClear2",
"FavoriteClear3",
"FavoriteRecall0",
"FavoriteRecall1",
"FavoriteRecall2",
"FavoriteRecall3",
"FavoriteStore0",
"FavoriteStore1",
"FavoriteStore2",
"FavoriteStore3",
"Guide",
"GuideNextDay",
"GuidePreviousDay",
"Info",
"InstantReplay",
"Link",
"ListProgram",
"LiveContent",
"Lock",
"MediaApps",
"MediaAudioTrack",
"MediaLast",
"MediaSkipBackward",
"MediaSkipForward",
"MediaStepBackward",
"MediaStepForward",
"MediaTopMenu",
"NavigateIn",
"NavigateNext",
"NavigateOut",
"NavigatePrevious",
"NextFavoriteChannel",
"NextUserProfile",
"OnDemand",
"Pairing",
"PinPDown",
"PinPMove",
"PinPToggle",
"PinPUp",
"PlaySpeedDown",
"PlaySpeedReset",
"PlaySpeedUp",
"RandomToggle",
"RcLowBattery",
"RecordSpeedNext",
"RfBypass",
"ScanChannelsToggle",
"ScreenModeNext",
"Settings",
"SplitScreenToggle",
"STBInput",
"STBPower",
"Subtitle",
"Teletext",
"VideoModeNext",
"Wink",
"ZoomToggle",
];
return !allPrintableKeys.includes(key); function isKeyPrintable(key: string): boolean {
return !ALL_PRINTABLE_KEYS.has(key);
} }
const ALL_PRINTABLE_KEYS = new Set([
// Modifier
"Alt",
"AltGraph",
"CapsLock",
"Control",
"Fn",
"FnLock",
"Meta",
"NumLock",
"ScrollLock",
"Shift",
"Symbol",
"SymbolLock",
// Legacy modifier
"Hyper",
"Super",
// White space
"Enter",
"Tab",
// Navigation
"ArrowDown",
"ArrowLeft",
"ArrowRight",
"ArrowUp",
"End",
"Home",
"PageDown",
"PageUp",
// Editing
"Backspace",
"Clear",
"Copy",
"CrSel",
"Cut",
"Delete",
"EraseEof",
"ExSel",
"Insert",
"Paste",
"Redo",
"Undo",
// UI
"Accept",
"Again",
"Attn",
"Cancel",
"ContextMenu",
"Escape",
"Execute",
"Find",
"Help",
"Pause",
"Play",
"Props",
"Select",
"ZoomIn",
"ZoomOut",
// Device
"BrightnessDown",
"BrightnessUp",
"Eject",
"LogOff",
"Power",
"PowerOff",
"PrintScreen",
"Hibernate",
"Standby",
"WakeUp",
// IME composition keys
"AllCandidates",
"Alphanumeric",
"CodeInput",
"Compose",
"Convert",
"Dead",
"FinalMode",
"GroupFirst",
"GroupLast",
"GroupNext",
"GroupPrevious",
"ModeChange",
"NextCandidate",
"NonConvert",
"PreviousCandidate",
"Process",
"SingleCandidate",
// Korean-specific
"HangulMode",
"HanjaMode",
"JunjaMode",
// Japanese-specific
"Eisu",
"Hankaku",
"Hiragana",
"HiraganaKatakana",
"KanaMode",
"KanjiMode",
"Katakana",
"Romaji",
"Zenkaku",
"ZenkakuHankaku",
// Common function
"F1",
"F2",
"F3",
"F4",
"F5",
"F6",
"F7",
"F8",
"F9",
"F10",
"F11",
"F12",
"Soft1",
"Soft2",
"Soft3",
"Soft4",
// Multimedia
"ChannelDown",
"ChannelUp",
"Close",
"MailForward",
"MailReply",
"MailSend",
"MediaClose",
"MediaFastForward",
"MediaPause",
"MediaPlay",
"MediaPlayPause",
"MediaRecord",
"MediaRewind",
"MediaStop",
"MediaTrackNext",
"MediaTrackPrevious",
"New",
"Open",
"Print",
"Save",
"SpellCheck",
// Multimedia numpad
"Key11",
"Key12",
// Audio
"AudioBalanceLeft",
"AudioBalanceRight",
"AudioBassBoostDown",
"AudioBassBoostToggle",
"AudioBassBoostUp",
"AudioFaderFront",
"AudioFaderRear",
"AudioSurroundModeNext",
"AudioTrebleDown",
"AudioTrebleUp",
"AudioVolumeDown",
"AudioVolumeUp",
"AudioVolumeMute",
"MicrophoneToggle",
"MicrophoneVolumeDown",
"MicrophoneVolumeUp",
"MicrophoneVolumeMute",
// Speech
"SpeechCorrectionList",
"SpeechInputToggle",
// Application
"LaunchApplication1",
"LaunchApplication2",
"LaunchCalendar",
"LaunchContacts",
"LaunchMail",
"LaunchMediaPlayer",
"LaunchMusicPlayer",
"LaunchPhone",
"LaunchScreenSaver",
"LaunchSpreadsheet",
"LaunchWebBrowser",
"LaunchWebCam",
"LaunchWordProcessor",
// Browser
"BrowserBack",
"BrowserFavorites",
"BrowserForward",
"BrowserHome",
"BrowserRefresh",
"BrowserSearch",
"BrowserStop",
// Mobile phone
"AppSwitch",
"Call",
"Camera",
"CameraFocus",
"EndCall",
"GoBack",
"GoHome",
"HeadsetHook",
"LastNumberRedial",
"Notification",
"MannerMode",
"VoiceDial",
// TV
"TV",
"TV3DMode",
"TVAntennaCable",
"TVAudioDescription",
"TVAudioDescriptionMixDown",
"TVAudioDescriptionMixUp",
"TVContentsMenu",
"TVDataService",
"TVInput",
"TVInputComponent1",
"TVInputComponent2",
"TVInputComposite1",
"TVInputComposite2",
"TVInputHDMI1",
"TVInputHDMI2",
"TVInputHDMI3",
"TVInputHDMI4",
"TVInputVGA1",
"TVMediaContext",
"TVNetwork",
"TVNumberEntry",
"TVPower",
"TVRadioService",
"TVSatellite",
"TVSatelliteBS",
"TVSatelliteCS",
"TVSatelliteToggle",
"TVTerrestrialAnalog",
"TVTerrestrialDigital",
"TVTimer",
// Media controls
"AVRInput",
"AVRPower",
"ColorF0Red",
"ColorF1Green",
"ColorF2Yellow",
"ColorF3Blue",
"ColorF4Grey",
"ColorF5Brown",
"ClosedCaptionToggle",
"Dimmer",
"DisplaySwap",
"DVR",
"Exit",
"FavoriteClear0",
"FavoriteClear1",
"FavoriteClear2",
"FavoriteClear3",
"FavoriteRecall0",
"FavoriteRecall1",
"FavoriteRecall2",
"FavoriteRecall3",
"FavoriteStore0",
"FavoriteStore1",
"FavoriteStore2",
"FavoriteStore3",
"Guide",
"GuideNextDay",
"GuidePreviousDay",
"Info",
"InstantReplay",
"Link",
"ListProgram",
"LiveContent",
"Lock",
"MediaApps",
"MediaAudioTrack",
"MediaLast",
"MediaSkipBackward",
"MediaSkipForward",
"MediaStepBackward",
"MediaStepForward",
"MediaTopMenu",
"NavigateIn",
"NavigateNext",
"NavigateOut",
"NavigatePrevious",
"NextFavoriteChannel",
"NextUserProfile",
"OnDemand",
"Pairing",
"PinPDown",
"PinPMove",
"PinPToggle",
"PinPUp",
"PlaySpeedDown",
"PlaySpeedReset",
"PlaySpeedUp",
"RandomToggle",
"RcLowBattery",
"RecordSpeedNext",
"RfBypass",
"ScanChannelsToggle",
"ScreenModeNext",
"Settings",
"SplitScreenToggle",
"STBInput",
"STBPower",
"Subtitle",
"Teletext",
"VideoModeNext",
"Wink",
"ZoomToggle",
]);
+3 -7
View File
@@ -30,10 +30,7 @@ export function createDialogState(editor: EditorState) {
const submitDialog = (): void => { const submitDialog = (): void => {
const firstEmphasizedButton = state.buttons.find((button) => button.props.emphasized && button.callback); const firstEmphasizedButton = state.buttons.find((button) => button.props.emphasized && button.callback);
if (firstEmphasizedButton) { firstEmphasizedButton?.callback?.();
// If statement satisfies TypeScript
if (firstEmphasizedButton.callback) firstEmphasizedButton.callback();
}
}; };
const dialogIsVisible = (): boolean => state.visible; const dialogIsVisible = (): boolean => state.visible;
@@ -52,8 +49,7 @@ export function createDialogState(editor: EditorState) {
callback: async () => window.open(`https://github.com/GraphiteEditor/Graphite/issues/${issueNumber}`, "_blank"), callback: async () => window.open(`https://github.com/GraphiteEditor/Graphite/issues/${issueNumber}`, "_blank"),
props: { label: `Issue #${issueNumber}`, minWidth: 96 }, props: { label: `Issue #${issueNumber}`, minWidth: 96 },
}; };
const buttons = [okButton]; const buttons = issueNumber ? [okButton, issueButton] : [okButton];
if (issueNumber) buttons.push(issueButton);
createDialog("Warning", "Coming soon", details, buttons); createDialog("Warning", "Coming soon", details, buttons);
}; };
@@ -65,7 +61,7 @@ export function createDialogState(editor: EditorState) {
const timezoneName = Intl.DateTimeFormat(undefined, { timeZoneName: "long" }) const timezoneName = Intl.DateTimeFormat(undefined, { timeZoneName: "long" })
.formatToParts(new Date()) .formatToParts(new Date())
.find((part) => part.type === "timeZoneName"); .find((part) => part.type === "timeZoneName");
const timezoneNameString = timezoneName && timezoneName.value; const timezoneNameString = timezoneName?.value;
const hash = (process.env.VUE_APP_COMMIT_HASH || "").substring(0, 12); const hash = (process.env.VUE_APP_COMMIT_HASH || "").substring(0, 12);
+4 -2
View File
@@ -9,18 +9,20 @@ export type RustEditorInstance = InstanceType<WasmInstance["JsEditorHandle"]>;
// `wasmImport` starts uninitialized until `initWasm()` is called in `main.ts` before the Vue app is created // `wasmImport` starts uninitialized until `initWasm()` is called in `main.ts` before the Vue app is created
let wasmImport: WasmInstance | null = null; let wasmImport: WasmInstance | null = null;
export async function initWasm(): Promise<void> { export async function initWasm(): Promise<void> {
// Skip if the wasm module is already initialized
if (wasmImport !== null) return; if (wasmImport !== null) return;
// Separating in two lines satisfies typescript when used below // Separating in two lines satisfies typescript when used below
const importedWasm = await import("@/../wasm/pkg").then(panicProxy); const importedWasm = await import("@/../wasm/pkg").then(panicProxy);
wasmImport = importedWasm; wasmImport = importedWasm;
// Provide a random starter seed which must occur after initializing the wasm module, since wasm can't generate is own random numbers
const randomSeed = BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)); const randomSeed = BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER));
importedWasm.set_random_seed(randomSeed); importedWasm.set_random_seed(randomSeed);
} }
// This works by proxying every function call wrapping a try-catch block to filter out redundant and confusing // This works by proxying every function call and wrapping a try-catch block to filter out redundant and confusing
// `RuntimeError: unreachable` exceptions sent to the console // `RuntimeError: unreachable` exceptions that would normally be printed in the browser's JS console upon a panic.
function panicProxy<T extends object>(module: T): T { function panicProxy<T extends object>(module: T): T {
const proxyHandler = { const proxyHandler = {
get(target: T, propKey: string | symbol, receiver: unknown): unknown { get(target: T, propKey: string | symbol, receiver: unknown): unknown {
+6 -5
View File
@@ -21,12 +21,13 @@ export async function upload(acceptedEextensions: string): Promise<{ filename: s
element.addEventListener( element.addEventListener(
"change", "change",
async () => { async () => {
if (!element.files || !element.files.length) return; if (element.files?.length) {
const file = element.files[0]; const file = element.files[0];
const filename = file.name; const filename = file.name;
const content = await file.text(); const content = await file.text();
resolve({ filename, content }); resolve({ filename, content });
}
}, },
{ capture: false, once: true } { capture: false, once: true }
); );
+10 -11
View File
@@ -23,12 +23,14 @@ fetch(fontListAPI)
.then((response) => response.json()) .then((response) => response.json())
.then((json) => { .then((json) => {
const loadedFonts = json.items as { family: string; variants: string[]; files: { [name: string]: string } }[]; const loadedFonts = json.items as { family: string; variants: string[]; files: { [name: string]: string } }[];
fontList = loadedFonts.map((font) => { fontList = loadedFonts.map((font) => {
const { family } = font; const { family } = font;
const variants = font.variants.map(formatFontStyleName); const variants = font.variants.map(formatFontStyleName);
const files = new Map(font.variants.map((x) => [formatFontStyleName(x), font.files[x]])); const files = new Map(font.variants.map((x) => [formatFontStyleName(x), font.files[x]]));
return { family, variants, files }; return { family, variants, files };
}); });
loadDefaultFont(); loadDefaultFont();
}); });
@@ -48,16 +50,13 @@ function formatFontStyleName(fontStyle: string): string {
return `${weightName}${isItalic ? " Italic" : ""} (${weight})`; return `${weightName}${isItalic ? " Italic" : ""} (${weight})`;
} }
export function loadDefaultFont(): void { export async function loadDefaultFont(): Promise<void> {
const font = getFontFile("Merriweather", "Normal (400)"); const font = getFontFile("Merriweather", "Normal (400)");
if (!font) return;
if (font) { const response = await fetch(font);
fetch(font) const responseBuffer = await response.arrayBuffer();
.then((response) => response.arrayBuffer()) loadDefaultFontCallback?.(font, new Uint8Array(responseBuffer));
.then((response) => {
if (loadDefaultFontCallback) loadDefaultFontCallback(font, new Uint8Array(response));
});
}
} }
export function setLoadDefaultFontCallback(callback: fontCallbackType): void { export function setLoadDefaultFontCallback(callback: fontCallbackType): void {
@@ -71,11 +70,11 @@ export function fontNames(): string[] {
export function getFontStyles(fontFamily: string): string[] { export function getFontStyles(fontFamily: string): string[] {
const font = fontList.find((value) => value.family === fontFamily); const font = fontList.find((value) => value.family === fontFamily);
return font ? font.variants : []; return font?.variants || [];
} }
export function getFontFile(fontFamily: string, fontStyle: string): string | undefined { export function getFontFile(fontFamily: string, fontStyle: string): string | undefined {
const font = fontList.find((value) => value.family === fontFamily); const font = fontList.find((value) => value.family === fontFamily);
const fontFile = font && font.files.get(fontStyle); const fontFile = font?.files.get(fontStyle);
return fontFile && fontFile.replace("http://", "https://"); return fontFile?.replace("http://", "https://");
} }
+110 -101
View File
@@ -100,109 +100,118 @@ import VectorShapeTool from "@/../assets/24px-two-tone/vector-shape-tool.svg";
import VectorSplineTool from "@/../assets/24px-two-tone/vector-spline-tool.svg"; import VectorSplineTool from "@/../assets/24px-two-tone/vector-spline-tool.svg";
import VectorTextTool from "@/../assets/24px-two-tone/vector-text-tool.svg"; import VectorTextTool from "@/../assets/24px-two-tone/vector-text-tool.svg";
export type IconName = keyof typeof ICON_LIST; const ICON_LIST = {
export type IconSize = 12 | 16 | 24; Checkmark: { component: Checkmark, size: 12 },
CloseX: { component: CloseX, size: 12 },
DropdownArrow: { component: DropdownArrow, size: 12 },
FullscreenEnter: { component: FullscreenEnter, size: 12 },
FullscreenExit: { component: FullscreenExit, size: 12 },
Grid: { component: Grid, size: 12 },
Info: { component: Info, size: 12 },
KeyboardArrowDown: { component: KeyboardArrowDown, size: 12 },
KeyboardArrowLeft: { component: KeyboardArrowLeft, size: 12 },
KeyboardArrowRight: { component: KeyboardArrowRight, size: 12 },
KeyboardArrowUp: { component: KeyboardArrowUp, size: 12 },
KeyboardBackspace: { component: KeyboardBackspace, size: 12 },
KeyboardCommand: { component: KeyboardCommand, size: 12 },
KeyboardEnter: { component: KeyboardEnter, size: 12 },
KeyboardOption: { component: KeyboardOption, size: 12 },
KeyboardShift: { component: KeyboardShift, size: 12 },
KeyboardSpace: { component: KeyboardSpace, size: 12 },
KeyboardTab: { component: KeyboardTab, size: 12 },
Link: { component: Link, size: 12 },
Overlays: { component: Overlays, size: 12 },
ResetColors: { component: ResetColors, size: 12 },
Snapping: { component: Snapping, size: 12 },
Swap: { component: Swap, size: 12 },
VerticalEllipsis: { component: VerticalEllipsis, size: 12 },
Warning: { component: Warning, size: 12 },
WindowButtonWinClose: { component: WindowButtonWinClose, size: 12 },
WindowButtonWinMaximize: { component: WindowButtonWinMaximize, size: 12 },
WindowButtonWinMinimize: { component: WindowButtonWinMinimize, size: 12 },
WindowButtonWinRestoreDown: { component: WindowButtonWinRestoreDown, size: 12 },
const size12: IconSize = 12; AlignBottom: { component: AlignBottom, size: 16 },
const size16: IconSize = 16; AlignHorizontalCenter: { component: AlignHorizontalCenter, size: 16 },
const size24: IconSize = 24; AlignLeft: { component: AlignLeft, size: 16 },
AlignRight: { component: AlignRight, size: 16 },
AlignTop: { component: AlignTop, size: 16 },
AlignVerticalCenter: { component: AlignVerticalCenter, size: 16 },
BooleanDifference: { component: BooleanDifference, size: 16 },
BooleanIntersect: { component: BooleanIntersect, size: 16 },
BooleanSubtractBack: { component: BooleanSubtractBack, size: 16 },
BooleanSubtractFront: { component: BooleanSubtractFront, size: 16 },
BooleanUnion: { component: BooleanUnion, size: 16 },
Copy: { component: Copy, size: 16 },
EyeHidden: { component: EyeHidden, size: 16 },
EyeVisible: { component: EyeVisible, size: 16 },
File: { component: File, size: 16 },
FlipHorizontal: { component: FlipHorizontal, size: 16 },
FlipVertical: { component: FlipVertical, size: 16 },
GraphiteLogo: { component: GraphiteLogo, size: 16 },
NodeArtboard: { component: NodeArtboard, size: 16 },
NodeFolder: { component: NodeFolder, size: 16 },
NodeImage: { component: NodeImage, size: 16 },
NodeShape: { component: NodeShape, size: 16 },
NodeText: { component: NodeText, size: 16 },
Paste: { component: Paste, size: 16 },
Trash: { component: Trash, size: 16 },
ViewModeNormal: { component: ViewModeNormal, size: 16 },
ViewModeOutline: { component: ViewModeOutline, size: 16 },
ViewModePixels: { component: ViewModePixels, size: 16 },
ViewportDesignMode: { component: ViewportDesignMode, size: 16 },
ViewportGuideMode: { component: ViewportGuideMode, size: 16 },
ViewportSelectMode: { component: ViewportSelectMode, size: 16 },
ZoomIn: { component: ZoomIn, size: 16 },
ZoomOut: { component: ZoomOut, size: 16 },
ZoomReset: { component: ZoomReset, size: 16 },
export const ICON_LIST = { MouseHintDrag: { component: MouseHintDrag, size: 16 },
Checkmark: { component: Checkmark, size: size12 }, MouseHintLmbDrag: { component: MouseHintLmbDrag, size: 16 },
CloseX: { component: CloseX, size: size12 }, MouseHintLmb: { component: MouseHintLmb, size: 16 },
DropdownArrow: { component: DropdownArrow, size: size12 }, MouseHintMmbDrag: { component: MouseHintMmbDrag, size: 16 },
FullscreenEnter: { component: FullscreenEnter, size: size12 }, MouseHintMmb: { component: MouseHintMmb, size: 16 },
FullscreenExit: { component: FullscreenExit, size: size12 }, MouseHintNone: { component: MouseHintNone, size: 16 },
Grid: { component: Grid, size: size12 }, MouseHintRmbDrag: { component: MouseHintRmbDrag, size: 16 },
Info: { component: Info, size: size12 }, MouseHintRmb: { component: MouseHintRmb, size: 16 },
KeyboardArrowDown: { component: KeyboardArrowDown, size: size12 }, MouseHintScrollDown: { component: MouseHintScrollDown, size: 16 },
KeyboardArrowLeft: { component: KeyboardArrowLeft, size: size12 }, MouseHintScrollUp: { component: MouseHintScrollUp, size: 16 },
KeyboardArrowRight: { component: KeyboardArrowRight, size: size12 },
KeyboardArrowUp: { component: KeyboardArrowUp, size: size12 },
KeyboardBackspace: { component: KeyboardBackspace, size: size12 },
KeyboardCommand: { component: KeyboardCommand, size: size12 },
KeyboardEnter: { component: KeyboardEnter, size: size12 },
KeyboardOption: { component: KeyboardOption, size: size12 },
KeyboardShift: { component: KeyboardShift, size: size12 },
KeyboardSpace: { component: KeyboardSpace, size: size12 },
KeyboardTab: { component: KeyboardTab, size: size12 },
Link: { component: Link, size: size12 },
Overlays: { component: Overlays, size: size12 },
ResetColors: { component: ResetColors, size: size12 },
Snapping: { component: Snapping, size: size12 },
Swap: { component: Swap, size: size12 },
VerticalEllipsis: { component: VerticalEllipsis, size: size12 },
Warning: { component: Warning, size: size12 },
WindowButtonWinClose: { component: WindowButtonWinClose, size: size12 },
WindowButtonWinMaximize: { component: WindowButtonWinMaximize, size: size12 },
WindowButtonWinMinimize: { component: WindowButtonWinMinimize, size: size12 },
WindowButtonWinRestoreDown: { component: WindowButtonWinRestoreDown, size: size12 },
AlignBottom: { component: AlignBottom, size: size16 }, GeneralArtboardTool: { component: GeneralArtboardTool, size: 24 },
AlignHorizontalCenter: { component: AlignHorizontalCenter, size: size16 }, GeneralEyedropperTool: { component: GeneralEyedropperTool, size: 24 },
AlignLeft: { component: AlignLeft, size: size16 }, GeneralNavigateTool: { component: GeneralNavigateTool, size: 24 },
AlignRight: { component: AlignRight, size: size16 }, GeneralSelectTool: { component: GeneralSelectTool, size: 24 },
AlignTop: { component: AlignTop, size: size16 }, GeneralFillTool: { component: GeneralFillTool, size: 24 },
AlignVerticalCenter: { component: AlignVerticalCenter, size: size16 }, GeneralGradientTool: { component: GeneralGradientTool, size: 24 },
BooleanDifference: { component: BooleanDifference, size: size16 }, RasterBrushTool: { component: RasterBrushTool, size: 24 },
BooleanIntersect: { component: BooleanIntersect, size: size16 }, RasterCloneTool: { component: RasterCloneTool, size: 24 },
BooleanSubtractBack: { component: BooleanSubtractBack, size: size16 }, RasterDetailTool: { component: RasterDetailTool, size: 24 },
BooleanSubtractFront: { component: BooleanSubtractFront, size: size16 }, RasterHealTool: { component: RasterHealTool, size: 24 },
BooleanUnion: { component: BooleanUnion, size: size16 }, RasterPatchTool: { component: RasterPatchTool, size: 24 },
Copy: { component: Copy, size: size16 }, RasterRelightTool: { component: RasterRelightTool, size: 24 },
EyeHidden: { component: EyeHidden, size: size16 }, VectorEllipseTool: { component: VectorEllipseTool, size: 24 },
EyeVisible: { component: EyeVisible, size: size16 }, VectorFreehandTool: { component: VectorFreehandTool, size: 24 },
File: { component: File, size: size16 }, VectorLineTool: { component: VectorLineTool, size: 24 },
FlipHorizontal: { component: FlipHorizontal, size: size16 }, VectorPathTool: { component: VectorPathTool, size: 24 },
FlipVertical: { component: FlipVertical, size: size16 }, VectorPenTool: { component: VectorPenTool, size: 24 },
GraphiteLogo: { component: GraphiteLogo, size: size16 }, VectorRectangleTool: { component: VectorRectangleTool, size: 24 },
NodeArtboard: { component: NodeArtboard, size: size16 }, VectorShapeTool: { component: VectorShapeTool, size: 24 },
NodeFolder: { component: NodeFolder, size: size16 }, VectorSplineTool: { component: VectorSplineTool, size: 24 },
NodeImage: { component: NodeImage, size: size16 }, VectorTextTool: { component: VectorTextTool, size: 24 },
NodeShape: { component: NodeShape, size: size16 }, } as const;
NodeText: { component: NodeText, size: size16 }, export const icons: IconDefinitionType<typeof ICON_LIST> = ICON_LIST;
Paste: { component: Paste, size: size16 },
Trash: { component: Trash, size: size16 },
ViewModeNormal: { component: ViewModeNormal, size: size16 },
ViewModeOutline: { component: ViewModeOutline, size: size16 },
ViewModePixels: { component: ViewModePixels, size: size16 },
ViewportDesignMode: { component: ViewportDesignMode, size: size16 },
ViewportGuideMode: { component: ViewportGuideMode, size: size16 },
ViewportSelectMode: { component: ViewportSelectMode, size: size16 },
ZoomIn: { component: ZoomIn, size: size16 },
ZoomOut: { component: ZoomOut, size: size16 },
ZoomReset: { component: ZoomReset, size: size16 },
MouseHintDrag: { component: MouseHintDrag, size: size16 }, export type IconName = keyof typeof icons;
MouseHintLmbDrag: { component: MouseHintLmbDrag, size: size16 }, export type IconSize = 12 | 16 | 24 | 32;
MouseHintLmb: { component: MouseHintLmb, size: size16 },
MouseHintMmbDrag: { component: MouseHintMmbDrag, size: size16 },
MouseHintMmb: { component: MouseHintMmb, size: size16 },
MouseHintNone: { component: MouseHintNone, size: size16 },
MouseHintRmbDrag: { component: MouseHintRmbDrag, size: size16 },
MouseHintRmb: { component: MouseHintRmb, size: size16 },
MouseHintScrollDown: { component: MouseHintScrollDown, size: size16 },
MouseHintScrollUp: { component: MouseHintScrollUp, size: size16 },
GeneralArtboardTool: { component: GeneralArtboardTool, size: size24 }, // The following helper type declarations allow us to avoid manually maintaining the `IconName` type declaration as a string union paralleling the keys of the
GeneralEyedropperTool: { component: GeneralEyedropperTool, size: size24 }, // icon definitions. It lets TypeScript do that for us. Our goal is to define the big icons key-value pair by constraining its values, but inferring its keys.
GeneralNavigateTool: { component: GeneralNavigateTool, size: size24 }, // Constraining its values means that TypeScript can make sure each icon definition has a valid size number from the union of numbers that is `IconSize`.
GeneralSelectTool: { component: GeneralSelectTool, size: size24 }, // Inferring its keys means we don't have to specify a supertype like `string` or `any` for the key-value pair's keys, which would prevent us from accessing
GeneralFillTool: { component: GeneralFillTool, size: size24 }, // the individual keys with `keyof typeof`. Absent a specified type for the keys, TypeScript falls back to inferring that the key-value pair's type is the
GeneralGradientTool: { component: GeneralGradientTool, size: size24 }, // map of all its individual entries. Having the full list of entries lets us automatically set the `IconName` type to the union of strings that is the full
RasterBrushTool: { component: RasterBrushTool, size: size24 }, // list of keys. The result is that we don't have to maintain a separate list of icon names since this scheme infers it from the keys of the icon definitions.
RasterCloneTool: { component: RasterCloneTool, size: size24 }, // Based on https://stackoverflow.com/a/64119715/775283
RasterDetailTool: { component: RasterDetailTool, size: size24 }, type IconDefinition = { component: string; size: IconSize };
RasterHealTool: { component: RasterHealTool, size: size24 }, type EvaluateType<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
RasterPatchTool: { component: RasterPatchTool, size: size24 }, type IconDefinitionType<T extends Record<string, IconDefinition>> = EvaluateType<{ [key in keyof T]: IconDefinition }>;
RasterRelightTool: { component: RasterRelightTool, size: size24 },
VectorEllipseTool: { component: VectorEllipseTool, size: size24 },
VectorFreehandTool: { component: VectorFreehandTool, size: size24 },
VectorLineTool: { component: VectorLineTool, size: size24 },
VectorPathTool: { component: VectorPathTool, size: size24 },
VectorPenTool: { component: VectorPenTool, size: size24 },
VectorRectangleTool: { component: VectorRectangleTool, size: size24 },
VectorShapeTool: { component: VectorShapeTool, size: size24 },
VectorSplineTool: { component: VectorSplineTool, size: size24 },
VectorTextTool: { component: VectorTextTool, size: size24 },
};