Clean up MenuList types and fix many Vue and Clippy warnings

Also remove hard-coded-in-Vue Graphite logo in the menu bar in favor of a Rust definition.
This commit is contained in:
Keavon Chambers 2022-08-25 14:41:16 -07:00
parent 1a90a4db86
commit 3a84de32ac
27 changed files with 361 additions and 374 deletions

View file

@ -1,20 +0,0 @@
<template>
<div style="display: flex">
<div tabindex="0" style="width: 50%; outline: none">
<App />
</div>
<div tabindex="0" style="width: 50%; outline: none">
<App />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import App from "@/App.vue";
export default defineComponent({
components: { App },
});
</script>

View file

@ -44,8 +44,8 @@
:open="entry.ref?.open || false"
:direction="'TopRight'"
:entries="entry.children"
v-bind="{ defaultAction, minWidth, drawIcon, scrollableY }"
:ref="(ref: typeof FloatingMenu) => ref && (entry.ref = ref)"
v-bind="{ minWidth, drawIcon, scrollableY }"
:ref="(ref: MenuListInstance) => ref && (entry.ref = ref)"
/>
</LayoutRow>
</template>
@ -160,7 +160,7 @@
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { MenuListEntry, SectionsOfMenuListEntries, MenuListEntryData } from "@/wasm-communication/messages";
import type { MenuListEntry } from "@/wasm-communication/messages";
import FloatingMenu, { MenuDirection } from "@/components/floating-menus/FloatingMenu.vue";
import LayoutCol from "@/components/layout/LayoutCol.vue";
@ -169,10 +169,13 @@ import IconLabel from "@/components/widgets/labels/IconLabel.vue";
import Separator from "@/components/widgets/labels/Separator.vue";
import UserInputLabel from "@/components/widgets/labels/UserInputLabel.vue";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type MenuListInstance = InstanceType<typeof MenuList>;
const MenuList = defineComponent({
emits: ["update:open", "update:activeEntry", "naturalWidth"],
props: {
entries: { type: Array as PropType<SectionsOfMenuListEntries>, required: true },
entries: { type: Array as PropType<MenuListEntry[][]>, required: true },
activeEntry: { type: Object as PropType<MenuListEntry>, required: false },
open: { type: Boolean as PropType<boolean>, required: true },
direction: { type: String as PropType<MenuDirection>, default: "Bottom" },
@ -181,7 +184,6 @@ const MenuList = defineComponent({
interactive: { type: Boolean as PropType<boolean>, default: false },
scrollableY: { type: Boolean as PropType<boolean>, default: false },
virtualScrollingEntryHeight: { type: Number as PropType<number>, default: 0 },
defaultAction: { type: Function as PropType<() => void>, required: false },
},
data() {
return {
@ -209,33 +211,32 @@ const MenuList = defineComponent({
},
},
methods: {
onEntryClick(menuEntry: MenuListEntry): void {
// Call the action, or a default, if either are provided
if (menuEntry.action) menuEntry.action();
else if (this.defaultAction) this.defaultAction();
onEntryClick(menuListEntry: MenuListEntry): void {
// Call the action if available
if (menuListEntry.action) menuListEntry.action();
// Emit the clicked entry as the new active entry
this.$emit("update:activeEntry", menuEntry);
this.$emit("update:activeEntry", menuListEntry);
// Close the containing menu
if (menuEntry.ref) menuEntry.ref.isOpen = false;
if (menuListEntry.ref) menuListEntry.ref.isOpen = false;
this.$emit("update:open", false);
this.isOpen = false; // TODO: This is a hack for MenuBarInput submenus, remove it when we get rid of using `ref`
},
onEntryPointerEnter(menuEntry: MenuListEntry): void {
if (!menuEntry.children?.length) return;
onEntryPointerEnter(menuListEntry: MenuListEntry): void {
if (!menuListEntry.children?.length) return;
if (menuEntry.ref) menuEntry.ref.isOpen = true;
if (menuListEntry.ref) menuListEntry.ref.isOpen = true;
else this.$emit("update:open", true);
},
onEntryPointerLeave(menuEntry: MenuListEntry): void {
if (!menuEntry.children?.length) return;
onEntryPointerLeave(menuListEntry: MenuListEntry): void {
if (!menuListEntry.children?.length) return;
if (menuEntry.ref) menuEntry.ref.isOpen = false;
if (menuListEntry.ref) menuListEntry.ref.isOpen = false;
else this.$emit("update:open", false);
},
isEntryOpen(menuEntry: MenuListEntry): boolean {
if (!menuEntry.children?.length) return false;
isEntryOpen(menuListEntry: MenuListEntry): boolean {
if (!menuListEntry.children?.length) return false;
return this.open;
},
@ -249,7 +250,7 @@ const MenuList = defineComponent({
const flatEntries = this.entries.flat().filter((entry) => !entry.disabled);
const openChild = flatEntries.findIndex((entry) => entry.children?.length && entry.ref?.isOpen);
const openSubmenu = (highlighted: MenuListEntry<string>): void => {
const openSubmenu = (highlighted: MenuListEntry): void => {
if (highlighted.ref && highlighted.children?.length) {
highlighted.ref.isOpen = true;
@ -316,7 +317,7 @@ const MenuList = defineComponent({
// By default, keep the menu stack open
return false;
},
setHighlighted(newHighlight: MenuListEntry<string> | undefined) {
setHighlighted(newHighlight: MenuListEntry | undefined) {
this.highlighted = newHighlight;
// Interactive menus should keep the active entry the same as the highlighted one
if (this.interactive && newHighlight?.value !== this.activeEntry?.value) this.$emit("update:activeEntry", newHighlight);
@ -327,14 +328,6 @@ const MenuList = defineComponent({
},
},
computed: {
entriesWithoutRefs(): MenuListEntryData[][] {
return this.entries.map((menuListEntries) =>
menuListEntries.map((entry) => {
const { ref, ...entryWithoutRef } = entry;
return entryWithoutRef;
})
);
},
virtualScrollingTotalHeight() {
return this.entries[0].length * this.virtualScrollingEntryHeight;
},

View file

@ -4,7 +4,7 @@
<WidgetLayout :layout="layerTreeOptionsLayout" />
</LayoutRow>
<LayoutRow class="layer-tree-rows" :scrollableY="true">
<LayoutCol class="list" ref="layerTreeList" @click="() => deselectAllLayers()" @dragover="(e) => draggable && updateInsertLine(e)" @dragend="() => draggable && drop()">
<LayoutCol class="list" ref="layerTreeList" @click="() => deselectAllLayers()" @dragover="(e: DragEvent) => draggable && updateInsertLine(e)" @dragend="() => draggable && drop()">
<LayoutRow
class="layer-row"
v-for="(listing, index) in layers"
@ -13,7 +13,7 @@
>
<LayoutRow class="visibility">
<IconButton
:action="(e) => (toggleLayerVisibility(listing.entry.path), e?.stopPropagation())"
:action="(e: MouseEvent) => (toggleLayerVisibility(listing.entry.path), e?.stopPropagation())"
:size="24"
:icon="listing.entry.visible ? 'EyeVisible' : 'EyeHidden'"
:title="listing.entry.visible ? 'Visible' : 'Hidden'"
@ -34,15 +34,15 @@
:data-index="index"
:title="listing.entry.tooltip"
:draggable="draggable"
@dragstart="(e) => draggable && dragStart(e, listing)"
@click.exact="(e) => selectLayer(false, false, false, listing, e)"
@click.shift.exact="(e) => selectLayer(false, false, true, listing, e)"
@click.ctrl.exact="(e) => selectLayer(true, false, false, listing, e)"
@click.ctrl.shift.exact="(e) => selectLayer(true, false, true, listing, e)"
@click.meta.exact="(e) => selectLayer(false, true, false, listing, e)"
@click.meta.shift.exact="(e) => selectLayer(false, true, true, listing, e)"
@click.ctrl.meta="(e) => e.stopPropagation()"
@click.alt="(e) => e.stopPropagation()"
@dragstart="(e: DragEvent) => draggable && dragStart(e, listing)"
@click.exact="(e: MouseEvent) => selectLayer(false, false, false, listing, e)"
@click.shift.exact="(e: MouseEvent) => selectLayer(false, false, true, listing, e)"
@click.ctrl.exact="(e: MouseEvent) => selectLayer(true, false, false, listing, e)"
@click.ctrl.shift.exact="(e: MouseEvent) => selectLayer(true, false, true, listing, e)"
@click.meta.exact="(e: MouseEvent) => selectLayer(false, true, false, listing, e)"
@click.meta.shift.exact="(e: MouseEvent) => selectLayer(false, true, true, listing, e)"
@click.ctrl.meta="(e: MouseEvent) => e.stopPropagation()"
@click.alt="(e: MouseEvent) => e.stopPropagation()"
>
<LayoutRow class="layer-type-icon">
<IconLabel v-if="listing.entry.layerType === 'Folder'" :icon="'NodeFolder'" :iconStyle="'Node'" title="Folder" />

View file

@ -3,11 +3,11 @@
<LayoutRow class="options-bar"></LayoutRow>
<LayoutRow
class="graph"
@wheel="(e) => scroll(e)"
@wheel="(e: WheelEvent) => scroll(e)"
ref="graph"
@pointerdown="(e) => pointerDown(e)"
@pointermove="(e) => pointerMove(e)"
@pointerup="(e) => pointerUp(e)"
@pointerdown="(e: PointerEvent) => pointerDown(e)"
@pointermove="(e: PointerEvent) => pointerMove(e)"
@pointerup="(e: PointerEvent) => pointerUp(e)"
:style="`--grid-spacing: ${gridSpacing}px; --grid-offset-x: ${transform.x * transform.scale}px; --grid-offset-y: ${transform.y * transform.scale}px; --dot-radius: ${dotRadius}px`"
>
<div

View file

@ -12,12 +12,7 @@
v-model:open="open"
@update:selectedIndex="(value: number) => updateLayout(component.widgetId, value)"
/>
<FontInput
v-if="component.props.kind === 'FontInput'"
v-bind="component.props"
v-model:open="open"
@changeFont="(value: { name: string, style: string, file: string }) => updateLayout(component.widgetId, value)"
/>
<FontInput v-if="component.props.kind === 'FontInput'" v-bind="component.props" v-model:open="open" @changeFont="(value: unknown) => updateLayout(component.widgetId, value)" />
<IconButton v-if="component.props.kind === 'IconButton'" v-bind="component.props" :action="() => updateLayout(component.widgetId, null)" />
<IconLabel v-if="component.props.kind === 'IconLabel'" v-bind="component.props" />
<NumberInput
@ -29,8 +24,8 @@
/>
<OptionalInput v-if="component.props.kind === 'OptionalInput'" v-bind="component.props" @update:checked="(value: boolean) => updateLayout(component.widgetId, value)" />
<PopoverButton v-if="component.props.kind === 'PopoverButton'" v-bind="component.props">
<h3>{{ component.props.header }}</h3>
<p>{{ component.props.text }}</p>
<h3>{{ (component.props as any).header }}</h3>
<p>{{ (component.props as any).text }}</p>
</PopoverButton>
<RadioInput v-if="component.props.kind === 'RadioInput'" v-bind="component.props" @update:selectedIndex="(value: number) => updateLayout(component.widgetId, value)" />
<Separator v-if="component.props.kind === 'Separator'" v-bind="component.props" />
@ -38,7 +33,7 @@
<TextAreaInput v-if="component.props.kind === 'TextAreaInput'" v-bind="component.props" @commitText="(value: string) => updateLayout(component.widgetId, value)" />
<TextButton v-if="component.props.kind === 'TextButton'" v-bind="component.props" :action="() => updateLayout(component.widgetId, null)" />
<TextInput v-if="component.props.kind === 'TextInput'" v-bind="component.props" @commitText="(value: string) => updateLayout(component.widgetId, value)" />
<TextLabel v-if="component.props.kind === 'TextLabel'" v-bind="withoutValue(component.props)">{{ component.props.value }}</TextLabel>
<TextLabel v-if="component.props.kind === 'TextLabel'" v-bind="withoutValue(component.props)">{{ (component.props as any).value }}</TextLabel>
</template>
</div>
</template>
@ -124,7 +119,8 @@ export default defineComponent({
updateLayout(widgetId: bigint, value: unknown) {
this.editor.instance.updateLayout(this.layoutTarget, widgetId, value);
},
withoutValue(props: Record<string, unknown>): Record<string, unknown> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
withoutValue(props: Record<string, any>): Record<string, unknown> {
const { value: _, ...rest } = props;
return rest;
},

View file

@ -1,12 +1,12 @@
<template>
<LayoutRow class="color-input" :title="tooltip">
<OptionalInput v-if="!noTransparency" :icon="'CloseX'" :checked="Boolean(value)" @update:checked="(val) => updateEnabled(val)"></OptionalInput>
<OptionalInput v-if="!noTransparency" :icon="'CloseX'" :checked="Boolean(value)" @update:checked="(state: boolean) => updateEnabled(state)"></OptionalInput>
<TextInput :value="displayValue" :label="label" :disabled="disabled || !value" @commitText="(value: string) => textInputUpdated(value)" :center="true" />
<Separator :type="'Related'" />
<LayoutRow class="swatch">
<button class="swatch-button" :class="{ 'disabled-swatch': !value }" :style="`--swatch-color: #${value}`" @click="() => $emit('update:open', true)"></button>
<FloatingMenu v-model:open="isOpen" :type="'Popover'" :direction="'Bottom'">
<ColorPicker @update:color="(color) => colorPickerUpdated(color)" :color="color" />
<ColorPicker @update:color="(color: RGBA) => colorPickerUpdated(color)" :color="color" />
</FloatingMenu>
</LayoutRow>
</LayoutRow>

View file

@ -6,7 +6,7 @@
:style="{ minWidth: `${minWidth}px` }"
@click="() => !disabled && (open = true)"
@blur="(e: FocusEvent) => blur(e)"
@keydown="(e) => keydown(e)"
@keydown="(e: KeyboardEvent) => keydown(e)"
ref="dropdownBox"
tabindex="0"
data-hover-menu-spawner
@ -99,7 +99,7 @@
<script lang="ts">
import { defineComponent, PropType, toRaw } from "vue";
import { MenuListEntry, SectionsOfMenuListEntries } from "@/wasm-communication/messages";
import { MenuListEntry } from "@/wasm-communication/messages";
import MenuList from "@/components/floating-menus/MenuList.vue";
import LayoutRow from "@/components/layout/LayoutRow.vue";
@ -110,7 +110,7 @@ const DASH_ENTRY = { label: "-" };
export default defineComponent({
emits: ["update:selectedIndex"],
props: {
entries: { type: Array as PropType<SectionsOfMenuListEntries>, required: true },
entries: { type: Array as PropType<MenuListEntry[][]>, required: true },
selectedIndex: { type: Number as PropType<number>, required: false }, // When not provided, a dash is displayed
drawIcon: { type: Boolean as PropType<boolean>, default: false },
interactive: { type: Boolean as PropType<boolean>, default: true },

View file

@ -1,31 +1,26 @@
<template>
<div class="menu-bar-input" data-menu-bar-input>
<div class="entry-container">
<button @click="() => visitWebsite('https://graphite.rs')" class="entry">
<IconLabel :icon="'GraphiteLogo'" />
</button>
</div>
<div class="entry-container" v-for="(entry, index) in entries" :key="index">
<div
@click="(e) => onClick(entry, e.target)"
tabindex="0"
@blur="(e: FocusEvent) => blur(e,entry)"
@keydown="entry.ref?.keydown"
@click="(e: MouseEvent) => onClick(entry, e.target)"
@blur="(e: FocusEvent) => blur(entry, e.target)"
@keydown="(e: KeyboardEvent) => entry.ref?.keydown(e, false)"
class="entry"
:class="{ open: entry.ref?.isOpen }"
tabindex="0"
data-hover-menu-spawner
>
<IconLabel v-if="entry.icon" :icon="entry.icon" />
<span v-if="entry.label">{{ entry.label }}</span>
</div>
<MenuList
v-if="entry.children && entry.children.length > 0"
:open="entry.ref?.open || false"
:entries="entry.children || []"
:direction="'Bottom'"
:minWidth="240"
:drawIcon="true"
:defaultAction="() => editor.instance.requestComingSoonDialog()"
:ref="(ref: typeof MenuList) => ref && (entry.ref = ref)"
:ref="(ref: MenuListInstance) => ref && (entry.ref = ref)"
/>
</div>
</div>
@ -73,11 +68,14 @@
import { defineComponent } from "vue";
import { platformIsMac } from "@/utility-functions/platform";
import { MenuEntry, UpdateMenuBarLayout, MenuListEntry, KeyRaw, KeysGroup } from "@/wasm-communication/messages";
import { MenuBarEntry, UpdateMenuBarLayout, MenuListEntry, KeyRaw, KeysGroup } from "@/wasm-communication/messages";
import MenuList from "@/components/floating-menus/MenuList.vue";
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type MenuListInstance = InstanceType<typeof MenuList>;
// TODO: Apparently, Safari does not support the Keyboard.lock() API but does relax its authority over certain keyboard shortcuts in fullscreen mode, which we should take advantage of
const accelKey = platformIsMac() ? "Command" : "Control";
const LOCK_REQUIRING_SHORTCUTS: KeyRaw[][] = [
@ -88,12 +86,6 @@ const LOCK_REQUIRING_SHORTCUTS: KeyRaw[][] = [
[accelKey, "Shift", "KeyT"],
];
type FrontendMenuColumn = {
label: string;
children: FrontendMenuEntry[][];
};
type FrontendMenuEntry = Omit<MenuEntry, "action" | "children"> & { shortcutRequiresLock: boolean | undefined; action: () => void; children: FrontendMenuEntry[][] | undefined };
export default defineComponent({
inject: ["editor"],
mounted() {
@ -106,39 +98,47 @@ export default defineComponent({
return LOCK_REQUIRING_SHORTCUTS.some((lockKeyCombo) => arraysEqual(shortcutKeys, lockKeyCombo));
};
const menuEntryToFrontendMenuEntry = (subLayout: MenuEntry[][]): FrontendMenuEntry[][] =>
subLayout.map((group) =>
group.map((entry) => ({
...entry,
children: entry.children ? menuEntryToFrontendMenuEntry(entry.children) : undefined,
action: (): void => this.editor.instance.updateLayout(updateMenuBarLayout.layoutTarget, entry.action.widgetId, undefined),
shortcutRequiresLock: entry.shortcut ? shortcutRequiresLock(entry.shortcut.keys) : undefined,
}))
);
const menuBarEntryToMenuListEntry = (entry: MenuBarEntry): MenuListEntry => ({
// From `MenuEntryCommon`
...entry,
this.entries = updateMenuBarLayout.layout.map((column) => ({ ...column, children: menuEntryToFrontendMenuEntry(column.children) }));
// Shared names with fields that need to be converted from the type used in `MenuBarEntry` to that of `MenuListEntry`
action: (): void => this.editor.instance.updateLayout(updateMenuBarLayout.layoutTarget, entry.action.widgetId, undefined),
children: entry.children ? entry.children.map((entries) => entries.map((entry) => menuBarEntryToMenuListEntry(entry))) : undefined,
// New fields in `MenuListEntry`
shortcutRequiresLock: entry.shortcut ? shortcutRequiresLock(entry.shortcut.keys) : undefined,
value: undefined,
disabled: undefined,
font: undefined,
ref: undefined,
});
this.entries = updateMenuBarLayout.layout.map(menuBarEntryToMenuListEntry);
});
},
methods: {
onClick(menuEntry: MenuListEntry, target: EventTarget | null) {
onClick(menuListEntry: MenuListEntry, target: EventTarget | null) {
// If there's no menu to open, trigger the action but don't try to open its non-existant children
if (!menuListEntry.children || menuListEntry.children.length === 0) {
if (menuListEntry.action && !menuListEntry.disabled) menuListEntry.action();
return;
}
// Focus the target so that keyboard inputs are sent to the dropdown
(target as HTMLElement)?.focus();
if (menuEntry.ref) menuEntry.ref.isOpen = true;
if (menuListEntry.ref) menuListEntry.ref.isOpen = true;
else throw new Error("The menu bar floating menu has no associated ref");
},
blur(e: FocusEvent, menuEntry: MenuListEntry) {
if ((e.target as HTMLElement).closest("[data-menu-bar-input]") !== this.$el && menuEntry.ref) menuEntry.ref.isOpen = false;
},
// TODO: Move to backend
visitWebsite(url: string) {
// This method is required because `window` isn't accessible from the Vue component HTML
window.open(url, "_blank");
blur(menuListEntry: MenuListEntry, target: EventTarget | null) {
if ((target as HTMLElement)?.closest("[data-menu-bar-input]") !== this.$el && menuListEntry.ref) menuListEntry.ref.isOpen = false;
},
},
data() {
return {
entries: [] as FrontendMenuColumn[],
entries: [] as MenuListEntry[],
open: false,
};
},

View file

@ -1,6 +1,6 @@
<template>
<LayoutRow class="optional-input">
<CheckboxInput :checked="checked" @input="(e) => $emit('update:checked', (e.target as HTMLInputElement).checked)" :icon="icon" :tooltip="tooltip" />
<CheckboxInput :checked="checked" @input="(e: Event) => $emit('update:checked', (e.target as HTMLInputElement).checked)" :icon="icon" :tooltip="tooltip" />
</LayoutRow>
</template>

View file

@ -77,11 +77,11 @@ export default defineComponent({
selectedIndex: { type: Number as PropType<number>, required: true },
},
methods: {
handleEntryClick(menuEntry: RadioEntryData) {
const index = this.entries.indexOf(menuEntry);
handleEntryClick(radioEntryData: RadioEntryData) {
const index = this.entries.indexOf(radioEntryData);
this.$emit("update:selectedIndex", index);
menuEntry.action?.();
radioEntryData.action?.();
},
},
components: {

View file

@ -8,11 +8,11 @@
data-tab
v-for="(tabLabel, tabIndex) in tabLabels"
:key="tabIndex"
@click="(e) => (e?.stopPropagation(), clickAction?.(tabIndex))"
@click.middle="(e) => (e?.stopPropagation(), closeAction?.(tabIndex))"
@click="(e: MouseEvent) => (e?.stopPropagation(), clickAction?.(tabIndex))"
@click.middle="(e: MouseEvent) => (e?.stopPropagation(), closeAction?.(tabIndex))"
>
<span>{{ tabLabel }}</span>
<IconButton :action="(e) => (e?.stopPropagation(), closeAction?.(tabIndex))" :icon="'CloseX'" :size="16" v-if="tabCloseButtons" />
<IconButton :action="(e: MouseEvent) => (e?.stopPropagation(), closeAction?.(tabIndex))" :icon="'CloseX'" :size="16" v-if="tabCloseButtons" />
</LayoutRow>
</LayoutRow>
<PopoverButton :icon="'VerticalEllipsis'">

View file

@ -8,23 +8,23 @@
:tabCloseButtons="true"
:tabMinWidths="true"
:tabLabels="portfolio.state.documents.map((doc) => doc.displayName)"
:clickAction="(tabIndex) => editor.instance.selectDocument(portfolio.state.documents[tabIndex].id)"
:closeAction="(tabIndex) => editor.instance.closeDocumentWithConfirmation(portfolio.state.documents[tabIndex].id)"
:clickAction="(tabIndex: number) => editor.instance.selectDocument(portfolio.state.documents[tabIndex].id)"
:closeAction="(tabIndex: number) => editor.instance.closeDocumentWithConfirmation(portfolio.state.documents[tabIndex].id)"
:tabActiveIndex="portfolio.state.activeDocumentIndex"
ref="documentsPanel"
/>
</LayoutRow>
<LayoutRow class="workspace-grid-resize-gutter" @pointerdown="(e) => resizePanel(e)" v-if="nodeGraphVisible"></LayoutRow>
<LayoutRow class="workspace-grid-resize-gutter" @pointerdown="(e: PointerEvent) => resizePanel(e)" v-if="nodeGraphVisible"></LayoutRow>
<LayoutRow class="workspace-grid-subdivision" v-if="nodeGraphVisible">
<Panel :panelType="'NodeGraph'" :tabLabels="['Node Graph']" :tabActiveIndex="0" />
</LayoutRow>
</LayoutCol>
<LayoutCol class="workspace-grid-resize-gutter" @pointerdown="(e) => resizePanel(e)"></LayoutCol>
<LayoutCol class="workspace-grid-resize-gutter" @pointerdown="(e: PointerEvent) => resizePanel(e)"></LayoutCol>
<LayoutCol class="workspace-grid-subdivision" style="flex-grow: 0.17">
<LayoutRow class="workspace-grid-subdivision" style="flex-grow: 402">
<Panel :panelType="'Properties'" :tabLabels="['Properties']" :tabActiveIndex="0" />
</LayoutRow>
<LayoutRow class="workspace-grid-resize-gutter" @pointerdown="(e) => resizePanel(e)"></LayoutRow>
<LayoutRow class="workspace-grid-resize-gutter" @pointerdown="(e: PointerEvent) => resizePanel(e)"></LayoutRow>
<LayoutRow class="workspace-grid-subdivision" style="flex-grow: 590">
<Panel :panelType="'LayerTree'" :tabLabels="['Layer Tree']" :tabActiveIndex="0" />
</LayoutRow>

View file

@ -244,7 +244,7 @@ export function createInputManager(editor: Editor, container: HTMLElement, dialo
if (editor.instance.hasCrashed()) return;
// Skip the message during development, since it's annoying when testing
if (process.env.NODE_ENV === "development") return;
if (editor.instance.inDevelopmentMode()) return;
const allDocumentsSaved = document.state.documents.reduce((acc, doc) => acc && doc.isSaved, true);
if (!allDocumentsSaved) {

View file

@ -1,23 +0,0 @@
import { type RGBA as RGBA_ } from "@/wasm-communication/messages";
import FloatingMenu from "@/components/floating-menus/FloatingMenu.vue";
import MenuList from "@/components/floating-menus/MenuList.vue";
// TODO: When a Volar bug is fixed (likely in v0.34.16):
// TODO: - Uncomment this block
// TODO: - Remove the `MenuList` and `FloatingMenu` lines from the `declare global` section below
// TODO: - And possibly add the empty export line of code `export {};` to the bottom of this file, for some reason
// declare module "vue" {
// interface ComponentCustomProperties {
// const MenuList: MenuList;
// const FloatingMenu: FloatingMenu;
// }
// }
// Satisfies Volar
// TODO: Move this back into `DropdownInput.vue` and `SwatchPairInput.vue` after https://github.com/johnsoncodehk/volar/issues/1321 is fixed
declare global {
const MenuList: MenuList;
const FloatingMenu: FloatingMenu;
type RGBA = RGBA_;
}

View file

@ -2,9 +2,11 @@
import { Transform, Type, plainToClass } from "class-transformer";
import { IconName, IconSize, IconStyle } from "@/utility-functions/icons";
import type { IconName, IconSize, IconStyle } from "@/utility-functions/icons";
import type { WasmEditorInstance, WasmRawInstance } from "@/wasm-communication/editor";
import type MenuList from "@/components/floating-menus/MenuList.vue";
export class JsMessage {
// The marker provides a way to check if an object is a sub-class constructor for a jsMessage.
static readonly jsMessageMarker = true;
@ -324,6 +326,8 @@ export class UpdateDocumentLayerDetails extends JsMessage {
export class LayerPanelEntry {
name!: string;
tooltip!: string;
visible!: boolean;
layerType!: LayerType;
@ -410,36 +414,32 @@ export class ColorInput extends WidgetProps {
tooltip!: string;
}
export type MenuColumn = {
type MenuEntryCommon = {
label: string;
children: MenuEntry[][];
};
export type MenuEntry = {
shortcut: ActionKeys | undefined;
action: Widget;
label: string;
icon: string | undefined;
children: undefined | MenuEntry[][];
};
export interface MenuListEntryData<Value = string> {
value?: Value;
label?: string;
icon?: IconName;
font?: URL;
shortcut?: ActionKeys;
shortcutRequiresLock?: boolean;
disabled?: boolean;
};
// The entry in the expanded menu or a sub-menu as received from the Rust backend
export type MenuBarEntry = MenuEntryCommon & {
action: Widget;
children?: MenuBarEntry[][];
};
// An entry in the all-encompassing MenuList component which defines all types of menus ranging from `MenuBarInput` to `DropdownInput` widgets
export type MenuListEntry = MenuEntryCommon & {
action?: () => void;
children?: SectionsOfMenuListEntries;
}
export type MenuListEntry<Value = string> = MenuListEntryData<Value> & { ref?: typeof FloatingMenu | typeof MenuList };
export type MenuListEntries<Value = string> = MenuListEntry<Value>[];
export type SectionsOfMenuListEntries<Value = string> = MenuListEntries<Value>[];
children?: MenuListEntry[][];
shortcutRequiresLock?: boolean;
value?: string;
disabled?: boolean;
font?: URL;
ref?: InstanceType<typeof MenuList>;
};
export class DropdownInput extends WidgetProps {
entries!: SectionsOfMenuListEntries;
entries!: MenuListEntry[][];
selectedIndex!: number | undefined;
@ -793,16 +793,19 @@ export class UpdateMenuBarLayout extends JsMessage {
// TODO: Replace `any` with correct typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Transform(({ value }: { value: any }) => createMenuLayout(value))
layout!: MenuColumn[];
layout!: MenuBarEntry[];
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function createMenuLayout(menuLayout: any[]): MenuColumn[] {
return menuLayout.map((column) => ({ ...column, children: createMenuLayoutRecursive(column.children) }));
function createMenuLayout(menuBarEntry: any[]): MenuBarEntry[] {
return menuBarEntry.map((entry) => ({
...entry,
children: createMenuLayoutRecursive(entry.children),
}));
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function createMenuLayoutRecursive(subLayout: any[][]): MenuEntry[][] {
return subLayout.map((groups) =>
function createMenuLayoutRecursive(children: any[][]): MenuBarEntry[][] {
return children.map((groups) =>
groups.map((entry) => ({
...entry,
action: hoistWidgetHolders([entry.action])[0],

View file

@ -98,11 +98,11 @@ License information is required on production builds. Aborting.`);
let licenses = (rustLicenses || []).map((rustLicense) => ({
licenseName: htmlDecode(rustLicense.licenseName),
licenseText: trimBlankLines(htmlDecode(rustLicense.licenseText)),
packages: rustLicense.packages.map((package) => ({
name: htmlDecode(package.name),
version: htmlDecode(package.version),
author: htmlDecode(package.author).replace(/\[(.*), \]/, "$1"),
repository: htmlDecode(package.repository),
packages: rustLicense.packages.map((packageInfo) => ({
name: htmlDecode(packageInfo.name),
version: htmlDecode(packageInfo.version),
author: htmlDecode(packageInfo.author).replace(/\[(.*), \]/, "$1"),
repository: htmlDecode(packageInfo.repository),
})),
}));
@ -119,7 +119,7 @@ License information is required on production builds. Aborting.`);
// Delete the internal Graphite crates, which are not third-party and belong elsewhere
licenses = licenses.filter((license) => {
license.packages = license.packages.filter((package) => !(package.repository && package.repository.includes("github.com/GraphiteEditor/Graphite")));
license.packages = license.packages.filter((packageInfo) => !(packageInfo.repository && packageInfo.repository.includes("github.com/GraphiteEditor/Graphite")));
return license.packages.length > 0;
});
@ -151,8 +151,8 @@ License information is required on production builds. Aborting.`);
licenses.forEach((license) => {
let packagesWithSameLicense = "";
license.packages.forEach((package) => {
const { name, version, author, repository } = package;
license.packages.forEach((packageInfo) => {
const { name, version, author, repository } = packageInfo;
packagesWithSameLicense += `${name} ${version}${author ? ` - ${author}` : ""}${repository ? ` - ${repository}` : ""}\n`;
});
packagesWithSameLicense = packagesWithSameLicense.trim();

View file

@ -137,6 +137,12 @@ impl JsEditorHandle {
EDITOR_HAS_CRASHED.load(Ordering::SeqCst)
}
/// Answer whether or not the editor is in development mode
#[wasm_bindgen(js_name = inDevelopmentMode)]
pub fn in_development_mode(&self) -> bool {
cfg!(debug_assertions)
}
/// Get the constant `FILE_SAVE_SUFFIX`
#[wasm_bindgen(js_name = fileSaveSuffix)]
pub fn file_save_suffix(&self) -> String {
@ -218,12 +224,6 @@ impl JsEditorHandle {
self.dispatch(message);
}
#[wasm_bindgen(js_name = requestComingSoonDialog)]
pub fn request_coming_soon_dialog(&self, issue: Option<i32>) {
let message = DialogMessage::RequestComingSoonDialog { issue };
self.dispatch(message);
}
/// Send new bounds when document panel viewports get resized or moved within the editor
/// [left, top, right, bottom]...
#[wasm_bindgen(js_name = boundsOfViewports)]
@ -468,6 +468,7 @@ impl JsEditorHandle {
impl Drop for JsEditorHandle {
fn drop(&mut self) {
// Consider removing after https://github.com/rustwasm/wasm-bindgen/pull/2984 is merged and released
EDITOR_INSTANCES.with(|instances| instances.borrow_mut().remove(&self.editor_id));
}
}