mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 05:18:19 +00:00
Move layouts definitions to backend and fix Firefox overlay scrollbars (#647)
* Fix two-axis scrollbars in scrollable regions on Firefox * Move Document Mode dropdown to the backend; and related code cleanup * Port the Layer Tree options bar layout to the backend * Port the tool shelf to the backend * Clean up initialization and wasm wrapper * Fix crash * Fix missing document bar * Remove unused functions in api.rs * Code review * Tool initalisation * Remove some frontend functions * Initalise -> Init so en-US/GB doesn't have to matter :) * Remove blend_mode and opacity from LayerPanelEntry Co-authored-by: 0hypercube <0hypercube@gmail.com>
This commit is contained in:
parent
e7d63276ad
commit
29e00e488b
50 changed files with 1034 additions and 978 deletions
|
@ -120,7 +120,7 @@ img {
|
|||
.layout-col {
|
||||
.scrollable-x,
|
||||
.scrollable-y {
|
||||
// Standard
|
||||
// Firefox (standardized in CSS, but less capable)
|
||||
scrollbar-width: thin;
|
||||
scrollbar-width: 6px;
|
||||
scrollbar-gutter: 6px;
|
||||
|
@ -130,7 +130,7 @@ img {
|
|||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
// WebKit
|
||||
// WebKit (only in Chromium/Safari but more capable)
|
||||
&::-webkit-scrollbar {
|
||||
width: calc(2px + 6px + 2px);
|
||||
height: calc(2px + 6px + 2px);
|
||||
|
@ -177,14 +177,14 @@ img {
|
|||
|
||||
.scrollable-x:not(.scrollable-y) {
|
||||
// Standard
|
||||
overflow-x: auto;
|
||||
overflow: auto hidden;
|
||||
// WebKit
|
||||
overflow-x: overlay;
|
||||
}
|
||||
|
||||
.scrollable-y:not(.scrollable-x) {
|
||||
// Standard
|
||||
overflow-y: auto;
|
||||
overflow: hidden auto;
|
||||
// WebKit
|
||||
overflow-y: overlay;
|
||||
}
|
||||
|
@ -325,6 +325,8 @@ export default defineComponent({
|
|||
},
|
||||
mounted() {
|
||||
this.inputManager = createInputManager(this.editor, this.$el.parentElement, this.dialog, this.portfolio, this.fullscreen);
|
||||
|
||||
this.editor.instance.init_app();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.inputManager?.removeListeners();
|
||||
|
|
|
@ -1,58 +1,18 @@
|
|||
<template>
|
||||
<LayoutCol class="document">
|
||||
<LayoutRow class="options-bar" :scrollableX="true">
|
||||
<LayoutRow class="left side">
|
||||
<DropdownInput :menuEntries="documentModeEntries" v-model:selectedIndex="documentModeSelectionIndex" :drawIcon="true" />
|
||||
|
||||
<Separator :type="'Section'" />
|
||||
|
||||
<WidgetLayout :layout="toolOptionsLayout" class="tool-options" />
|
||||
</LayoutRow>
|
||||
<WidgetLayout :layout="documentModeLayout" />
|
||||
<Separator :type="'Section'" />
|
||||
<WidgetLayout :layout="toolOptionsLayout" />
|
||||
|
||||
<LayoutRow class="spacer"></LayoutRow>
|
||||
|
||||
<WidgetLayout :layout="documentBarLayout" class="right side document-bar" />
|
||||
<WidgetLayout :layout="documentBarLayout" />
|
||||
</LayoutRow>
|
||||
<LayoutRow class="shelf-and-viewport">
|
||||
<LayoutCol class="shelf">
|
||||
<LayoutCol class="tools" :scrollableY="true">
|
||||
<ShelfItemInput icon="GeneralSelectTool" title="Select Tool (V)" :active="activeTool === 'Select'" :action="() => selectTool('Select')" />
|
||||
<ShelfItemInput icon="GeneralArtboardTool" title="Artboard Tool" :active="activeTool === 'Artboard'" :action="() => selectTool('Artboard')" />
|
||||
<ShelfItemInput icon="GeneralNavigateTool" title="Navigate Tool (Z)" :active="activeTool === 'Navigate'" :action="() => selectTool('Navigate')" />
|
||||
<ShelfItemInput icon="GeneralEyedropperTool" title="Eyedropper Tool (I)" :active="activeTool === 'Eyedropper'" :action="() => selectTool('Eyedropper')" />
|
||||
<ShelfItemInput icon="GeneralFillTool" title="Fill Tool (F)" :active="activeTool === 'Fill'" :action="() => selectTool('Fill')" />
|
||||
<ShelfItemInput icon="GeneralGradientTool" title="Gradient Tool (H)" :active="activeTool === 'Gradient'" :action="() => selectTool('Gradient')" />
|
||||
|
||||
<Separator :type="'Section'" :direction="'Vertical'" />
|
||||
|
||||
<ShelfItemInput icon="VectorPathTool" title="Path Tool (A)" :active="activeTool === 'Path'" :action="() => selectTool('Path')" />
|
||||
<ShelfItemInput icon="VectorPenTool" title="Pen Tool (P)" :active="activeTool === 'Pen'" :action="() => selectTool('Pen')" />
|
||||
<ShelfItemInput icon="VectorFreehandTool" title="Freehand Tool (N)" :active="activeTool === 'Freehand'" :action="() => selectTool('Freehand')" />
|
||||
<ShelfItemInput icon="VectorSplineTool" title="Spline Tool" :active="activeTool === 'Spline'" :action="() => selectTool('Spline')" />
|
||||
<ShelfItemInput icon="VectorLineTool" title="Line Tool (L)" :active="activeTool === 'Line'" :action="() => selectTool('Line')" />
|
||||
<ShelfItemInput icon="VectorRectangleTool" title="Rectangle Tool (M)" :active="activeTool === 'Rectangle'" :action="() => selectTool('Rectangle')" />
|
||||
<ShelfItemInput icon="VectorEllipseTool" title="Ellipse Tool (E)" :active="activeTool === 'Ellipse'" :action="() => selectTool('Ellipse')" />
|
||||
<ShelfItemInput icon="VectorShapeTool" title="Shape Tool (Y)" :active="activeTool === 'Shape'" :action="() => selectTool('Shape')" />
|
||||
<ShelfItemInput icon="VectorTextTool" title="Text Tool (T)" :active="activeTool === 'Text'" :action="() => selectTool('Text')" />
|
||||
|
||||
<Separator :type="'Section'" :direction="'Vertical'" />
|
||||
|
||||
<ShelfItemInput icon="RasterBrushTool" title="Coming Soon: Brush Tool (B)" :active="activeTool === 'Brush'" :action="() => (dialog.comingSoon(), false) && selectTool('Brush')" />
|
||||
<ShelfItemInput icon="RasterHealTool" title="Coming Soon: Heal Tool (J)" :active="activeTool === 'Heal'" :action="() => (dialog.comingSoon(), false) && selectTool('Heal')" />
|
||||
<ShelfItemInput icon="RasterCloneTool" title="Coming Soon: Clone Tool (C)" :active="activeTool === 'Clone'" :action="() => (dialog.comingSoon(), false) && selectTool('Clone')" />
|
||||
<ShelfItemInput icon="RasterPatchTool" title="Coming Soon: Patch Tool" :active="activeTool === 'Patch'" :action="() => (dialog.comingSoon(), false) && selectTool('Patch')" />
|
||||
<ShelfItemInput
|
||||
icon="RasterDetailTool"
|
||||
title="Coming Soon: Detail Tool (D)"
|
||||
:active="activeTool === 'Detail'"
|
||||
:action="() => (dialog.comingSoon(), false) && selectTool('Detail')"
|
||||
/>
|
||||
<ShelfItemInput
|
||||
icon="RasterRelightTool"
|
||||
title="Coming Soon: Relight Tool (O)"
|
||||
:active="activeTool === 'Relight'"
|
||||
:action="() => (dialog.comingSoon(), false) && selectTool('Relight')"
|
||||
/>
|
||||
<WidgetLayout :layout="toolShelfLayout" />
|
||||
</LayoutCol>
|
||||
|
||||
<LayoutCol class="spacer"></LayoutCol>
|
||||
|
@ -128,13 +88,7 @@
|
|||
.options-bar {
|
||||
height: 32px;
|
||||
flex: 0 0 auto;
|
||||
|
||||
.side {
|
||||
height: 100%;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
margin: 0 4px;
|
||||
}
|
||||
margin: 0 4px;
|
||||
|
||||
.spacer {
|
||||
min-width: 40px;
|
||||
|
@ -148,7 +102,7 @@
|
|||
.tools {
|
||||
flex: 0 1 auto;
|
||||
|
||||
.shelf-item-input[title^="Coming Soon"] {
|
||||
.icon-button[title^="Coming Soon"] {
|
||||
opacity: 0.25;
|
||||
transition: opacity 0.25s;
|
||||
|
||||
|
@ -272,13 +226,11 @@ import {
|
|||
UpdateDocumentOverlays,
|
||||
UpdateDocumentScrollbars,
|
||||
UpdateDocumentRulers,
|
||||
UpdateActiveTool,
|
||||
UpdateCanvasZoom,
|
||||
UpdateCanvasRotation,
|
||||
ToolName,
|
||||
UpdateDocumentArtboards,
|
||||
UpdateMouseCursor,
|
||||
UpdateDocumentModeLayout,
|
||||
UpdateToolOptionsLayout,
|
||||
UpdateToolShelfLayout,
|
||||
defaultWidgetLayout,
|
||||
UpdateDocumentBarLayout,
|
||||
UpdateImageData,
|
||||
|
@ -299,10 +251,6 @@ import { loadDefaultFont, setLoadDefaultFontCallback } from "@/utilities/fonts";
|
|||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
||||
import { SectionsOfMenuListEntries } from "@/components/widgets/floating-menus/MenuList.vue";
|
||||
import DropdownInput from "@/components/widgets/inputs/DropdownInput.vue";
|
||||
import { RadioEntries } from "@/components/widgets/inputs/RadioInput.vue";
|
||||
import ShelfItemInput from "@/components/widgets/inputs/ShelfItemInput.vue";
|
||||
import SwatchPairInput from "@/components/widgets/inputs/SwatchPairInput.vue";
|
||||
import CanvasRuler from "@/components/widgets/rulers/CanvasRuler.vue";
|
||||
import PersistentScrollbar from "@/components/widgets/scrollbars/PersistentScrollbar.vue";
|
||||
|
@ -366,9 +314,6 @@ export default defineComponent({
|
|||
const move = delta < 0 ? 1 : -1;
|
||||
this.editor.instance.translate_canvas_by_fraction(0, move);
|
||||
},
|
||||
selectTool(toolName: string) {
|
||||
this.editor.instance.select_tool(toolName);
|
||||
},
|
||||
swapWorkingColors() {
|
||||
this.editor.instance.swap_colors();
|
||||
},
|
||||
|
@ -440,19 +385,6 @@ export default defineComponent({
|
|||
this.rulerInterval = updateDocumentRulers.interval;
|
||||
});
|
||||
|
||||
this.editor.dispatcher.subscribeJsMessage(UpdateActiveTool, (updateActiveTool) => {
|
||||
this.activeTool = updateActiveTool.tool_name;
|
||||
});
|
||||
|
||||
this.editor.dispatcher.subscribeJsMessage(UpdateCanvasZoom, (updateCanvasZoom) => {
|
||||
this.documentZoom = updateCanvasZoom.factor * 100;
|
||||
});
|
||||
|
||||
this.editor.dispatcher.subscribeJsMessage(UpdateCanvasRotation, (updateCanvasRotation) => {
|
||||
const newRotation = updateCanvasRotation.angle_radians * (180 / Math.PI);
|
||||
this.documentRotation = (360 + (newRotation % 360)) % 360;
|
||||
});
|
||||
|
||||
this.editor.dispatcher.subscribeJsMessage(UpdateMouseCursor, (updateMouseCursor) => {
|
||||
this.canvasCursor = updateMouseCursor.cursor;
|
||||
});
|
||||
|
@ -502,6 +434,10 @@ export default defineComponent({
|
|||
);
|
||||
});
|
||||
|
||||
this.editor.dispatcher.subscribeJsMessage(UpdateDocumentModeLayout, (updateDocumentModeLayout) => {
|
||||
this.documentModeLayout = updateDocumentModeLayout;
|
||||
});
|
||||
|
||||
this.editor.dispatcher.subscribeJsMessage(UpdateToolOptionsLayout, (updateToolOptionsLayout) => {
|
||||
this.toolOptionsLayout = updateToolOptionsLayout;
|
||||
});
|
||||
|
@ -509,6 +445,11 @@ export default defineComponent({
|
|||
this.editor.dispatcher.subscribeJsMessage(UpdateDocumentBarLayout, (updateDocumentBarLayout) => {
|
||||
this.documentBarLayout = updateDocumentBarLayout;
|
||||
});
|
||||
|
||||
this.editor.dispatcher.subscribeJsMessage(UpdateToolShelfLayout, (updateToolShelfLayout) => {
|
||||
this.toolShelfLayout = updateToolShelfLayout;
|
||||
});
|
||||
|
||||
this.editor.dispatcher.subscribeJsMessage(TriggerViewportResize, this.viewportResize);
|
||||
|
||||
this.editor.dispatcher.subscribeJsMessage(UpdateImageData, (updateImageData) => {
|
||||
|
@ -524,7 +465,7 @@ export default defineComponent({
|
|||
});
|
||||
});
|
||||
|
||||
// Gets metadat populated in `frontend/vue.config.js`. We could potentially move this functionality in a build.rs file.
|
||||
// Gets metadata populated in `frontend/vue.config.js`. We could potentially move this functionality in a build.rs file.
|
||||
const loadBuildMetadata = (): void => {
|
||||
const release = process.env.VUE_APP_RELEASE_SERIES;
|
||||
let timestamp = "";
|
||||
|
@ -544,64 +485,50 @@ export default defineComponent({
|
|||
this.editor.instance.populate_build_metadata(release || "", timestamp, hash, branch || "");
|
||||
};
|
||||
|
||||
// TODO(mfish33): Replace with initialization system Issue:#524
|
||||
// Get initial Document Bar
|
||||
this.editor.instance.init_document_bar();
|
||||
setLoadDefaultFontCallback((font: string, data: Uint8Array) => this.editor.instance.on_font_load(font, data, true));
|
||||
|
||||
loadBuildMetadata();
|
||||
},
|
||||
data() {
|
||||
const documentModeEntries: SectionsOfMenuListEntries = [
|
||||
[
|
||||
{ label: "Design Mode", icon: "ViewportDesignMode" },
|
||||
{ label: "Select Mode", icon: "ViewportSelectMode", action: (): void => this.dialog.comingSoon(330) },
|
||||
{ label: "Guide Mode", icon: "ViewportGuideMode", action: (): void => this.dialog.comingSoon(331) },
|
||||
],
|
||||
];
|
||||
const viewModeEntries: RadioEntries = [
|
||||
{ value: "normal", icon: "ViewModeNormal", tooltip: "View Mode: Normal", action: (): void => this.setViewMode("Normal") },
|
||||
{ value: "outline", icon: "ViewModeOutline", tooltip: "View Mode: Outline", action: (): void => this.setViewMode("Outline") },
|
||||
{ value: "pixels", icon: "ViewModePixels", tooltip: "View Mode: Pixels", action: (): void => this.dialog.comingSoon(320) },
|
||||
];
|
||||
|
||||
return {
|
||||
artworkSvg: "",
|
||||
artboardSvg: "",
|
||||
overlaysSvg: "",
|
||||
// Interactive text editing
|
||||
textInput: undefined as undefined | HTMLDivElement,
|
||||
|
||||
// CSS properties
|
||||
canvasSvgWidth: "100%",
|
||||
canvasSvgHeight: "100%",
|
||||
canvasCursor: "default",
|
||||
activeTool: "Select" as ToolName,
|
||||
toolOptionsLayout: defaultWidgetLayout(),
|
||||
documentBarLayout: defaultWidgetLayout(),
|
||||
documentModeEntries,
|
||||
viewModeEntries,
|
||||
documentModeSelectionIndex: 0,
|
||||
viewModeIndex: 0,
|
||||
snappingEnabled: true,
|
||||
gridEnabled: true,
|
||||
overlaysEnabled: true,
|
||||
documentRotation: 0,
|
||||
documentZoom: 100,
|
||||
|
||||
// Scrollbars
|
||||
scrollbarPos: { x: 0.5, y: 0.5 },
|
||||
scrollbarSize: { x: 0.5, y: 0.5 },
|
||||
scrollbarMultiplier: { x: 0, y: 0 },
|
||||
|
||||
// Rulers
|
||||
rulerOrigin: { x: 0, y: 0 },
|
||||
rulerSpacing: 100,
|
||||
rulerInterval: 100,
|
||||
textInput: undefined as undefined | HTMLDivElement,
|
||||
|
||||
// Rendered SVG viewport data
|
||||
artworkSvg: "",
|
||||
artboardSvg: "",
|
||||
overlaysSvg: "",
|
||||
|
||||
// Layouts
|
||||
documentModeLayout: defaultWidgetLayout(),
|
||||
toolOptionsLayout: defaultWidgetLayout(),
|
||||
documentBarLayout: defaultWidgetLayout(),
|
||||
toolShelfLayout: defaultWidgetLayout(),
|
||||
};
|
||||
},
|
||||
components: {
|
||||
LayoutRow,
|
||||
LayoutCol,
|
||||
SwatchPairInput,
|
||||
ShelfItemInput,
|
||||
Separator,
|
||||
PersistentScrollbar,
|
||||
CanvasRuler,
|
||||
IconButton,
|
||||
DropdownInput,
|
||||
WidgetLayout,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,36 +1,7 @@
|
|||
<template>
|
||||
<LayoutCol class="layer-tree">
|
||||
<LayoutRow class="options-bar">
|
||||
<DropdownInput
|
||||
v-model:selectedIndex="blendModeSelectedIndex"
|
||||
@update:selectedIndex="(newSelectedIndex: number) => setLayerBlendMode(newSelectedIndex)"
|
||||
:menuEntries="blendModeEntries"
|
||||
:disabled="blendModeDropdownDisabled"
|
||||
/>
|
||||
|
||||
<Separator :type="'Related'" />
|
||||
|
||||
<NumberInput
|
||||
v-model:value="opacity"
|
||||
@update:value="(newOpacity: number) => setLayerOpacity(newOpacity)"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:unit="'%'"
|
||||
:displayDecimalPlaces="2"
|
||||
:label="'Opacity'"
|
||||
:disabled="opacityNumberInputDisabled"
|
||||
/>
|
||||
|
||||
<!-- <PopoverButton>
|
||||
<h3>Compositing Options</h3>
|
||||
<p>The contents of this popover menu are coming soon</p>
|
||||
</PopoverButton> -->
|
||||
|
||||
<Separator :type="'Section'" />
|
||||
|
||||
<!-- TODO: Remember to make these tooltip input hints customized to macOS also -->
|
||||
<IconButton :action="createEmptyFolder" :icon="'NodeFolder'" title="New Folder (Ctrl+Shift+N)" :size="24" />
|
||||
<IconButton :action="deleteSelectedLayers" :icon="'Trash'" title="Delete Selected (Del)" :size="24" />
|
||||
<LayoutRow class="options-bar" :scrollableX="true">
|
||||
<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()">
|
||||
|
@ -101,22 +72,31 @@
|
|||
.layer-tree {
|
||||
min-height: 0;
|
||||
|
||||
// Options bar
|
||||
.options-bar {
|
||||
height: 32px;
|
||||
flex: 0 0 auto;
|
||||
margin: 0 4px;
|
||||
align-items: center;
|
||||
|
||||
.widget-layout {
|
||||
width: 100%;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
// Blend mode selector
|
||||
.dropdown-input {
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
// Blend mode selector and opacity slider
|
||||
.dropdown-input,
|
||||
.number-input {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Layer tree
|
||||
.layer-tree-rows {
|
||||
margin-top: 4px;
|
||||
// Crop away the 1px border below the bottom layer entry when it uses the full space of this panel
|
||||
|
@ -283,58 +263,16 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
import { BlendMode, UpdateDocumentLayerTreeStructure, UpdateDocumentLayer, LayerPanelEntry } from "@/dispatcher/js-messages";
|
||||
import { defaultWidgetLayout, UpdateDocumentLayerTreeStructure, UpdateDocumentLayerDetails, UpdateLayerTreeOptionsLayout, LayerPanelEntry } from "@/dispatcher/js-messages";
|
||||
|
||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
||||
import { SectionsOfMenuListEntries } from "@/components/widgets/floating-menus/MenuList.vue";
|
||||
import DropdownInput from "@/components/widgets/inputs/DropdownInput.vue";
|
||||
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
import Separator from "@/components/widgets/separators/Separator.vue";
|
||||
import WidgetLayout from "@/components/widgets/WidgetLayout.vue";
|
||||
|
||||
type LayerListingInfo = { folderIndex: number; bottomLayer: boolean; editingName: boolean; entry: LayerPanelEntry };
|
||||
|
||||
const blendModeEntries: SectionsOfMenuListEntries<BlendMode> = [
|
||||
[{ label: "Normal", value: "Normal" }],
|
||||
[
|
||||
{ label: "Multiply", value: "Multiply" },
|
||||
{ label: "Darken", value: "Darken" },
|
||||
{ label: "Color Burn", value: "ColorBurn" },
|
||||
// { label: "Linear Burn", value: "" }, // Not supported by SVG
|
||||
// { label: "Darker Color", value: "" }, // Not supported by SVG
|
||||
],
|
||||
[
|
||||
{ label: "Screen", value: "Screen" },
|
||||
{ label: "Lighten", value: "Lighten" },
|
||||
{ label: "Color Dodge", value: "ColorDodge" },
|
||||
// { label: "Linear Dodge (Add)", value: "" }, // Not supported by SVG
|
||||
// { label: "Lighter Color", value: "" }, // Not supported by SVG
|
||||
],
|
||||
[
|
||||
{ label: "Overlay", value: "Overlay" },
|
||||
{ label: "Soft Light", value: "SoftLight" },
|
||||
{ label: "Hard Light", value: "HardLight" },
|
||||
// { label: "Vivid Light", value: "" }, // Not supported by SVG
|
||||
// { label: "Linear Light", value: "" }, // Not supported by SVG
|
||||
// { label: "Pin Light", value: "" }, // Not supported by SVG
|
||||
// { label: "Hard Mix", value: "" }, // Not supported by SVG
|
||||
],
|
||||
[
|
||||
{ label: "Difference", value: "Difference" },
|
||||
{ label: "Exclusion", value: "Exclusion" },
|
||||
// { label: "Subtract", value: "" }, // Not supported by SVG
|
||||
// { label: "Divide", value: "" }, // Not supported by SVG
|
||||
],
|
||||
[
|
||||
{ label: "Hue", value: "Hue" },
|
||||
{ label: "Saturation", value: "Saturation" },
|
||||
{ label: "Color", value: "Color" },
|
||||
{ label: "Luminosity", value: "Luminosity" },
|
||||
],
|
||||
];
|
||||
|
||||
const RANGE_TO_INSERT_WITHIN_BOTTOM_FOLDER_NOT_ROOT = 20;
|
||||
const LAYER_INDENT = 16;
|
||||
const INSERT_MARK_MARGIN_LEFT = 4 + 32 + LAYER_INDENT;
|
||||
|
@ -346,20 +284,17 @@ export default defineComponent({
|
|||
inject: ["editor"],
|
||||
data() {
|
||||
return {
|
||||
blendModeEntries,
|
||||
blendModeSelectedIndex: 0,
|
||||
blendModeDropdownDisabled: true,
|
||||
opacityNumberInputDisabled: true,
|
||||
// TODO: replace with BigUint64Array as index
|
||||
layerCache: new Map() as Map<string, LayerPanelEntry>,
|
||||
// Layer data
|
||||
layerCache: new Map() as Map<string, LayerPanelEntry>, // TODO: replace with BigUint64Array as index
|
||||
layers: [] as LayerListingInfo[],
|
||||
layerDepths: [] as number[],
|
||||
selectionRangeStartLayer: undefined as undefined | LayerPanelEntry,
|
||||
selectionRangeEndLayer: undefined as undefined | LayerPanelEntry,
|
||||
opacity: 100,
|
||||
devMode: process.env.NODE_ENV === "development",
|
||||
|
||||
// Interactive dragging
|
||||
draggable: true,
|
||||
draggingData: undefined as undefined | DraggingData,
|
||||
devMode: process.env.NODE_ENV === "development",
|
||||
|
||||
// Layouts
|
||||
layerTreeOptionsLayout: defaultWidgetLayout(),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
@ -372,12 +307,6 @@ export default defineComponent({
|
|||
markTopOffset(height: number): string {
|
||||
return `${height}px`;
|
||||
},
|
||||
createEmptyFolder() {
|
||||
this.editor.instance.create_empty_folder();
|
||||
},
|
||||
deleteSelectedLayers() {
|
||||
this.editor.instance.delete_selected_layers();
|
||||
},
|
||||
toggleLayerVisibility(path: BigUint64Array) {
|
||||
this.editor.instance.toggle_layer_visibility(path);
|
||||
},
|
||||
|
@ -413,27 +342,12 @@ export default defineComponent({
|
|||
window.getSelection()?.removeAllRanges();
|
||||
});
|
||||
},
|
||||
async setLayerBlendMode(newSelectedIndex: number) {
|
||||
const blendMode = this.blendModeEntries.flat()[newSelectedIndex].value;
|
||||
if (blendMode) this.editor.instance.set_blend_mode_for_selected_layers(blendMode);
|
||||
},
|
||||
async setLayerOpacity(newOpacity: number) {
|
||||
this.editor.instance.set_opacity_for_selected_layers(newOpacity);
|
||||
},
|
||||
async selectLayer(clickedLayer: LayerPanelEntry, ctrl: boolean, shift: boolean) {
|
||||
this.editor.instance.select_layer(clickedLayer.path, ctrl, shift);
|
||||
},
|
||||
async deselectAllLayers() {
|
||||
this.selectionRangeStartLayer = undefined;
|
||||
this.selectionRangeEndLayer = undefined;
|
||||
|
||||
this.editor.instance.deselect_all_layers();
|
||||
},
|
||||
async clearSelection() {
|
||||
this.layers.forEach((layer) => {
|
||||
layer.entry.layer_metadata.selected = false;
|
||||
});
|
||||
},
|
||||
calculateDragIndex(tree: HTMLElement, clientY: number): DraggingData {
|
||||
const treeChildren = tree.children;
|
||||
const treeOffset = tree.getBoundingClientRect().top;
|
||||
|
@ -523,51 +437,7 @@ export default defineComponent({
|
|||
this.draggingData = undefined;
|
||||
}
|
||||
},
|
||||
// TODO: Move blend mode setting logic to backend based on the layers it knows are selected
|
||||
setBlendModeForSelectedLayers() {
|
||||
const selected = this.layers.filter((layer) => layer.entry.layer_metadata.selected);
|
||||
|
||||
if (selected.length < 1) {
|
||||
this.blendModeSelectedIndex = 0;
|
||||
this.blendModeDropdownDisabled = true;
|
||||
return;
|
||||
}
|
||||
this.blendModeDropdownDisabled = false;
|
||||
|
||||
const firstEncounteredBlendMode = selected[0].entry.blend_mode;
|
||||
const allBlendModesAlike = !selected.find((layer) => layer.entry.blend_mode !== firstEncounteredBlendMode);
|
||||
|
||||
if (allBlendModesAlike) {
|
||||
this.blendModeSelectedIndex = this.blendModeEntries.flat().findIndex((entry) => entry.value === firstEncounteredBlendMode);
|
||||
} else {
|
||||
// Display a dash when they are not all the same value
|
||||
this.blendModeSelectedIndex = NaN;
|
||||
}
|
||||
},
|
||||
// TODO: Move opacity setting logic to backend based on the layers it knows are selected
|
||||
setOpacityForSelectedLayers() {
|
||||
const selected = this.layers.filter((layer) => layer.entry.layer_metadata.selected);
|
||||
|
||||
if (selected.length < 1) {
|
||||
this.opacity = 100;
|
||||
this.opacityNumberInputDisabled = true;
|
||||
return;
|
||||
}
|
||||
this.opacityNumberInputDisabled = false;
|
||||
|
||||
const firstEncounteredOpacity = selected[0].entry.opacity;
|
||||
const allOpacitiesAlike = !selected.find((layer) => layer.entry.opacity !== firstEncounteredOpacity);
|
||||
|
||||
if (allOpacitiesAlike) {
|
||||
this.opacity = firstEncounteredOpacity;
|
||||
} else {
|
||||
// Display a dash when they are not all the same value
|
||||
this.opacity = NaN;
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.editor.dispatcher.subscribeJsMessage(UpdateDocumentLayerTreeStructure, (updateDocumentLayerTreeStructure) => {
|
||||
rebuildLayerTree(updateDocumentLayerTreeStructure: UpdateDocumentLayerTreeStructure) {
|
||||
const layerWithNameBeingEdited = this.layers.find((layer: LayerListingInfo) => layer.editingName);
|
||||
const layerPathWithNameBeingEdited = layerWithNameBeingEdited?.entry.path;
|
||||
const layerIdWithNameBeingEdited = layerPathWithNameBeingEdited?.slice(-1)[0];
|
||||
|
@ -598,31 +468,35 @@ export default defineComponent({
|
|||
};
|
||||
|
||||
recurse(updateDocumentLayerTreeStructure, this.layers, this.layerCache);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.editor.dispatcher.subscribeJsMessage(UpdateDocumentLayerTreeStructure, (updateDocumentLayerTreeStructure) => {
|
||||
this.rebuildLayerTree(updateDocumentLayerTreeStructure);
|
||||
});
|
||||
|
||||
this.editor.dispatcher.subscribeJsMessage(UpdateDocumentLayer, (updateDocumentLayer) => {
|
||||
const targetPath = updateDocumentLayer.data.path;
|
||||
const targetLayer = updateDocumentLayer.data;
|
||||
this.editor.dispatcher.subscribeJsMessage(UpdateLayerTreeOptionsLayout, (updateLayerTreeOptionsLayout) => {
|
||||
this.layerTreeOptionsLayout = updateLayerTreeOptionsLayout;
|
||||
});
|
||||
|
||||
this.editor.dispatcher.subscribeJsMessage(UpdateDocumentLayerDetails, (updateDocumentLayerDetails) => {
|
||||
const targetPath = updateDocumentLayerDetails.data.path;
|
||||
const targetLayer = updateDocumentLayerDetails.data;
|
||||
|
||||
const layer = this.layerCache.get(targetPath.toString());
|
||||
if (layer) {
|
||||
Object.assign(this.layerCache.get(targetPath.toString()), targetLayer);
|
||||
Object.assign(layer, targetLayer);
|
||||
} else {
|
||||
this.layerCache.set(targetPath.toString(), targetLayer);
|
||||
}
|
||||
|
||||
this.setBlendModeForSelectedLayers();
|
||||
this.setOpacityForSelectedLayers();
|
||||
});
|
||||
},
|
||||
components: {
|
||||
LayoutRow,
|
||||
LayoutCol,
|
||||
Separator,
|
||||
NumberInput,
|
||||
WidgetLayout,
|
||||
IconButton,
|
||||
IconLabel,
|
||||
DropdownInput,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<template>
|
||||
<div class="widget-layout">
|
||||
<template v-for="(layoutRow, index) in layout.layout" :key="index">
|
||||
<component :is="layoutRowType(layoutRow)" :widgetData="layoutRow" :layoutTarget="layout.layout_target"></component>
|
||||
</template>
|
||||
<component :is="layoutRowType(layoutRow)" :widgetData="layoutRow" :layoutTarget="layout.layout_target" v-for="(layoutRow, index) in layout.layout" :key="index" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -18,7 +16,7 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "vue";
|
||||
|
||||
import { isWidgetRow, isWidgetSection, LayoutRow, WidgetLayout } from "@/dispatcher/js-messages";
|
||||
import { isWidgetColumn, isWidgetRow, isWidgetSection, LayoutRow, WidgetLayout } from "@/dispatcher/js-messages";
|
||||
|
||||
import WidgetRow from "@/components/widgets/WidgetRow.vue";
|
||||
import WidgetSection from "@/components/widgets/WidgetSection.vue";
|
||||
|
@ -29,6 +27,7 @@ export default defineComponent({
|
|||
},
|
||||
methods: {
|
||||
layoutRowType(layoutRow: LayoutRow): unknown {
|
||||
if (isWidgetColumn(layoutRow)) return WidgetRow;
|
||||
if (isWidgetRow(layoutRow)) return WidgetRow;
|
||||
if (isWidgetSection(layoutRow)) return WidgetSection;
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="widget-row">
|
||||
<template v-for="(component, index) in widgetData.widgets" :key="index">
|
||||
<div :class="`widget-${direction}`">
|
||||
<template v-for="(component, index) in widgets" :key="index">
|
||||
<!-- TODO: Use `<component :is="" v-bind="attributesObject"></component>` to avoid all the separate components with `v-if` -->
|
||||
<CheckboxInput v-if="component.kind === 'CheckboxInput'" v-bind="component.props" @update:checked="(value: boolean) => updateLayout(component.widget_id, value)" />
|
||||
<ColorInput v-if="component.kind === 'ColorInput'" v-bind="component.props" @update:value="(value: string) => updateLayout(component.widget_id, value)" />
|
||||
|
@ -35,6 +35,12 @@
|
|||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.widget-column {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.widget-row {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
|
@ -63,7 +69,7 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "vue";
|
||||
|
||||
import { WidgetRow } from "@/dispatcher/js-messages";
|
||||
import { WidgetColumn, WidgetRow, isWidgetColumn, isWidgetRow } from "@/dispatcher/js-messages";
|
||||
|
||||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
||||
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
|
||||
|
@ -84,9 +90,21 @@ import Separator from "@/components/widgets/separators/Separator.vue";
|
|||
export default defineComponent({
|
||||
inject: ["editor"],
|
||||
props: {
|
||||
widgetData: { type: Object as PropType<WidgetRow>, required: true },
|
||||
widgetData: { type: Object as PropType<WidgetColumn | WidgetRow>, required: true },
|
||||
layoutTarget: { required: true },
|
||||
},
|
||||
computed: {
|
||||
direction() {
|
||||
if (isWidgetColumn(this.widgetData)) return "column";
|
||||
if (isWidgetRow(this.widgetData)) return "row";
|
||||
return "ERROR";
|
||||
},
|
||||
widgets() {
|
||||
if (isWidgetColumn(this.widgetData)) return this.widgetData.columnWidgets;
|
||||
if (isWidgetRow(this.widgetData)) return this.widgetData.rowWidgets;
|
||||
return [];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateLayout(widgetId: BigInt, value: unknown) {
|
||||
this.editor.instance.update_layout(this.layoutTarget, widgetId, value);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<button class="icon-button" :class="`size-${size}`" @click="(e: MouseEvent) => action(e)">
|
||||
<button :class="['icon-button', `size-${size}`, active && 'active']" @click="(e: MouseEvent) => action(e)">
|
||||
<IconLabel :icon="icon" />
|
||||
</button>
|
||||
</template>
|
||||
|
@ -25,7 +25,11 @@
|
|||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&.active {
|
||||
background: var(--color-accent);
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
background: var(--color-6-lowergray);
|
||||
color: var(--color-f-white);
|
||||
|
||||
|
@ -68,6 +72,7 @@ export default defineComponent({
|
|||
action: { type: Function as PropType<(e?: MouseEvent) => void>, required: true },
|
||||
icon: { type: String as PropType<IconName>, required: true },
|
||||
size: { type: Number as PropType<IconSize>, required: true },
|
||||
active: { type: Boolean as PropType<boolean>, default: false },
|
||||
gapAfter: { type: Boolean as PropType<boolean>, default: false },
|
||||
},
|
||||
components: { IconLabel },
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
</LayoutCol>
|
||||
<LayoutCol class="main-column">
|
||||
<WidgetLayout v-if="dialog.state.widgets.layout.length > 0" :layout="dialog.state.widgets" class="details" />
|
||||
<LayoutRow v-if="dialog.state.jsCallbackBasedButtons?.length > 0" class="panic-buttons-row">
|
||||
<LayoutRow v-if="(dialog.state.jsCallbackBasedButtons?.length || NaN) > 0" class="panic-buttons-row">
|
||||
<TextButton v-for="(button, index) in dialog.state.jsCallbackBasedButtons" :key="index" :action="() => button.callback?.()" v-bind="button.props" />
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<FloatingMenu class="menu-list" :direction="direction" :type="'Dropdown'" ref="floatingMenu" :windowEdgeMargin="0" :scrollableY="scrollableY" data-hover-menu-keep-open>
|
||||
<template v-for="(section, sectionIndex) in menuEntries" :key="sectionIndex">
|
||||
<template v-for="(section, sectionIndex) in entries" :key="sectionIndex">
|
||||
<Separator :type="'List'" :direction="'Vertical'" v-if="sectionIndex > 0" />
|
||||
<LayoutRow
|
||||
v-for="(entry, entryIndex) in section"
|
||||
|
@ -27,7 +27,7 @@
|
|||
<MenuList
|
||||
v-if="entry.children"
|
||||
:direction="'TopRight'"
|
||||
:menuEntries="entry.children"
|
||||
:entries="entry.children"
|
||||
v-bind="{ defaultAction, minWidth, drawIcon, scrollableY }"
|
||||
:ref="(ref: any) => setEntryRefs(entry, ref)"
|
||||
/>
|
||||
|
@ -167,7 +167,7 @@ const MenuList = defineComponent({
|
|||
inject: ["fullscreen"],
|
||||
props: {
|
||||
direction: { type: String as PropType<MenuDirection>, default: "Bottom" },
|
||||
menuEntries: { type: Array as PropType<SectionsOfMenuListEntries>, required: true },
|
||||
entries: { type: Array as PropType<SectionsOfMenuListEntries>, required: true },
|
||||
activeEntry: { type: Object as PropType<MenuListEntry>, required: false },
|
||||
defaultAction: { type: Function as PropType<() => void>, required: false },
|
||||
minWidth: { type: Number as PropType<number>, default: 0 },
|
||||
|
@ -243,9 +243,9 @@ const MenuList = defineComponent({
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
menuEntriesWithoutRefs(): MenuListEntryData[][] {
|
||||
return this.menuEntries.map((entries) =>
|
||||
entries.map((entry) => {
|
||||
entriesWithoutRefs(): MenuListEntryData[][] {
|
||||
return this.entries.map((menuListEntries) =>
|
||||
menuListEntries.map((entry) => {
|
||||
const { ref, ...entryWithoutRef } = entry;
|
||||
return entryWithoutRef;
|
||||
})
|
||||
|
@ -259,7 +259,7 @@ const MenuList = defineComponent({
|
|||
this.measureAndReportWidth();
|
||||
},
|
||||
watch: {
|
||||
menuEntriesWithoutRefs: {
|
||||
entriesWithoutRefs: {
|
||||
handler() {
|
||||
this.measureAndReportWidth();
|
||||
},
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
v-model:activeEntry="activeEntry"
|
||||
@update:activeEntry="(newActiveEntry: typeof MENU_LIST_ENTRY) => activeEntryChanged(newActiveEntry)"
|
||||
@widthChanged="(newWidth: number) => onWidthChanged(newWidth)"
|
||||
:menuEntries="menuEntries"
|
||||
:entries="entries"
|
||||
:direction="'Bottom'"
|
||||
:drawIcon="drawIcon"
|
||||
:scrollableY="true"
|
||||
|
@ -100,23 +100,23 @@ declare global {
|
|||
export default defineComponent({
|
||||
emits: ["update:selectedIndex"],
|
||||
props: {
|
||||
menuEntries: { type: Array as PropType<SectionsOfMenuListEntries>, required: true },
|
||||
selectedIndex: { type: Number as PropType<number>, required: true },
|
||||
entries: { type: Array as PropType<SectionsOfMenuListEntries>, 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 },
|
||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeEntry: this.menuEntries.flat()[this.selectedIndex],
|
||||
activeEntry: this.selectedIndex !== undefined ? this.entries.flat()[this.selectedIndex] : { label: "-" },
|
||||
minWidth: 0,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
// Called only when `selectedIndex` is changed from outside this component (with v-model)
|
||||
selectedIndex(newSelectedIndex: number) {
|
||||
const entries = this.menuEntries.flat();
|
||||
selectedIndex(newSelectedIndex: number | undefined) {
|
||||
const entries = this.entries.flat();
|
||||
|
||||
if (!Number.isNaN(newSelectedIndex) && newSelectedIndex >= 0 && newSelectedIndex < entries.length) {
|
||||
if (newSelectedIndex !== undefined && newSelectedIndex >= 0 && newSelectedIndex < entries.length) {
|
||||
this.activeEntry = entries[newSelectedIndex];
|
||||
} else {
|
||||
this.activeEntry = { label: "-" };
|
||||
|
@ -126,7 +126,7 @@ export default defineComponent({
|
|||
methods: {
|
||||
// Called only when `activeEntry` is changed from the child MenuList component via user input
|
||||
activeEntryChanged(newActiveEntry: MenuListEntry) {
|
||||
this.$emit("update:selectedIndex", this.menuEntries.flat().indexOf(newActiveEntry));
|
||||
this.$emit("update:selectedIndex", this.entries.flat().indexOf(newActiveEntry));
|
||||
},
|
||||
clickDropdownBox() {
|
||||
if (!this.disabled) (this.$refs.menuList as typeof MenuList).setOpen();
|
||||
|
|
|
@ -4,14 +4,7 @@
|
|||
<span>{{ activeEntry.label }}</span>
|
||||
<IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" />
|
||||
</LayoutRow>
|
||||
<MenuList
|
||||
v-model:activeEntry="activeEntry"
|
||||
@widthChanged="(newWidth: number) => onWidthChanged(newWidth)"
|
||||
:menuEntries="menuEntries"
|
||||
:direction="'Bottom'"
|
||||
:scrollableY="true"
|
||||
ref="menuList"
|
||||
/>
|
||||
<MenuList v-model:activeEntry="activeEntry" @widthChanged="(newWidth: number) => onWidthChanged(newWidth)" :entries="entries" :direction="'Bottom'" :scrollableY="true" ref="menuList" />
|
||||
</LayoutRow>
|
||||
</template>
|
||||
|
||||
|
@ -100,9 +93,9 @@ export default defineComponent({
|
|||
isStyle: { type: Boolean as PropType<boolean>, default: false },
|
||||
},
|
||||
data() {
|
||||
const { menuEntries, activeEntry } = this.updateEntries();
|
||||
const { entries, activeEntry } = this.updateEntries();
|
||||
return {
|
||||
menuEntries,
|
||||
entries,
|
||||
activeEntry,
|
||||
minWidth: 0,
|
||||
};
|
||||
|
@ -133,12 +126,12 @@ export default defineComponent({
|
|||
onWidthChanged(newWidth: number) {
|
||||
this.minWidth = newWidth;
|
||||
},
|
||||
updateEntries(): { menuEntries: SectionsOfMenuListEntries; activeEntry: MenuListEntry } {
|
||||
updateEntries(): { entries: SectionsOfMenuListEntries; activeEntry: MenuListEntry } {
|
||||
const choices = this.isStyle ? getFontStyles(this.fontFamily) : fontNames();
|
||||
const selectedChoice = this.isStyle ? this.fontStyle : this.fontFamily;
|
||||
|
||||
let selectedEntry: MenuListEntry | undefined;
|
||||
const entries = choices.map((name) => {
|
||||
const menuListEntries = choices.map((name) => {
|
||||
const result: MenuListEntry = {
|
||||
label: name,
|
||||
action: (): void => this.selectFont(name),
|
||||
|
@ -149,21 +142,21 @@ export default defineComponent({
|
|||
return result;
|
||||
});
|
||||
|
||||
const menuEntries: SectionsOfMenuListEntries = [entries];
|
||||
const entries: SectionsOfMenuListEntries = [menuListEntries];
|
||||
const activeEntry = selectedEntry || { label: "-" };
|
||||
|
||||
return { menuEntries, activeEntry };
|
||||
return { entries, activeEntry };
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
fontFamily() {
|
||||
const { menuEntries, activeEntry } = this.updateEntries();
|
||||
this.menuEntries = menuEntries;
|
||||
const { entries, activeEntry } = this.updateEntries();
|
||||
this.entries = entries;
|
||||
this.activeEntry = activeEntry;
|
||||
},
|
||||
fontStyle() {
|
||||
const { menuEntries, activeEntry } = this.updateEntries();
|
||||
this.menuEntries = menuEntries;
|
||||
const { entries, activeEntry } = this.updateEntries();
|
||||
this.entries = entries;
|
||||
this.activeEntry = activeEntry;
|
||||
},
|
||||
},
|
||||
|
|
|
@ -5,12 +5,12 @@
|
|||
<IconLabel :icon="'GraphiteLogo'" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry-container" v-for="(entry, index) in menuEntries" :key="index">
|
||||
<div class="entry-container" v-for="(entry, index) in entries" :key="index">
|
||||
<div @click="() => handleEntryClick(entry)" class="entry" :class="{ open: entry.ref?.isOpen() }" data-hover-menu-spawner>
|
||||
<IconLabel :icon="entry.icon" v-if="entry.icon" />
|
||||
<span v-if="entry.label">{{ entry.label }}</span>
|
||||
</div>
|
||||
<MenuList :menuEntries="entry.children || []" :direction="'Bottom'" :minWidth="240" :drawIcon="true" :defaultAction="comingSoon" :ref="(ref: any) => setEntryRefs(entry, ref)" />
|
||||
<MenuList :entries="entry.children || []" :direction="'Bottom'" :minWidth="240" :drawIcon="true" :defaultAction="comingSoon" :ref="(ref: any) => setEntryRefs(entry, ref)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -58,7 +58,7 @@ import { EditorState } from "@/state/wasm-loader";
|
|||
import MenuList, { MenuListEntry, MenuListEntries } from "@/components/widgets/floating-menus/MenuList.vue";
|
||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||
|
||||
function makeMenuEntries(editor: EditorState): MenuListEntries {
|
||||
function makeEntries(editor: EditorState): MenuListEntries {
|
||||
return [
|
||||
{
|
||||
label: "File",
|
||||
|
@ -213,7 +213,7 @@ export default defineComponent({
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
menuEntries: makeMenuEntries(this.editor),
|
||||
entries: makeEntries(this.editor),
|
||||
comingSoon: (): void => this.dialog.comingSoon(),
|
||||
};
|
||||
},
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
@cancelTextChange="() => onCancelTextChange()"
|
||||
ref="fieldInput"
|
||||
>
|
||||
<button v-if="!Number.isNaN(value)" class="arrow left" @click="() => onIncrement('Decrease')"></button>
|
||||
<button v-if="!Number.isNaN(value)" class="arrow right" @click="() => onIncrement('Increase')"></button>
|
||||
<button v-if="value !== undefined" class="arrow left" @click="() => onIncrement('Decrease')"></button>
|
||||
<button v-if="value !== undefined" class="arrow right" @click="() => onIncrement('Increase')"></button>
|
||||
</FieldInput>
|
||||
</template>
|
||||
|
||||
|
@ -94,7 +94,7 @@ import FieldInput from "@/components/widgets/inputs/FieldInput.vue";
|
|||
export default defineComponent({
|
||||
emits: ["update:value"],
|
||||
props: {
|
||||
value: { type: Number as PropType<number>, required: true },
|
||||
value: { type: Number as PropType<number>, required: false }, // When not provided, a dash is displayed
|
||||
min: { type: Number as PropType<number>, required: false },
|
||||
max: { type: Number as PropType<number>, required: false },
|
||||
incrementBehavior: { type: String as PropType<IncrementBehavior>, default: "Add" },
|
||||
|
@ -116,7 +116,7 @@ export default defineComponent({
|
|||
},
|
||||
methods: {
|
||||
onTextFocused() {
|
||||
if (Number.isNaN(this.value)) this.text = "";
|
||||
if (this.value === undefined) this.text = "";
|
||||
else if (this.unitIsHiddenWhenEditing) this.text = `${this.value}`;
|
||||
else this.text = `${this.value}${this.unit}`;
|
||||
|
||||
|
@ -131,18 +131,20 @@ export default defineComponent({
|
|||
// enter key (via the `change` event) or when the <input> element is defocused (with the `blur` event binding)
|
||||
onTextChanged() {
|
||||
// The `inputElement.blur()` call at the bottom of this function causes itself to be run again, so this check skips a second run
|
||||
if (this.editing) {
|
||||
const newValue = parseFloat(this.text);
|
||||
this.updateValue(newValue);
|
||||
if (!this.editing) return;
|
||||
|
||||
this.editing = false;
|
||||
const parsed = parseFloat(this.text);
|
||||
const newValue = Number.isNaN(parsed) ? undefined : parsed;
|
||||
|
||||
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement;
|
||||
inputElement.blur();
|
||||
}
|
||||
this.updateValue(newValue);
|
||||
|
||||
this.editing = false;
|
||||
|
||||
const inputElement = (this.$refs.fieldInput as typeof FieldInput).$refs.input as HTMLInputElement;
|
||||
inputElement.blur();
|
||||
},
|
||||
onCancelTextChange() {
|
||||
this.updateValue(NaN);
|
||||
this.updateValue(undefined);
|
||||
|
||||
this.editing = false;
|
||||
|
||||
|
@ -150,16 +152,16 @@ export default defineComponent({
|
|||
inputElement.blur();
|
||||
},
|
||||
onIncrement(direction: IncrementDirection) {
|
||||
if (Number.isNaN(this.value)) return;
|
||||
if (this.value === undefined) return;
|
||||
|
||||
const actions = {
|
||||
Add: (): void => {
|
||||
const directionAddend = direction === "Increase" ? this.incrementFactor : -this.incrementFactor;
|
||||
this.updateValue(this.value + directionAddend);
|
||||
this.updateValue(this.value !== undefined ? this.value + directionAddend : undefined);
|
||||
},
|
||||
Multiply: (): void => {
|
||||
const directionMultiplier = direction === "Increase" ? this.incrementFactor : 1 / this.incrementFactor;
|
||||
this.updateValue(this.value * directionMultiplier);
|
||||
this.updateValue(this.value !== undefined ? this.value * directionMultiplier : undefined);
|
||||
},
|
||||
Callback: (): void => {
|
||||
if (direction === "Increase") this.incrementCallbackIncrease?.();
|
||||
|
@ -170,20 +172,20 @@ export default defineComponent({
|
|||
const action = actions[this.incrementBehavior];
|
||||
action();
|
||||
},
|
||||
updateValue(newValue: number) {
|
||||
const invalid = Number.isNaN(newValue);
|
||||
updateValue(newValue: number | undefined) {
|
||||
const nowValid = this.value !== undefined && this.isInteger ? Math.round(this.value) : this.value;
|
||||
let cleaned = newValue !== undefined ? newValue : nowValid;
|
||||
|
||||
let sanitized = newValue;
|
||||
if (invalid) sanitized = this.value;
|
||||
if (this.isInteger) sanitized = Math.round(sanitized);
|
||||
if (typeof this.min === "number" && !Number.isNaN(this.min)) sanitized = Math.max(sanitized, this.min);
|
||||
if (typeof this.max === "number" && !Number.isNaN(this.max)) sanitized = Math.min(sanitized, this.max);
|
||||
if (typeof this.min === "number" && !Number.isNaN(this.min) && cleaned !== undefined) cleaned = Math.max(cleaned, this.min);
|
||||
if (typeof this.max === "number" && !Number.isNaN(this.max) && cleaned !== undefined) cleaned = Math.min(cleaned, this.max);
|
||||
|
||||
if (!invalid) this.$emit("update:value", sanitized);
|
||||
if (newValue !== undefined) this.$emit("update:value", cleaned);
|
||||
|
||||
this.text = this.displayText(sanitized);
|
||||
this.text = this.displayText(cleaned);
|
||||
},
|
||||
displayText(value: number): string {
|
||||
displayText(value: number | undefined): string {
|
||||
if (value === undefined) return "-";
|
||||
|
||||
// Find the amount of digits on the left side of the decimal
|
||||
// 10.25 == 2
|
||||
// 1.23 == 1
|
||||
|
@ -199,8 +201,8 @@ export default defineComponent({
|
|||
},
|
||||
watch: {
|
||||
// Called only when `value` is changed from outside this component (with v-model)
|
||||
value(newValue: number) {
|
||||
if (Number.isNaN(newValue)) {
|
||||
value(newValue: number | undefined) {
|
||||
if (newValue === undefined) {
|
||||
this.text = "-";
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
<template>
|
||||
<LayoutRow class="shelf-item-input" :class="{ active: active }">
|
||||
<IconButton :action="action" :icon="icon" :size="32" />
|
||||
</LayoutRow>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.shelf-item-input {
|
||||
flex: 0 0 auto;
|
||||
border-radius: 2px;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-6-lowergray);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--color-accent);
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
background: unset;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "vue";
|
||||
|
||||
import { IconName } from "@/utilities/icons";
|
||||
|
||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
IconButton,
|
||||
LayoutRow,
|
||||
},
|
||||
props: {
|
||||
icon: { type: String as PropType<IconName>, required: true },
|
||||
action: { type: Function as PropType<(e?: MouseEvent) => void>, required: true },
|
||||
active: { type: Boolean as PropType<boolean>, default: false },
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -7,12 +7,14 @@
|
|||
<style lang="scss">
|
||||
.separator {
|
||||
&.vertical {
|
||||
flex: 0 0 auto;
|
||||
|
||||
&.related {
|
||||
margin-top: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
&.unrelated {
|
||||
margin-top: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&.section,
|
||||
|
@ -37,12 +39,14 @@
|
|||
}
|
||||
|
||||
&.horizontal {
|
||||
flex: 0 0 auto;
|
||||
|
||||
&.related {
|
||||
margin-left: 4px;
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&.unrelated {
|
||||
margin-left: 8px;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
&.section,
|
||||
|
|
|
@ -61,10 +61,6 @@ export default defineComponent({
|
|||
this.editor.dispatcher.subscribeJsMessage(UpdateInputHints, (updateInputHints) => {
|
||||
this.hintData = updateInputHints.hint_data;
|
||||
});
|
||||
|
||||
// Switch away from, and back to, the Select Tool to make it display the correct hints in the status bar
|
||||
this.editor.instance.select_tool("Path");
|
||||
this.editor.instance.select_tool("Select");
|
||||
},
|
||||
components: {
|
||||
UserInputLabel,
|
||||
|
|
|
@ -117,33 +117,6 @@ export class UpdateWorkingColors extends JsMessage {
|
|||
readonly secondary!: Color;
|
||||
}
|
||||
|
||||
export type ToolName =
|
||||
| "Select"
|
||||
| "Artboard"
|
||||
| "Navigate"
|
||||
| "Eyedropper"
|
||||
| "Text"
|
||||
| "Fill"
|
||||
| "Gradient"
|
||||
| "Brush"
|
||||
| "Heal"
|
||||
| "Clone"
|
||||
| "Patch"
|
||||
| "Detail"
|
||||
| "Relight"
|
||||
| "Path"
|
||||
| "Pen"
|
||||
| "Freehand"
|
||||
| "Spline"
|
||||
| "Line"
|
||||
| "Rectangle"
|
||||
| "Ellipse"
|
||||
| "Shape";
|
||||
|
||||
export class UpdateActiveTool extends JsMessage {
|
||||
readonly tool_name!: ToolName;
|
||||
}
|
||||
|
||||
export class UpdateActiveDocument extends JsMessage {
|
||||
readonly document_id!: BigInt;
|
||||
}
|
||||
|
@ -320,48 +293,16 @@ export class UpdateImageData extends JsMessage {
|
|||
|
||||
export class DisplayRemoveEditableTextbox extends JsMessage {}
|
||||
|
||||
export class UpdateDocumentLayer extends JsMessage {
|
||||
export class UpdateDocumentLayerDetails extends JsMessage {
|
||||
@Type(() => LayerPanelEntry)
|
||||
readonly data!: LayerPanelEntry;
|
||||
}
|
||||
|
||||
export class UpdateCanvasZoom extends JsMessage {
|
||||
readonly factor!: number;
|
||||
}
|
||||
|
||||
export class UpdateCanvasRotation extends JsMessage {
|
||||
readonly angle_radians!: number;
|
||||
}
|
||||
|
||||
export type BlendMode =
|
||||
| "Normal"
|
||||
| "Multiply"
|
||||
| "Darken"
|
||||
| "ColorBurn"
|
||||
| "Screen"
|
||||
| "Lighten"
|
||||
| "ColorDodge"
|
||||
| "Overlay"
|
||||
| "SoftLight"
|
||||
| "HardLight"
|
||||
| "Difference"
|
||||
| "Exclusion"
|
||||
| "Hue"
|
||||
| "Saturation"
|
||||
| "Color"
|
||||
| "Luminosity";
|
||||
|
||||
export class LayerPanelEntry {
|
||||
name!: string;
|
||||
|
||||
visible!: boolean;
|
||||
|
||||
blend_mode!: BlendMode;
|
||||
|
||||
// On the rust side opacity is out of 1 rather than 100
|
||||
@Transform(({ value }) => value * 100)
|
||||
opacity!: number;
|
||||
|
||||
layer_type!: LayerType;
|
||||
|
||||
@Transform(({ value }) => new BigUint64Array(value))
|
||||
|
@ -433,15 +374,21 @@ export function defaultWidgetLayout(): WidgetLayout {
|
|||
};
|
||||
}
|
||||
|
||||
export type LayoutRow = WidgetRow | WidgetSection;
|
||||
// TODO: Rename LayoutRow to something more generic
|
||||
export type LayoutRow = WidgetRow | WidgetColumn | WidgetSection;
|
||||
|
||||
export type WidgetRow = { widgets: Widget[] };
|
||||
export function isWidgetRow(layoutRow: WidgetRow | WidgetSection): layoutRow is WidgetRow {
|
||||
return Boolean((layoutRow as WidgetRow).widgets);
|
||||
export type WidgetColumn = { columnWidgets: Widget[] };
|
||||
export function isWidgetColumn(layoutColumn: LayoutRow): layoutColumn is WidgetColumn {
|
||||
return Boolean((layoutColumn as WidgetColumn).columnWidgets);
|
||||
}
|
||||
|
||||
export type WidgetRow = { rowWidgets: Widget[] };
|
||||
export function isWidgetRow(layoutRow: LayoutRow): layoutRow is WidgetRow {
|
||||
return Boolean((layoutRow as WidgetRow).rowWidgets);
|
||||
}
|
||||
|
||||
export type WidgetSection = { name: string; layout: LayoutRow[] };
|
||||
export function isWidgetSection(layoutRow: WidgetRow | WidgetSection): layoutRow is WidgetSection {
|
||||
export function isWidgetSection(layoutRow: LayoutRow): layoutRow is WidgetSection {
|
||||
return Boolean((layoutRow as WidgetSection).layout);
|
||||
}
|
||||
|
||||
|
@ -476,6 +423,13 @@ export class UpdateDialogDetails extends JsMessage implements WidgetLayout {
|
|||
layout!: LayoutRow[];
|
||||
}
|
||||
|
||||
export class UpdateDocumentModeLayout extends JsMessage implements WidgetLayout {
|
||||
layout_target!: unknown;
|
||||
|
||||
@Transform(({ value }) => createWidgetLayout(value))
|
||||
layout!: LayoutRow[];
|
||||
}
|
||||
|
||||
export class UpdateToolOptionsLayout extends JsMessage implements WidgetLayout {
|
||||
layout_target!: unknown;
|
||||
|
||||
|
@ -490,6 +444,13 @@ export class UpdateDocumentBarLayout extends JsMessage implements WidgetLayout {
|
|||
layout!: LayoutRow[];
|
||||
}
|
||||
|
||||
export class UpdateToolShelfLayout extends JsMessage implements WidgetLayout {
|
||||
layout_target!: unknown;
|
||||
|
||||
@Transform(({ value }) => createWidgetLayout(value))
|
||||
layout!: LayoutRow[];
|
||||
}
|
||||
|
||||
export class UpdatePropertyPanelOptionsLayout extends JsMessage implements WidgetLayout {
|
||||
layout_target!: unknown;
|
||||
|
||||
|
@ -504,13 +465,20 @@ export class UpdatePropertyPanelSectionsLayout extends JsMessage implements Widg
|
|||
layout!: LayoutRow[];
|
||||
}
|
||||
|
||||
export class UpdateLayerTreeOptionsLayout extends JsMessage implements WidgetLayout {
|
||||
layout_target!: unknown;
|
||||
|
||||
@Transform(({ value }) => createWidgetLayout(value))
|
||||
layout!: LayoutRow[];
|
||||
}
|
||||
|
||||
// Unpacking rust types to more usable type in the frontend
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function createWidgetLayout(widgetLayout: any[]): LayoutRow[] {
|
||||
return widgetLayout.map((rowOrSection): LayoutRow => {
|
||||
if (rowOrSection.Row) {
|
||||
return widgetLayout.map((layoutType): LayoutRow => {
|
||||
if (layoutType.Column) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const widgets = rowOrSection.Row.widgets.map((widgetHolder: any) => {
|
||||
const columnWidgets = layoutType.Column.columnWidgets.map((widgetHolder: any) => {
|
||||
const { widget_id } = widgetHolder;
|
||||
const kind = Object.keys(widgetHolder.widget)[0];
|
||||
const props = widgetHolder.widget[kind];
|
||||
|
@ -518,13 +486,27 @@ function createWidgetLayout(widgetLayout: any[]): LayoutRow[] {
|
|||
return { widget_id, kind, props };
|
||||
});
|
||||
|
||||
const result: WidgetRow = { widgets };
|
||||
const result: WidgetColumn = { columnWidgets };
|
||||
return result;
|
||||
}
|
||||
|
||||
if (rowOrSection.Section) {
|
||||
const { name } = rowOrSection.Section;
|
||||
const layout = createWidgetLayout(rowOrSection.Section.layout);
|
||||
if (layoutType.Row) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const rowWidgets = layoutType.Row.rowWidgets.map((widgetHolder: any) => {
|
||||
const { widget_id } = widgetHolder;
|
||||
const kind = Object.keys(widgetHolder.widget)[0];
|
||||
const props = widgetHolder.widget[kind];
|
||||
|
||||
return { widget_id, kind, props };
|
||||
});
|
||||
|
||||
const result: WidgetRow = { rowWidgets };
|
||||
return result;
|
||||
}
|
||||
|
||||
if (layoutType.Section) {
|
||||
const { name } = layoutType.Section;
|
||||
const layout = createWidgetLayout(layoutType.Section.layout);
|
||||
|
||||
const result: WidgetSection = { name, layout };
|
||||
return result;
|
||||
|
@ -567,14 +549,12 @@ export const messageMakers: Record<string, MessageMaker> = {
|
|||
TriggerViewportResize,
|
||||
TriggerVisitLink,
|
||||
UpdateActiveDocument,
|
||||
UpdateActiveTool,
|
||||
UpdateCanvasRotation,
|
||||
UpdateCanvasZoom,
|
||||
UpdateDialogDetails,
|
||||
UpdateDocumentArtboards,
|
||||
UpdateDocumentArtwork,
|
||||
UpdateDocumentBarLayout,
|
||||
UpdateDocumentLayer,
|
||||
UpdateToolShelfLayout,
|
||||
UpdateDocumentLayerDetails,
|
||||
UpdateDocumentOverlays,
|
||||
UpdateDocumentRulers,
|
||||
UpdateDocumentScrollbars,
|
||||
|
@ -584,6 +564,8 @@ export const messageMakers: Record<string, MessageMaker> = {
|
|||
UpdateOpenDocumentsList,
|
||||
UpdatePropertyPanelOptionsLayout,
|
||||
UpdatePropertyPanelSectionsLayout,
|
||||
UpdateLayerTreeOptionsLayout,
|
||||
UpdateDocumentModeLayout,
|
||||
UpdateToolOptionsLayout,
|
||||
UpdateWorkingColors,
|
||||
} as const;
|
||||
|
|
|
@ -24,7 +24,7 @@ function preparePanicDialog(dialogState: DialogState, title: string, details: st
|
|||
const widgets: WidgetLayout = {
|
||||
layout: [
|
||||
{
|
||||
widgets: [
|
||||
rowWidgets: [
|
||||
{
|
||||
kind: "TextLabel",
|
||||
props: { value: title, bold: true },
|
||||
|
@ -34,7 +34,7 @@ function preparePanicDialog(dialogState: DialogState, title: string, details: st
|
|||
],
|
||||
},
|
||||
{
|
||||
widgets: [
|
||||
rowWidgets: [
|
||||
{
|
||||
kind: "TextLabel",
|
||||
props: { value: details, multiline: true },
|
||||
|
|
|
@ -12,7 +12,7 @@ export async function initWasm(): Promise<void> {
|
|||
// Skip if the wasm module is already initialized
|
||||
if (wasmImport !== null) return;
|
||||
|
||||
// Separating in two lines satisfies typescript when used below
|
||||
// Separating in two lines satisfies TypeScript
|
||||
const importedWasm = await import("@/../wasm/pkg").then(panicProxy);
|
||||
wasmImport = importedWasm;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue