mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-23 21:58:12 +08:00
Move JS script to the top of each Vue file
This commit is contained in:
Vendored
-35
@@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
// Place your Graphite workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
|
|
||||||
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
|
|
||||||
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
|
|
||||||
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
|
||||||
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
|
|
||||||
// Placeholders with the same ids are connected.
|
|
||||||
"Vue Component Template": {
|
|
||||||
"prefix": "vuecomp",
|
|
||||||
"body": [
|
|
||||||
"<template>",
|
|
||||||
"\t<${1:div}>",
|
|
||||||
"\t\t$0",
|
|
||||||
"\t</${1:div}>",
|
|
||||||
"</template>",
|
|
||||||
"",
|
|
||||||
"<style lang=\"scss\">",
|
|
||||||
"",
|
|
||||||
"</style>",
|
|
||||||
"",
|
|
||||||
"<script lang=\"ts\">",
|
|
||||||
"import { defineComponent } from \"vue\";",
|
|
||||||
"",
|
|
||||||
"export default defineComponent({",
|
|
||||||
"\tprops: {",
|
|
||||||
"\t},",
|
|
||||||
"\tcomponents: {",
|
|
||||||
"\t},",
|
|
||||||
"});",
|
|
||||||
"</script>",
|
|
||||||
"",
|
|
||||||
],
|
|
||||||
"description": "Template for a new Vue component file"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+101
-101
@@ -1,3 +1,104 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
|
import { createClipboardManager } from "@/io-managers/clipboard";
|
||||||
|
import { createDragManager } from "@/io-managers/drag";
|
||||||
|
import { createHyperlinkManager } from "@/io-managers/hyperlinks";
|
||||||
|
import { createInputManager } from "@/io-managers/input";
|
||||||
|
import { createLocalizationManager } from "@/io-managers/localization";
|
||||||
|
import { createPanicManager } from "@/io-managers/panic";
|
||||||
|
import { createPersistenceManager } from "@/io-managers/persistence";
|
||||||
|
import { createDialogState, type DialogState } from "@/state-providers/dialog";
|
||||||
|
import { createDocumentState, type DocumentState } from "@/state-providers/document";
|
||||||
|
import { createFontsState, type FontsState } from "@/state-providers/fonts";
|
||||||
|
import { createFullscreenState, type FullscreenState } from "@/state-providers/fullscreen";
|
||||||
|
import { createNodeGraphState, type NodeGraphState } from "@/state-providers/node-graph";
|
||||||
|
import { createPanelsState, type PanelsState } from "@/state-providers/panels";
|
||||||
|
import { createPortfolioState, type PortfolioState } from "@/state-providers/portfolio";
|
||||||
|
import { createWorkspaceState, type WorkspaceState } from "@/state-providers/workspace";
|
||||||
|
import { operatingSystem } from "@/utility-functions/platform";
|
||||||
|
import { createEditor, type Editor } from "@/wasm-communication/editor";
|
||||||
|
|
||||||
|
import MainWindow from "@/components/window/MainWindow.vue";
|
||||||
|
|
||||||
|
const managerDestructors: {
|
||||||
|
createClipboardManager?: () => void;
|
||||||
|
createDragManager?: () => void;
|
||||||
|
createHyperlinkManager?: () => void;
|
||||||
|
createInputManager?: () => void;
|
||||||
|
createLocalizationManager?: () => void;
|
||||||
|
createPanicManager?: () => void;
|
||||||
|
createPersistenceManager?: () => void;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
// Vue injects don't play well with TypeScript (all injects will show up as `any`) but we can define these types as a solution
|
||||||
|
declare module "@vue/runtime-core" {
|
||||||
|
// Systems `provide`d by the root App to be `inject`ed into descendant components and used for reactive bindings
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||||
|
interface ComponentCustomProperties {
|
||||||
|
// Graphite WASM editor instance
|
||||||
|
editor: Editor;
|
||||||
|
|
||||||
|
// State provider systems
|
||||||
|
dialog: DialogState;
|
||||||
|
fonts: FontsState;
|
||||||
|
fullscreen: FullscreenState;
|
||||||
|
panels: PanelsState;
|
||||||
|
portfolio: PortfolioState;
|
||||||
|
workspace: WorkspaceState;
|
||||||
|
nodeGraph: NodeGraphState;
|
||||||
|
document: DocumentState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
provide() {
|
||||||
|
return { ...this.$data };
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
const editor = createEditor();
|
||||||
|
return {
|
||||||
|
// Graphite WASM editor instance
|
||||||
|
editor,
|
||||||
|
|
||||||
|
// State provider systems
|
||||||
|
dialog: createDialogState(editor),
|
||||||
|
fonts: createFontsState(editor),
|
||||||
|
fullscreen: createFullscreenState(editor),
|
||||||
|
panels: createPanelsState(editor),
|
||||||
|
portfolio: createPortfolioState(editor),
|
||||||
|
workspace: createWorkspaceState(editor),
|
||||||
|
nodeGraph: createNodeGraphState(editor),
|
||||||
|
document: createDocumentState(editor),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// Initialize managers, which are isolated systems that subscribe to backend messages to link them to browser API functionality (like JS events, IndexedDB, etc.)
|
||||||
|
Object.assign(managerDestructors, {
|
||||||
|
createClipboardManager: createClipboardManager(this.editor),
|
||||||
|
createDragManager: createDragManager(),
|
||||||
|
createHyperlinkManager: createHyperlinkManager(this.editor),
|
||||||
|
createInputManager: createInputManager(this.editor, this.$el.parentElement, this.dialog, this.portfolio, this.fullscreen),
|
||||||
|
createLocalizationManager: createLocalizationManager(this.editor),
|
||||||
|
createPanicManager: createPanicManager(this.editor, this.dialog),
|
||||||
|
createPersistenceManager: createPersistenceManager(this.editor, this.portfolio),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready
|
||||||
|
const platform = operatingSystem();
|
||||||
|
this.editor.instance.initAfterFrontendReady(platform);
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
// Call the destructor for each manager
|
||||||
|
Object.values(managerDestructors).forEach((destructor) => destructor?.());
|
||||||
|
|
||||||
|
// Destroy the WASM editor instance
|
||||||
|
this.editor.instance.free();
|
||||||
|
},
|
||||||
|
components: { MainWindow },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MainWindow />
|
<MainWindow />
|
||||||
</template>
|
</template>
|
||||||
@@ -232,104 +333,3 @@ img {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
import { createClipboardManager } from "@/io-managers/clipboard";
|
|
||||||
import { createDragManager } from "@/io-managers/drag";
|
|
||||||
import { createHyperlinkManager } from "@/io-managers/hyperlinks";
|
|
||||||
import { createInputManager } from "@/io-managers/input";
|
|
||||||
import { createLocalizationManager } from "@/io-managers/localization";
|
|
||||||
import { createPanicManager } from "@/io-managers/panic";
|
|
||||||
import { createPersistenceManager } from "@/io-managers/persistence";
|
|
||||||
import { createDialogState, type DialogState } from "@/state-providers/dialog";
|
|
||||||
import { createDocumentState, type DocumentState } from "@/state-providers/document";
|
|
||||||
import { createFontsState, type FontsState } from "@/state-providers/fonts";
|
|
||||||
import { createFullscreenState, type FullscreenState } from "@/state-providers/fullscreen";
|
|
||||||
import { createNodeGraphState, type NodeGraphState } from "@/state-providers/node-graph";
|
|
||||||
import { createPanelsState, type PanelsState } from "@/state-providers/panels";
|
|
||||||
import { createPortfolioState, type PortfolioState } from "@/state-providers/portfolio";
|
|
||||||
import { createWorkspaceState, type WorkspaceState } from "@/state-providers/workspace";
|
|
||||||
import { operatingSystem } from "@/utility-functions/platform";
|
|
||||||
import { createEditor, type Editor } from "@/wasm-communication/editor";
|
|
||||||
|
|
||||||
import MainWindow from "@/components/window/MainWindow.vue";
|
|
||||||
|
|
||||||
const managerDestructors: {
|
|
||||||
createClipboardManager?: () => void;
|
|
||||||
createDragManager?: () => void;
|
|
||||||
createHyperlinkManager?: () => void;
|
|
||||||
createInputManager?: () => void;
|
|
||||||
createLocalizationManager?: () => void;
|
|
||||||
createPanicManager?: () => void;
|
|
||||||
createPersistenceManager?: () => void;
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
// Vue injects don't play well with TypeScript (all injects will show up as `any`) but we can define these types as a solution
|
|
||||||
declare module "@vue/runtime-core" {
|
|
||||||
// Systems `provide`d by the root App to be `inject`ed into descendant components and used for reactive bindings
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
|
||||||
interface ComponentCustomProperties {
|
|
||||||
// Graphite WASM editor instance
|
|
||||||
editor: Editor;
|
|
||||||
|
|
||||||
// State provider systems
|
|
||||||
dialog: DialogState;
|
|
||||||
fonts: FontsState;
|
|
||||||
fullscreen: FullscreenState;
|
|
||||||
panels: PanelsState;
|
|
||||||
portfolio: PortfolioState;
|
|
||||||
workspace: WorkspaceState;
|
|
||||||
nodeGraph: NodeGraphState;
|
|
||||||
document: DocumentState;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
provide() {
|
|
||||||
return { ...this.$data };
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
const editor = createEditor();
|
|
||||||
return {
|
|
||||||
// Graphite WASM editor instance
|
|
||||||
editor,
|
|
||||||
|
|
||||||
// State provider systems
|
|
||||||
dialog: createDialogState(editor),
|
|
||||||
fonts: createFontsState(editor),
|
|
||||||
fullscreen: createFullscreenState(editor),
|
|
||||||
panels: createPanelsState(editor),
|
|
||||||
portfolio: createPortfolioState(editor),
|
|
||||||
workspace: createWorkspaceState(editor),
|
|
||||||
nodeGraph: createNodeGraphState(editor),
|
|
||||||
document: createDocumentState(editor),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
// Initialize managers, which are isolated systems that subscribe to backend messages to link them to browser API functionality (like JS events, IndexedDB, etc.)
|
|
||||||
Object.assign(managerDestructors, {
|
|
||||||
createClipboardManager: createClipboardManager(this.editor),
|
|
||||||
createDragManager: createDragManager(),
|
|
||||||
createHyperlinkManager: createHyperlinkManager(this.editor),
|
|
||||||
createInputManager: createInputManager(this.editor, this.$el.parentElement, this.dialog, this.portfolio, this.fullscreen),
|
|
||||||
createLocalizationManager: createLocalizationManager(this.editor),
|
|
||||||
createPanicManager: createPanicManager(this.editor, this.dialog),
|
|
||||||
createPersistenceManager: createPersistenceManager(this.editor, this.portfolio),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize certain setup tasks required by the editor backend to be ready for the user now that the frontend is ready
|
|
||||||
const platform = operatingSystem();
|
|
||||||
this.editor.instance.initAfterFrontendReady(platform);
|
|
||||||
},
|
|
||||||
beforeUnmount() {
|
|
||||||
// Call the destructor for each manager
|
|
||||||
Object.values(managerDestructors).forEach((destructor) => destructor?.());
|
|
||||||
|
|
||||||
// Destroy the WASM editor instance
|
|
||||||
this.editor.instance.free();
|
|
||||||
},
|
|
||||||
components: { MainWindow },
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,268 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
import { clamp } from "@/utility-functions/math";
|
||||||
|
import type { HSV, RGB } from "@/wasm-communication/messages";
|
||||||
|
import { Color } from "@/wasm-communication/messages";
|
||||||
|
|
||||||
|
import FloatingMenu, { type MenuDirection } from "@/components/layout/FloatingMenu.vue";
|
||||||
|
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||||
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
|
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
||||||
|
import DropdownInput from "@/components/widgets/inputs/DropdownInput.vue";
|
||||||
|
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
|
||||||
|
import TextInput from "@/components/widgets/inputs/TextInput.vue";
|
||||||
|
import Separator from "@/components/widgets/labels/Separator.vue";
|
||||||
|
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||||
|
|
||||||
|
type PresetColors = "none" | "black" | "white" | "red" | "yellow" | "green" | "cyan" | "blue" | "magenta";
|
||||||
|
|
||||||
|
const PURE_COLORS: Record<PresetColors, [number, number, number]> = {
|
||||||
|
none: [0, 0, 0],
|
||||||
|
black: [0, 0, 0],
|
||||||
|
white: [1, 1, 1],
|
||||||
|
red: [1, 0, 0],
|
||||||
|
yellow: [1, 1, 0],
|
||||||
|
green: [0, 1, 0],
|
||||||
|
cyan: [0, 1, 1],
|
||||||
|
blue: [0, 0, 1],
|
||||||
|
magenta: [1, 0, 1],
|
||||||
|
};
|
||||||
|
|
||||||
|
const COLOR_SPACE_CHOICES = [[{ label: "sRGB" }]];
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
inject: ["editor"],
|
||||||
|
emits: ["update:color", "update:open"],
|
||||||
|
props: {
|
||||||
|
color: { type: Object as PropType<Color>, required: true },
|
||||||
|
allowNone: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
allowTransparency: { type: Boolean as PropType<boolean>, default: false }, // TODO: Implement this
|
||||||
|
direction: { type: String as PropType<MenuDirection>, default: "Bottom" },
|
||||||
|
// TODO: See if this should be made to follow the pattern of DropdownInput.vue so this could be removed
|
||||||
|
open: { type: Boolean as PropType<boolean>, required: true },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
const hsvaOrNone = this.color.toHSVA();
|
||||||
|
const hsva = hsvaOrNone || { h: 0, s: 0, v: 0, a: 1 };
|
||||||
|
|
||||||
|
return {
|
||||||
|
hue: hsva.h,
|
||||||
|
saturation: hsva.s,
|
||||||
|
value: hsva.v,
|
||||||
|
alpha: hsva.a,
|
||||||
|
isNone: hsvaOrNone === undefined,
|
||||||
|
initialHue: hsva.h,
|
||||||
|
initialSaturation: hsva.s,
|
||||||
|
initialValue: hsva.v,
|
||||||
|
initialAlpha: hsva.a,
|
||||||
|
initialIsNone: hsvaOrNone === undefined,
|
||||||
|
draggingPickerTrack: undefined as HTMLDivElement | undefined,
|
||||||
|
colorSpaceChoices: COLOR_SPACE_CHOICES,
|
||||||
|
strayCloses: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
opaqueHueColor(): Color {
|
||||||
|
return new Color({ h: this.hue, s: 1, v: 1, a: 1 });
|
||||||
|
},
|
||||||
|
newColor(): Color {
|
||||||
|
if (this.isNone) return new Color("none");
|
||||||
|
return new Color({ h: this.hue, s: this.saturation, v: this.value, a: this.alpha });
|
||||||
|
},
|
||||||
|
initialColor(): Color {
|
||||||
|
if (this.initialIsNone) return new Color("none");
|
||||||
|
return new Color({ h: this.initialHue, s: this.initialSaturation, v: this.initialValue, a: this.initialAlpha });
|
||||||
|
},
|
||||||
|
black(): Color {
|
||||||
|
return new Color(0, 0, 0, 1);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// Called only when `open` is changed from outside this component (with v-model)
|
||||||
|
open(isOpen: boolean) {
|
||||||
|
if (isOpen) this.setInitialHSVA(this.hue, this.saturation, this.value, this.alpha, this.isNone);
|
||||||
|
},
|
||||||
|
// Called only when `color` is changed from outside this component (with v-model)
|
||||||
|
color(color: Color) {
|
||||||
|
const hsva = color.toHSVA();
|
||||||
|
|
||||||
|
if (hsva !== undefined) {
|
||||||
|
// Update the hue, but only if it is necessary so we don't:
|
||||||
|
// - ...jump the user's hue from 360° (top) to the equivalent 0° (bottom)
|
||||||
|
// - ...reset the hue to 0° if the color is fully desaturated, where all hues are equivalent
|
||||||
|
// - ...reset the hue to 0° if the color's value is black, where all hues are equivalent
|
||||||
|
if (!(hsva.h === 0 && this.hue === 1) && hsva.s > 0 && hsva.v > 0) this.hue = hsva.h;
|
||||||
|
// Update the saturation, but only if it is necessary so we don't:
|
||||||
|
// - ...reset the saturation to the left is the color's value is black along the bottom edge, where all saturations are equivalent
|
||||||
|
if (hsva.v !== 0) this.saturation = hsva.s;
|
||||||
|
// Update the value
|
||||||
|
this.value = hsva.v;
|
||||||
|
// Update the alpha
|
||||||
|
this.alpha = hsva.a;
|
||||||
|
// Update the status of this not being a color
|
||||||
|
this.isNone = false;
|
||||||
|
} else {
|
||||||
|
this.setNewHSVA(0, 0, 0, 1, true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onPointerDown(e: PointerEvent) {
|
||||||
|
const target = (e.target || undefined) as HTMLElement | undefined;
|
||||||
|
this.draggingPickerTrack = target?.closest("[data-saturation-value-picker], [data-hue-picker], [data-alpha-picker]") || undefined;
|
||||||
|
|
||||||
|
this.addEvents();
|
||||||
|
|
||||||
|
this.onPointerMove(e);
|
||||||
|
},
|
||||||
|
onPointerMove(e: PointerEvent) {
|
||||||
|
// Just in case the mouseup event is lost
|
||||||
|
if (e.buttons === 0) this.removeEvents();
|
||||||
|
|
||||||
|
if (this.draggingPickerTrack?.hasAttribute("data-saturation-value-picker")) {
|
||||||
|
const rectangle = this.draggingPickerTrack.getBoundingClientRect();
|
||||||
|
|
||||||
|
this.saturation = clamp((e.clientX - rectangle.left) / rectangle.width, 0, 1);
|
||||||
|
this.value = clamp(1 - (e.clientY - rectangle.top) / rectangle.height, 0, 1);
|
||||||
|
this.strayCloses = false;
|
||||||
|
} else if (this.draggingPickerTrack?.hasAttribute("data-hue-picker")) {
|
||||||
|
const rectangle = this.draggingPickerTrack.getBoundingClientRect();
|
||||||
|
|
||||||
|
this.hue = clamp(1 - (e.clientY - rectangle.top) / rectangle.height, 0, 1);
|
||||||
|
this.strayCloses = false;
|
||||||
|
} else if (this.draggingPickerTrack?.hasAttribute("data-alpha-picker")) {
|
||||||
|
const rectangle = this.draggingPickerTrack.getBoundingClientRect();
|
||||||
|
|
||||||
|
this.alpha = clamp(1 - (e.clientY - rectangle.top) / rectangle.height, 0, 1);
|
||||||
|
this.strayCloses = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const color = new Color({ h: this.hue, s: this.saturation, v: this.value, a: this.alpha });
|
||||||
|
this.setColor(color);
|
||||||
|
},
|
||||||
|
onPointerUp() {
|
||||||
|
this.removeEvents();
|
||||||
|
},
|
||||||
|
addEvents() {
|
||||||
|
document.addEventListener("pointermove", this.onPointerMove);
|
||||||
|
document.addEventListener("pointerup", this.onPointerUp);
|
||||||
|
},
|
||||||
|
removeEvents() {
|
||||||
|
this.draggingPickerTrack = undefined;
|
||||||
|
this.strayCloses = true;
|
||||||
|
|
||||||
|
document.removeEventListener("pointermove", this.onPointerMove);
|
||||||
|
document.removeEventListener("pointerup", this.onPointerUp);
|
||||||
|
},
|
||||||
|
emitOpenState(isOpen: boolean) {
|
||||||
|
this.$emit("update:open", isOpen);
|
||||||
|
},
|
||||||
|
setColor(color?: Color) {
|
||||||
|
const colorToEmit = color || new Color({ h: this.hue, s: this.saturation, v: this.value, a: this.alpha });
|
||||||
|
this.$emit("update:color", colorToEmit);
|
||||||
|
},
|
||||||
|
swapNewWithInitial() {
|
||||||
|
const initial = this.initialColor;
|
||||||
|
|
||||||
|
const tempHue = this.hue;
|
||||||
|
const tempSaturation = this.saturation;
|
||||||
|
const tempValue = this.value;
|
||||||
|
const tempAlpha = this.alpha;
|
||||||
|
const tempIsNone = this.isNone;
|
||||||
|
|
||||||
|
this.setNewHSVA(this.initialHue, this.initialSaturation, this.initialValue, this.initialAlpha, this.initialIsNone);
|
||||||
|
this.setInitialHSVA(tempHue, tempSaturation, tempValue, tempAlpha, tempIsNone);
|
||||||
|
|
||||||
|
this.setColor(initial);
|
||||||
|
},
|
||||||
|
setColorCode(colorCode: string) {
|
||||||
|
const color = Color.fromCSS(colorCode);
|
||||||
|
if (color) this.setColor(color);
|
||||||
|
},
|
||||||
|
setColorRGB(channel: keyof RGB, strength: number) {
|
||||||
|
if (channel === "r") this.setColor(new Color(strength / 255, this.newColor.green, this.newColor.blue, this.newColor.alpha));
|
||||||
|
else if (channel === "g") this.setColor(new Color(this.newColor.red, strength / 255, this.newColor.blue, this.newColor.alpha));
|
||||||
|
else if (channel === "b") this.setColor(new Color(this.newColor.red, this.newColor.green, strength / 255, this.newColor.alpha));
|
||||||
|
},
|
||||||
|
setColorHSV(channel: keyof HSV, strength: number) {
|
||||||
|
if (channel === "h") this.hue = strength / 360;
|
||||||
|
else if (channel === "s") this.saturation = strength / 100;
|
||||||
|
else if (channel === "v") this.value = strength / 100;
|
||||||
|
|
||||||
|
this.setColor();
|
||||||
|
},
|
||||||
|
setColorAlphaPercent(alpha: number) {
|
||||||
|
this.alpha = alpha / 100;
|
||||||
|
this.setColor();
|
||||||
|
},
|
||||||
|
setColorPresetSubtile(e: MouseEvent) {
|
||||||
|
const clickedTile = e.target as HTMLDivElement | undefined;
|
||||||
|
const tileColor = clickedTile?.getAttribute("data-pure-tile") || undefined;
|
||||||
|
|
||||||
|
if (tileColor) this.setColorPreset(tileColor as PresetColors);
|
||||||
|
},
|
||||||
|
setColorPreset(preset: PresetColors) {
|
||||||
|
if (preset === "none") {
|
||||||
|
this.setNewHSVA(0, 0, 0, 1, true);
|
||||||
|
this.setColor(new Color("none"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const presetColor = new Color(...PURE_COLORS[preset], 1);
|
||||||
|
const hsva = presetColor.toHSVA() || { h: 0, s: 0, v: 0, a: 0 };
|
||||||
|
|
||||||
|
this.setNewHSVA(hsva.h, hsva.s, hsva.v, hsva.a, false);
|
||||||
|
this.setColor(presetColor);
|
||||||
|
},
|
||||||
|
setNewHSVA(hue: number, saturation: number, value: number, alpha: number, isNone: boolean) {
|
||||||
|
this.hue = hue;
|
||||||
|
this.saturation = saturation;
|
||||||
|
this.value = value;
|
||||||
|
this.alpha = alpha;
|
||||||
|
this.isNone = isNone;
|
||||||
|
},
|
||||||
|
setInitialHSVA(hue: number, saturation: number, value: number, alpha: number, isNone: boolean) {
|
||||||
|
this.initialHue = hue;
|
||||||
|
this.initialSaturation = saturation;
|
||||||
|
this.initialValue = value;
|
||||||
|
this.initialAlpha = alpha;
|
||||||
|
this.initialIsNone = isNone;
|
||||||
|
},
|
||||||
|
async activateEyedropperSample() {
|
||||||
|
// TODO: Replace this temporary solution that only works in Chromium-based browsers with the custom color sampler used by the Eyedropper tool
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
if (!(window as any).EyeDropper) {
|
||||||
|
this.editor.instance.eyedropperSampleForColorPicker();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const result = await new (window as any).EyeDropper().open();
|
||||||
|
this.setColorCode(result.sRGBHex);
|
||||||
|
} catch {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
unmounted() {
|
||||||
|
this.removeEvents();
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
DropdownInput,
|
||||||
|
FloatingMenu,
|
||||||
|
IconButton,
|
||||||
|
LayoutCol,
|
||||||
|
LayoutRow,
|
||||||
|
NumberInput,
|
||||||
|
Separator,
|
||||||
|
TextInput,
|
||||||
|
TextLabel,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FloatingMenu class="color-picker" :open="open" @update:open="(isOpen) => emitOpenState(isOpen)" :strayCloses="strayCloses" :direction="direction" :type="'Popover'">
|
<FloatingMenu class="color-picker" :open="open" @update:open="(isOpen) => emitOpenState(isOpen)" :strayCloses="strayCloses" :direction="direction" :type="'Popover'">
|
||||||
<LayoutRow
|
<LayoutRow
|
||||||
@@ -354,268 +619,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
import { clamp } from "@/utility-functions/math";
|
|
||||||
import type { HSV, RGB } from "@/wasm-communication/messages";
|
|
||||||
import { Color } from "@/wasm-communication/messages";
|
|
||||||
|
|
||||||
import FloatingMenu, { type MenuDirection } from "@/components/layout/FloatingMenu.vue";
|
|
||||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|
||||||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
|
||||||
import DropdownInput from "@/components/widgets/inputs/DropdownInput.vue";
|
|
||||||
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
|
|
||||||
import TextInput from "@/components/widgets/inputs/TextInput.vue";
|
|
||||||
import Separator from "@/components/widgets/labels/Separator.vue";
|
|
||||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
|
||||||
|
|
||||||
type PresetColors = "none" | "black" | "white" | "red" | "yellow" | "green" | "cyan" | "blue" | "magenta";
|
|
||||||
|
|
||||||
const PURE_COLORS: Record<PresetColors, [number, number, number]> = {
|
|
||||||
none: [0, 0, 0],
|
|
||||||
black: [0, 0, 0],
|
|
||||||
white: [1, 1, 1],
|
|
||||||
red: [1, 0, 0],
|
|
||||||
yellow: [1, 1, 0],
|
|
||||||
green: [0, 1, 0],
|
|
||||||
cyan: [0, 1, 1],
|
|
||||||
blue: [0, 0, 1],
|
|
||||||
magenta: [1, 0, 1],
|
|
||||||
};
|
|
||||||
|
|
||||||
const COLOR_SPACE_CHOICES = [[{ label: "sRGB" }]];
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
inject: ["editor"],
|
|
||||||
emits: ["update:color", "update:open"],
|
|
||||||
props: {
|
|
||||||
color: { type: Object as PropType<Color>, required: true },
|
|
||||||
allowNone: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
allowTransparency: { type: Boolean as PropType<boolean>, default: false }, // TODO: Implement this
|
|
||||||
direction: { type: String as PropType<MenuDirection>, default: "Bottom" },
|
|
||||||
// TODO: See if this should be made to follow the pattern of DropdownInput.vue so this could be removed
|
|
||||||
open: { type: Boolean as PropType<boolean>, required: true },
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
const hsvaOrNone = this.color.toHSVA();
|
|
||||||
const hsva = hsvaOrNone || { h: 0, s: 0, v: 0, a: 1 };
|
|
||||||
|
|
||||||
return {
|
|
||||||
hue: hsva.h,
|
|
||||||
saturation: hsva.s,
|
|
||||||
value: hsva.v,
|
|
||||||
alpha: hsva.a,
|
|
||||||
isNone: hsvaOrNone === undefined,
|
|
||||||
initialHue: hsva.h,
|
|
||||||
initialSaturation: hsva.s,
|
|
||||||
initialValue: hsva.v,
|
|
||||||
initialAlpha: hsva.a,
|
|
||||||
initialIsNone: hsvaOrNone === undefined,
|
|
||||||
draggingPickerTrack: undefined as HTMLDivElement | undefined,
|
|
||||||
colorSpaceChoices: COLOR_SPACE_CHOICES,
|
|
||||||
strayCloses: true,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
opaqueHueColor(): Color {
|
|
||||||
return new Color({ h: this.hue, s: 1, v: 1, a: 1 });
|
|
||||||
},
|
|
||||||
newColor(): Color {
|
|
||||||
if (this.isNone) return new Color("none");
|
|
||||||
return new Color({ h: this.hue, s: this.saturation, v: this.value, a: this.alpha });
|
|
||||||
},
|
|
||||||
initialColor(): Color {
|
|
||||||
if (this.initialIsNone) return new Color("none");
|
|
||||||
return new Color({ h: this.initialHue, s: this.initialSaturation, v: this.initialValue, a: this.initialAlpha });
|
|
||||||
},
|
|
||||||
black(): Color {
|
|
||||||
return new Color(0, 0, 0, 1);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
// Called only when `open` is changed from outside this component (with v-model)
|
|
||||||
open(isOpen: boolean) {
|
|
||||||
if (isOpen) this.setInitialHSVA(this.hue, this.saturation, this.value, this.alpha, this.isNone);
|
|
||||||
},
|
|
||||||
// Called only when `color` is changed from outside this component (with v-model)
|
|
||||||
color(color: Color) {
|
|
||||||
const hsva = color.toHSVA();
|
|
||||||
|
|
||||||
if (hsva !== undefined) {
|
|
||||||
// Update the hue, but only if it is necessary so we don't:
|
|
||||||
// - ...jump the user's hue from 360° (top) to the equivalent 0° (bottom)
|
|
||||||
// - ...reset the hue to 0° if the color is fully desaturated, where all hues are equivalent
|
|
||||||
// - ...reset the hue to 0° if the color's value is black, where all hues are equivalent
|
|
||||||
if (!(hsva.h === 0 && this.hue === 1) && hsva.s > 0 && hsva.v > 0) this.hue = hsva.h;
|
|
||||||
// Update the saturation, but only if it is necessary so we don't:
|
|
||||||
// - ...reset the saturation to the left is the color's value is black along the bottom edge, where all saturations are equivalent
|
|
||||||
if (hsva.v !== 0) this.saturation = hsva.s;
|
|
||||||
// Update the value
|
|
||||||
this.value = hsva.v;
|
|
||||||
// Update the alpha
|
|
||||||
this.alpha = hsva.a;
|
|
||||||
// Update the status of this not being a color
|
|
||||||
this.isNone = false;
|
|
||||||
} else {
|
|
||||||
this.setNewHSVA(0, 0, 0, 1, true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onPointerDown(e: PointerEvent) {
|
|
||||||
const target = (e.target || undefined) as HTMLElement | undefined;
|
|
||||||
this.draggingPickerTrack = target?.closest("[data-saturation-value-picker], [data-hue-picker], [data-alpha-picker]") || undefined;
|
|
||||||
|
|
||||||
this.addEvents();
|
|
||||||
|
|
||||||
this.onPointerMove(e);
|
|
||||||
},
|
|
||||||
onPointerMove(e: PointerEvent) {
|
|
||||||
// Just in case the mouseup event is lost
|
|
||||||
if (e.buttons === 0) this.removeEvents();
|
|
||||||
|
|
||||||
if (this.draggingPickerTrack?.hasAttribute("data-saturation-value-picker")) {
|
|
||||||
const rectangle = this.draggingPickerTrack.getBoundingClientRect();
|
|
||||||
|
|
||||||
this.saturation = clamp((e.clientX - rectangle.left) / rectangle.width, 0, 1);
|
|
||||||
this.value = clamp(1 - (e.clientY - rectangle.top) / rectangle.height, 0, 1);
|
|
||||||
this.strayCloses = false;
|
|
||||||
} else if (this.draggingPickerTrack?.hasAttribute("data-hue-picker")) {
|
|
||||||
const rectangle = this.draggingPickerTrack.getBoundingClientRect();
|
|
||||||
|
|
||||||
this.hue = clamp(1 - (e.clientY - rectangle.top) / rectangle.height, 0, 1);
|
|
||||||
this.strayCloses = false;
|
|
||||||
} else if (this.draggingPickerTrack?.hasAttribute("data-alpha-picker")) {
|
|
||||||
const rectangle = this.draggingPickerTrack.getBoundingClientRect();
|
|
||||||
|
|
||||||
this.alpha = clamp(1 - (e.clientY - rectangle.top) / rectangle.height, 0, 1);
|
|
||||||
this.strayCloses = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const color = new Color({ h: this.hue, s: this.saturation, v: this.value, a: this.alpha });
|
|
||||||
this.setColor(color);
|
|
||||||
},
|
|
||||||
onPointerUp() {
|
|
||||||
this.removeEvents();
|
|
||||||
},
|
|
||||||
addEvents() {
|
|
||||||
document.addEventListener("pointermove", this.onPointerMove);
|
|
||||||
document.addEventListener("pointerup", this.onPointerUp);
|
|
||||||
},
|
|
||||||
removeEvents() {
|
|
||||||
this.draggingPickerTrack = undefined;
|
|
||||||
this.strayCloses = true;
|
|
||||||
|
|
||||||
document.removeEventListener("pointermove", this.onPointerMove);
|
|
||||||
document.removeEventListener("pointerup", this.onPointerUp);
|
|
||||||
},
|
|
||||||
emitOpenState(isOpen: boolean) {
|
|
||||||
this.$emit("update:open", isOpen);
|
|
||||||
},
|
|
||||||
setColor(color?: Color) {
|
|
||||||
const colorToEmit = color || new Color({ h: this.hue, s: this.saturation, v: this.value, a: this.alpha });
|
|
||||||
this.$emit("update:color", colorToEmit);
|
|
||||||
},
|
|
||||||
swapNewWithInitial() {
|
|
||||||
const initial = this.initialColor;
|
|
||||||
|
|
||||||
const tempHue = this.hue;
|
|
||||||
const tempSaturation = this.saturation;
|
|
||||||
const tempValue = this.value;
|
|
||||||
const tempAlpha = this.alpha;
|
|
||||||
const tempIsNone = this.isNone;
|
|
||||||
|
|
||||||
this.setNewHSVA(this.initialHue, this.initialSaturation, this.initialValue, this.initialAlpha, this.initialIsNone);
|
|
||||||
this.setInitialHSVA(tempHue, tempSaturation, tempValue, tempAlpha, tempIsNone);
|
|
||||||
|
|
||||||
this.setColor(initial);
|
|
||||||
},
|
|
||||||
setColorCode(colorCode: string) {
|
|
||||||
const color = Color.fromCSS(colorCode);
|
|
||||||
if (color) this.setColor(color);
|
|
||||||
},
|
|
||||||
setColorRGB(channel: keyof RGB, strength: number) {
|
|
||||||
if (channel === "r") this.setColor(new Color(strength / 255, this.newColor.green, this.newColor.blue, this.newColor.alpha));
|
|
||||||
else if (channel === "g") this.setColor(new Color(this.newColor.red, strength / 255, this.newColor.blue, this.newColor.alpha));
|
|
||||||
else if (channel === "b") this.setColor(new Color(this.newColor.red, this.newColor.green, strength / 255, this.newColor.alpha));
|
|
||||||
},
|
|
||||||
setColorHSV(channel: keyof HSV, strength: number) {
|
|
||||||
if (channel === "h") this.hue = strength / 360;
|
|
||||||
else if (channel === "s") this.saturation = strength / 100;
|
|
||||||
else if (channel === "v") this.value = strength / 100;
|
|
||||||
|
|
||||||
this.setColor();
|
|
||||||
},
|
|
||||||
setColorAlphaPercent(alpha: number) {
|
|
||||||
this.alpha = alpha / 100;
|
|
||||||
this.setColor();
|
|
||||||
},
|
|
||||||
setColorPresetSubtile(e: MouseEvent) {
|
|
||||||
const clickedTile = e.target as HTMLDivElement | undefined;
|
|
||||||
const tileColor = clickedTile?.getAttribute("data-pure-tile") || undefined;
|
|
||||||
|
|
||||||
if (tileColor) this.setColorPreset(tileColor as PresetColors);
|
|
||||||
},
|
|
||||||
setColorPreset(preset: PresetColors) {
|
|
||||||
if (preset === "none") {
|
|
||||||
this.setNewHSVA(0, 0, 0, 1, true);
|
|
||||||
this.setColor(new Color("none"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const presetColor = new Color(...PURE_COLORS[preset], 1);
|
|
||||||
const hsva = presetColor.toHSVA() || { h: 0, s: 0, v: 0, a: 0 };
|
|
||||||
|
|
||||||
this.setNewHSVA(hsva.h, hsva.s, hsva.v, hsva.a, false);
|
|
||||||
this.setColor(presetColor);
|
|
||||||
},
|
|
||||||
setNewHSVA(hue: number, saturation: number, value: number, alpha: number, isNone: boolean) {
|
|
||||||
this.hue = hue;
|
|
||||||
this.saturation = saturation;
|
|
||||||
this.value = value;
|
|
||||||
this.alpha = alpha;
|
|
||||||
this.isNone = isNone;
|
|
||||||
},
|
|
||||||
setInitialHSVA(hue: number, saturation: number, value: number, alpha: number, isNone: boolean) {
|
|
||||||
this.initialHue = hue;
|
|
||||||
this.initialSaturation = saturation;
|
|
||||||
this.initialValue = value;
|
|
||||||
this.initialAlpha = alpha;
|
|
||||||
this.initialIsNone = isNone;
|
|
||||||
},
|
|
||||||
async activateEyedropperSample() {
|
|
||||||
// TODO: Replace this temporary solution that only works in Chromium-based browsers with the custom color sampler used by the Eyedropper tool
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
if (!(window as any).EyeDropper) {
|
|
||||||
this.editor.instance.eyedropperSampleForColorPicker();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const result = await new (window as any).EyeDropper().open();
|
|
||||||
this.setColorCode(result.sRGBHex);
|
|
||||||
} catch {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
unmounted() {
|
|
||||||
this.removeEvents();
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
DropdownInput,
|
|
||||||
FloatingMenu,
|
|
||||||
IconButton,
|
|
||||||
LayoutCol,
|
|
||||||
LayoutRow,
|
|
||||||
NumberInput,
|
|
||||||
Separator,
|
|
||||||
TextInput,
|
|
||||||
TextLabel,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,37 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
|
import FloatingMenu from "@/components/layout/FloatingMenu.vue";
|
||||||
|
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||||
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
|
import TextButton from "@/components/widgets/buttons/TextButton.vue";
|
||||||
|
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||||
|
import WidgetLayout from "@/components/widgets/WidgetLayout.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
inject: ["dialog"],
|
||||||
|
methods: {
|
||||||
|
dismiss() {
|
||||||
|
this.dialog.dismissDialog();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// Focus the first button in the popup
|
||||||
|
const dialogModal: HTMLDivElement | undefined = this.$el;
|
||||||
|
const emphasizedOrFirstButton = (dialogModal?.querySelector("[data-emphasized]") || dialogModal?.querySelector("[data-text-button]") || undefined) as HTMLButtonElement | undefined;
|
||||||
|
emphasizedOrFirstButton?.focus();
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
FloatingMenu,
|
||||||
|
IconLabel,
|
||||||
|
LayoutCol,
|
||||||
|
LayoutRow,
|
||||||
|
TextButton,
|
||||||
|
WidgetLayout,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FloatingMenu :open="true" class="dialog-modal" :type="'Dialog'" :direction="'Center'" data-dialog-modal>
|
<FloatingMenu :open="true" class="dialog-modal" :type="'Dialog'" :direction="'Center'" data-dialog-modal>
|
||||||
<LayoutRow>
|
<LayoutRow>
|
||||||
@@ -65,37 +99,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
import FloatingMenu from "@/components/layout/FloatingMenu.vue";
|
|
||||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|
||||||
import TextButton from "@/components/widgets/buttons/TextButton.vue";
|
|
||||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
|
||||||
import WidgetLayout from "@/components/widgets/WidgetLayout.vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
inject: ["dialog"],
|
|
||||||
methods: {
|
|
||||||
dismiss() {
|
|
||||||
this.dialog.dismissDialog();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
// Focus the first button in the popup
|
|
||||||
const dialogModal: HTMLDivElement | undefined = this.$el;
|
|
||||||
const emphasizedOrFirstButton = (dialogModal?.querySelector("[data-emphasized]") || dialogModal?.querySelector("[data-text-button]") || undefined) as HTMLButtonElement | undefined;
|
|
||||||
emphasizedOrFirstButton?.focus();
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
FloatingMenu,
|
|
||||||
IconLabel,
|
|
||||||
LayoutCol,
|
|
||||||
LayoutRow,
|
|
||||||
TextButton,
|
|
||||||
WidgetLayout,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,61 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
import FloatingMenu from "@/components/layout/FloatingMenu.vue";
|
||||||
|
|
||||||
|
// Should be equal to the width and height of the canvas in the CSS above
|
||||||
|
const ZOOM_WINDOW_DIMENSIONS_EXPANDED = 110;
|
||||||
|
// SHould be equal to the width and height of the `.pixel-outline` div in the CSS above, and should be evenly divisible into the number above
|
||||||
|
const UPSCALE_FACTOR = 10;
|
||||||
|
|
||||||
|
export const ZOOM_WINDOW_DIMENSIONS = ZOOM_WINDOW_DIMENSIONS_EXPANDED / UPSCALE_FACTOR;
|
||||||
|
|
||||||
|
const temporaryCanvas = document.createElement("canvas");
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
imageData: { type: Object as PropType<ImageData> },
|
||||||
|
colorChoice: { type: String as PropType<string>, required: true },
|
||||||
|
primaryColor: { type: String as PropType<string>, required: true },
|
||||||
|
secondaryColor: { type: String as PropType<string>, required: true },
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.displayImageDataPreview(this.imageData);
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
imageData(imageData: ImageData | undefined) {
|
||||||
|
this.displayImageDataPreview(imageData);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
displayImageDataPreview(imageData: ImageData | undefined) {
|
||||||
|
const canvas = this.$refs.zoomPreviewCanvas as HTMLCanvasElement | undefined;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
canvas.width = ZOOM_WINDOW_DIMENSIONS;
|
||||||
|
canvas.height = ZOOM_WINDOW_DIMENSIONS;
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
|
||||||
|
temporaryCanvas.width = ZOOM_WINDOW_DIMENSIONS;
|
||||||
|
temporaryCanvas.height = ZOOM_WINDOW_DIMENSIONS;
|
||||||
|
const temporaryContext = temporaryCanvas.getContext("2d");
|
||||||
|
|
||||||
|
if (!imageData || !context || !temporaryContext) return;
|
||||||
|
|
||||||
|
temporaryContext.putImageData(imageData, 0, 0, 0, 0, ZOOM_WINDOW_DIMENSIONS, ZOOM_WINDOW_DIMENSIONS);
|
||||||
|
|
||||||
|
context.fillStyle = "black";
|
||||||
|
context.fillRect(0, 0, ZOOM_WINDOW_DIMENSIONS, ZOOM_WINDOW_DIMENSIONS);
|
||||||
|
|
||||||
|
context.drawImage(temporaryCanvas, 0, 0);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
FloatingMenu,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FloatingMenu
|
<FloatingMenu
|
||||||
:open="true"
|
:open="true"
|
||||||
@@ -79,61 +137,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
import FloatingMenu from "@/components/layout/FloatingMenu.vue";
|
|
||||||
|
|
||||||
// Should be equal to the width and height of the canvas in the CSS above
|
|
||||||
const ZOOM_WINDOW_DIMENSIONS_EXPANDED = 110;
|
|
||||||
// SHould be equal to the width and height of the `.pixel-outline` div in the CSS above, and should be evenly divisible into the number above
|
|
||||||
const UPSCALE_FACTOR = 10;
|
|
||||||
|
|
||||||
export const ZOOM_WINDOW_DIMENSIONS = ZOOM_WINDOW_DIMENSIONS_EXPANDED / UPSCALE_FACTOR;
|
|
||||||
|
|
||||||
const temporaryCanvas = document.createElement("canvas");
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
imageData: { type: Object as PropType<ImageData> },
|
|
||||||
colorChoice: { type: String as PropType<string>, required: true },
|
|
||||||
primaryColor: { type: String as PropType<string>, required: true },
|
|
||||||
secondaryColor: { type: String as PropType<string>, required: true },
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.displayImageDataPreview(this.imageData);
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
imageData(imageData: ImageData | undefined) {
|
|
||||||
this.displayImageDataPreview(imageData);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
displayImageDataPreview(imageData: ImageData | undefined) {
|
|
||||||
const canvas = this.$refs.zoomPreviewCanvas as HTMLCanvasElement | undefined;
|
|
||||||
if (!canvas) return;
|
|
||||||
|
|
||||||
canvas.width = ZOOM_WINDOW_DIMENSIONS;
|
|
||||||
canvas.height = ZOOM_WINDOW_DIMENSIONS;
|
|
||||||
const context = canvas.getContext("2d");
|
|
||||||
|
|
||||||
temporaryCanvas.width = ZOOM_WINDOW_DIMENSIONS;
|
|
||||||
temporaryCanvas.height = ZOOM_WINDOW_DIMENSIONS;
|
|
||||||
const temporaryContext = temporaryCanvas.getContext("2d");
|
|
||||||
|
|
||||||
if (!imageData || !context || !temporaryContext) return;
|
|
||||||
|
|
||||||
temporaryContext.putImageData(imageData, 0, 0, 0, 0, ZOOM_WINDOW_DIMENSIONS, ZOOM_WINDOW_DIMENSIONS);
|
|
||||||
|
|
||||||
context.fillStyle = "black";
|
|
||||||
context.fillRect(0, 0, ZOOM_WINDOW_DIMENSIONS, ZOOM_WINDOW_DIMENSIONS);
|
|
||||||
|
|
||||||
context.drawImage(temporaryCanvas, 0, 0);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
FloatingMenu,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,166 +1,3 @@
|
|||||||
<template>
|
|
||||||
<FloatingMenu
|
|
||||||
class="menu-list"
|
|
||||||
v-model:open="isOpen"
|
|
||||||
@naturalWidth="(newNaturalWidth: number) => $emit('naturalWidth', newNaturalWidth)"
|
|
||||||
:type="'Dropdown'"
|
|
||||||
:windowEdgeMargin="0"
|
|
||||||
:escapeCloses="false"
|
|
||||||
v-bind="{ direction, scrollableY: scrollableY && virtualScrollingEntryHeight === 0, minWidth }"
|
|
||||||
ref="floatingMenu"
|
|
||||||
>
|
|
||||||
<!-- If we put the scrollableY on the layoutcol for non-font dropdowns then for some reason it always creates a tiny scrollbar.
|
|
||||||
However when we are using the virtual scrolling then we need the layoutcol to be scrolling so we can bind the events without using $refs. -->
|
|
||||||
<LayoutCol ref="scroller" :scrollableY="scrollableY && virtualScrollingEntryHeight !== 0" @scroll="onScroll" :style="{ minWidth: virtualScrollingEntryHeight ? `${minWidth}px` : `inherit` }">
|
|
||||||
<LayoutRow v-if="virtualScrollingEntryHeight" class="scroll-spacer" :style="{ height: `${virtualScrollingStartIndex * virtualScrollingEntryHeight}px` }"></LayoutRow>
|
|
||||||
<template v-for="(section, sectionIndex) in entries" :key="sectionIndex">
|
|
||||||
<Separator :type="'List'" :direction="'Vertical'" v-if="sectionIndex > 0" />
|
|
||||||
<LayoutRow
|
|
||||||
v-for="(entry, entryIndex) in virtualScrollingEntryHeight ? section.slice(virtualScrollingStartIndex, virtualScrollingEndIndex) : section"
|
|
||||||
:key="entryIndex + (virtualScrollingEntryHeight ? virtualScrollingStartIndex : 0)"
|
|
||||||
class="row"
|
|
||||||
:class="{ open: isEntryOpen(entry), active: entry.label === highlighted?.label, disabled: entry.disabled }"
|
|
||||||
:style="{ height: virtualScrollingEntryHeight || '20px' }"
|
|
||||||
:title="tooltip"
|
|
||||||
@click="() => !entry.disabled && onEntryClick(entry)"
|
|
||||||
@pointerenter="() => !entry.disabled && onEntryPointerEnter(entry)"
|
|
||||||
@pointerleave="() => !entry.disabled && onEntryPointerLeave(entry)"
|
|
||||||
>
|
|
||||||
<IconLabel v-if="entry.icon && drawIcon" :icon="entry.icon" class="entry-icon" />
|
|
||||||
<div v-else-if="drawIcon" class="no-icon"></div>
|
|
||||||
|
|
||||||
<link v-if="entry.font" rel="stylesheet" :href="entry.font?.toString()" />
|
|
||||||
|
|
||||||
<TextLabel class="entry-label" :style="{ fontFamily: `${!entry.font ? 'inherit' : entry.value}` }">{{ entry.label }}</TextLabel>
|
|
||||||
|
|
||||||
<UserInputLabel v-if="entry.shortcut?.keys.length" :keysWithLabelsGroups="[entry.shortcut.keys]" :requiresLock="entry.shortcutRequiresLock" />
|
|
||||||
|
|
||||||
<div class="submenu-arrow" v-if="entry.children?.length"></div>
|
|
||||||
<div class="no-submenu-arrow" v-else></div>
|
|
||||||
|
|
||||||
<MenuList
|
|
||||||
v-if="entry.children"
|
|
||||||
@naturalWidth="(newNaturalWidth: number) => $emit('naturalWidth', newNaturalWidth)"
|
|
||||||
:open="entry.ref?.open || false"
|
|
||||||
:direction="'TopRight'"
|
|
||||||
:entries="entry.children"
|
|
||||||
v-bind="{ minWidth, drawIcon, scrollableY }"
|
|
||||||
:ref="(ref: MenuListInstance): void => (ref && (entry.ref = ref), undefined)"
|
|
||||||
/>
|
|
||||||
</LayoutRow>
|
|
||||||
</template>
|
|
||||||
<LayoutRow
|
|
||||||
v-if="virtualScrollingEntryHeight"
|
|
||||||
class="scroll-spacer"
|
|
||||||
:style="{ height: `${virtualScrollingTotalHeight - virtualScrollingEndIndex * virtualScrollingEntryHeight}px` }"
|
|
||||||
></LayoutRow>
|
|
||||||
</LayoutCol>
|
|
||||||
</FloatingMenu>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.menu-list {
|
|
||||||
.floating-menu-container .floating-menu-content {
|
|
||||||
padding: 4px 0;
|
|
||||||
|
|
||||||
.separator div {
|
|
||||||
background: var(--color-4-dimgray);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-spacer {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
|
||||||
height: 20px;
|
|
||||||
align-items: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
position: relative;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
|
|
||||||
& > * {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-icon svg {
|
|
||||||
fill: var(--color-e-nearwhite);
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-icon {
|
|
||||||
width: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-label {
|
|
||||||
flex: 1 1 100%;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-icon,
|
|
||||||
.no-icon {
|
|
||||||
margin: 0 4px;
|
|
||||||
|
|
||||||
& + .entry-label {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-input-label {
|
|
||||||
margin-left: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submenu-arrow {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 3px 0 3px 6px;
|
|
||||||
border-color: transparent transparent transparent var(--color-e-nearwhite);
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-submenu-arrow {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submenu-arrow,
|
|
||||||
.no-submenu-arrow {
|
|
||||||
margin-left: 6px;
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&.open {
|
|
||||||
background: var(--color-6-lowergray);
|
|
||||||
color: var(--color-f-white);
|
|
||||||
|
|
||||||
.entry-icon svg {
|
|
||||||
fill: var(--color-f-white);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background: var(--color-e-nearwhite);
|
|
||||||
color: var(--color-2-mildblack);
|
|
||||||
|
|
||||||
.entry-icon svg {
|
|
||||||
fill: var(--color-2-mildblack);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
color: var(--color-8-uppergray);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
fill: var(--color-8-uppergray);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
@@ -357,3 +194,166 @@ const MenuList = defineComponent({
|
|||||||
});
|
});
|
||||||
export default MenuList;
|
export default MenuList;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FloatingMenu
|
||||||
|
class="menu-list"
|
||||||
|
v-model:open="isOpen"
|
||||||
|
@naturalWidth="(newNaturalWidth: number) => $emit('naturalWidth', newNaturalWidth)"
|
||||||
|
:type="'Dropdown'"
|
||||||
|
:windowEdgeMargin="0"
|
||||||
|
:escapeCloses="false"
|
||||||
|
v-bind="{ direction, scrollableY: scrollableY && virtualScrollingEntryHeight === 0, minWidth }"
|
||||||
|
ref="floatingMenu"
|
||||||
|
>
|
||||||
|
<!-- If we put the scrollableY on the layoutcol for non-font dropdowns then for some reason it always creates a tiny scrollbar.
|
||||||
|
However when we are using the virtual scrolling then we need the layoutcol to be scrolling so we can bind the events without using $refs. -->
|
||||||
|
<LayoutCol ref="scroller" :scrollableY="scrollableY && virtualScrollingEntryHeight !== 0" @scroll="onScroll" :style="{ minWidth: virtualScrollingEntryHeight ? `${minWidth}px` : `inherit` }">
|
||||||
|
<LayoutRow v-if="virtualScrollingEntryHeight" class="scroll-spacer" :style="{ height: `${virtualScrollingStartIndex * virtualScrollingEntryHeight}px` }"></LayoutRow>
|
||||||
|
<template v-for="(section, sectionIndex) in entries" :key="sectionIndex">
|
||||||
|
<Separator :type="'List'" :direction="'Vertical'" v-if="sectionIndex > 0" />
|
||||||
|
<LayoutRow
|
||||||
|
v-for="(entry, entryIndex) in virtualScrollingEntryHeight ? section.slice(virtualScrollingStartIndex, virtualScrollingEndIndex) : section"
|
||||||
|
:key="entryIndex + (virtualScrollingEntryHeight ? virtualScrollingStartIndex : 0)"
|
||||||
|
class="row"
|
||||||
|
:class="{ open: isEntryOpen(entry), active: entry.label === highlighted?.label, disabled: entry.disabled }"
|
||||||
|
:style="{ height: virtualScrollingEntryHeight || '20px' }"
|
||||||
|
:title="tooltip"
|
||||||
|
@click="() => !entry.disabled && onEntryClick(entry)"
|
||||||
|
@pointerenter="() => !entry.disabled && onEntryPointerEnter(entry)"
|
||||||
|
@pointerleave="() => !entry.disabled && onEntryPointerLeave(entry)"
|
||||||
|
>
|
||||||
|
<IconLabel v-if="entry.icon && drawIcon" :icon="entry.icon" class="entry-icon" />
|
||||||
|
<div v-else-if="drawIcon" class="no-icon"></div>
|
||||||
|
|
||||||
|
<link v-if="entry.font" rel="stylesheet" :href="entry.font?.toString()" />
|
||||||
|
|
||||||
|
<TextLabel class="entry-label" :style="{ fontFamily: `${!entry.font ? 'inherit' : entry.value}` }">{{ entry.label }}</TextLabel>
|
||||||
|
|
||||||
|
<UserInputLabel v-if="entry.shortcut?.keys.length" :keysWithLabelsGroups="[entry.shortcut.keys]" :requiresLock="entry.shortcutRequiresLock" />
|
||||||
|
|
||||||
|
<div class="submenu-arrow" v-if="entry.children?.length"></div>
|
||||||
|
<div class="no-submenu-arrow" v-else></div>
|
||||||
|
|
||||||
|
<MenuList
|
||||||
|
v-if="entry.children"
|
||||||
|
@naturalWidth="(newNaturalWidth: number) => $emit('naturalWidth', newNaturalWidth)"
|
||||||
|
:open="entry.ref?.open || false"
|
||||||
|
:direction="'TopRight'"
|
||||||
|
:entries="entry.children"
|
||||||
|
v-bind="{ minWidth, drawIcon, scrollableY }"
|
||||||
|
:ref="(ref: MenuListInstance): void => (ref && (entry.ref = ref), undefined)"
|
||||||
|
/>
|
||||||
|
</LayoutRow>
|
||||||
|
</template>
|
||||||
|
<LayoutRow
|
||||||
|
v-if="virtualScrollingEntryHeight"
|
||||||
|
class="scroll-spacer"
|
||||||
|
:style="{ height: `${virtualScrollingTotalHeight - virtualScrollingEndIndex * virtualScrollingEntryHeight}px` }"
|
||||||
|
></LayoutRow>
|
||||||
|
</LayoutCol>
|
||||||
|
</FloatingMenu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.menu-list {
|
||||||
|
.floating-menu-container .floating-menu-content {
|
||||||
|
padding: 4px 0;
|
||||||
|
|
||||||
|
.separator div {
|
||||||
|
background: var(--color-4-dimgray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-spacer {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
height: 20px;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
position: relative;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-icon svg {
|
||||||
|
fill: var(--color-e-nearwhite);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-icon {
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-label {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-icon,
|
||||||
|
.no-icon {
|
||||||
|
margin: 0 4px;
|
||||||
|
|
||||||
|
& + .entry-label {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-input-label {
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-arrow {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 3px 0 3px 6px;
|
||||||
|
border-color: transparent transparent transparent var(--color-e-nearwhite);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-submenu-arrow {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu-arrow,
|
||||||
|
.no-submenu-arrow {
|
||||||
|
margin-left: 6px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.open {
|
||||||
|
background: var(--color-6-lowergray);
|
||||||
|
color: var(--color-f-white);
|
||||||
|
|
||||||
|
.entry-icon svg {
|
||||||
|
fill: var(--color-f-white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: var(--color-e-nearwhite);
|
||||||
|
color: var(--color-2-mildblack);
|
||||||
|
|
||||||
|
.entry-icon svg {
|
||||||
|
fill: var(--color-2-mildblack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
color: var(--color-8-uppergray);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: var(--color-8-uppergray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,186 +1,3 @@
|
|||||||
<template>
|
|
||||||
<div class="floating-menu" :class="[direction.toLowerCase(), type.toLowerCase()]">
|
|
||||||
<div class="tail" v-if="displayTail" ref="tail"></div>
|
|
||||||
<div class="floating-menu-container" v-if="displayContainer" ref="floatingMenuContainer">
|
|
||||||
<LayoutCol class="floating-menu-content" :style="{ minWidth: minWidthStyleValue }" :scrollableY="scrollableY" ref="floatingMenuContent" data-floating-menu-content>
|
|
||||||
<slot></slot>
|
|
||||||
</LayoutCol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.floating-menu {
|
|
||||||
position: absolute;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
display: flex;
|
|
||||||
// Floating menus begin at a z-index of 1000
|
|
||||||
z-index: 1000;
|
|
||||||
--floating-menu-content-offset: 0;
|
|
||||||
--floating-menu-content-border-radius: 4px;
|
|
||||||
|
|
||||||
&.bottom {
|
|
||||||
--floating-menu-content-border-radius: 0 0 4px 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tail {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-style: solid;
|
|
||||||
// Put the tail above the floating menu's shadow
|
|
||||||
z-index: 10;
|
|
||||||
// Draw over the application without being clipped by the containing panel's `overflow: hidden`
|
|
||||||
position: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.floating-menu-container {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.floating-menu-content {
|
|
||||||
background: rgba(var(--color-2-mildblack-rgb), 0.95);
|
|
||||||
box-shadow: rgba(var(--color-0-black-rgb), 50%) 0 2px 4px;
|
|
||||||
border-radius: var(--floating-menu-content-border-radius);
|
|
||||||
color: var(--color-e-nearwhite);
|
|
||||||
font-size: inherit;
|
|
||||||
padding: 8px;
|
|
||||||
z-index: 0;
|
|
||||||
// Draw over the application without being clipped by the containing panel's `overflow: hidden`
|
|
||||||
position: fixed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.dropdown {
|
|
||||||
&.top {
|
|
||||||
width: 100%;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.bottom {
|
|
||||||
width: 100%;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.left {
|
|
||||||
height: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.right {
|
|
||||||
height: 100%;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.topleft {
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
margin-top: -4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.topright {
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
margin-top: -4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.topleft {
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
margin-bottom: -4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.topright {
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
margin-bottom: -4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.top.dropdown .floating-menu-container,
|
|
||||||
&.bottom.dropdown .floating-menu-container {
|
|
||||||
justify-content: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.popover {
|
|
||||||
--floating-menu-content-offset: 10px;
|
|
||||||
--floating-menu-content-border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.cursor .floating-menu-container .floating-menu-content {
|
|
||||||
background: none;
|
|
||||||
box-shadow: none;
|
|
||||||
border-radius: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.center {
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
> .floating-menu-container > .floating-menu-content {
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.top,
|
|
||||||
&.bottom {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.top .tail {
|
|
||||||
border-width: 8px 6px 0 6px;
|
|
||||||
border-color: rgba(var(--color-2-mildblack-rgb), 0.95) transparent transparent transparent;
|
|
||||||
margin-left: -6px;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.bottom .tail {
|
|
||||||
border-width: 0 6px 8px 6px;
|
|
||||||
border-color: transparent transparent rgba(var(--color-2-mildblack-rgb), 0.95) transparent;
|
|
||||||
margin-left: -6px;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.left .tail {
|
|
||||||
border-width: 6px 0 6px 8px;
|
|
||||||
border-color: transparent transparent transparent rgba(var(--color-2-mildblack-rgb), 0.95);
|
|
||||||
margin-top: -6px;
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.right .tail {
|
|
||||||
border-width: 6px 8px 6px 0;
|
|
||||||
border-color: transparent rgba(var(--color-2-mildblack-rgb), 0.95) transparent transparent;
|
|
||||||
margin-top: -6px;
|
|
||||||
margin-left: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.top .floating-menu-container {
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: var(--floating-menu-content-offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.bottom .floating-menu-container {
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: var(--floating-menu-content-offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.left .floating-menu-container {
|
|
||||||
align-items: center;
|
|
||||||
margin-right: var(--floating-menu-content-offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.right .floating-menu-container {
|
|
||||||
align-items: center;
|
|
||||||
margin-left: var(--floating-menu-content-offset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, nextTick, type PropType } from "vue";
|
import { defineComponent, nextTick, type PropType } from "vue";
|
||||||
|
|
||||||
@@ -555,3 +372,186 @@ export default defineComponent({
|
|||||||
components: { LayoutCol },
|
components: { LayoutCol },
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="floating-menu" :class="[direction.toLowerCase(), type.toLowerCase()]">
|
||||||
|
<div class="tail" v-if="displayTail" ref="tail"></div>
|
||||||
|
<div class="floating-menu-container" v-if="displayContainer" ref="floatingMenuContainer">
|
||||||
|
<LayoutCol class="floating-menu-content" :style="{ minWidth: minWidthStyleValue }" :scrollableY="scrollableY" ref="floatingMenuContent" data-floating-menu-content>
|
||||||
|
<slot></slot>
|
||||||
|
</LayoutCol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.floating-menu {
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
display: flex;
|
||||||
|
// Floating menus begin at a z-index of 1000
|
||||||
|
z-index: 1000;
|
||||||
|
--floating-menu-content-offset: 0;
|
||||||
|
--floating-menu-content-border-radius: 4px;
|
||||||
|
|
||||||
|
&.bottom {
|
||||||
|
--floating-menu-content-border-radius: 0 0 4px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tail {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-style: solid;
|
||||||
|
// Put the tail above the floating menu's shadow
|
||||||
|
z-index: 10;
|
||||||
|
// Draw over the application without being clipped by the containing panel's `overflow: hidden`
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-menu-container {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.floating-menu-content {
|
||||||
|
background: rgba(var(--color-2-mildblack-rgb), 0.95);
|
||||||
|
box-shadow: rgba(var(--color-0-black-rgb), 50%) 0 2px 4px;
|
||||||
|
border-radius: var(--floating-menu-content-border-radius);
|
||||||
|
color: var(--color-e-nearwhite);
|
||||||
|
font-size: inherit;
|
||||||
|
padding: 8px;
|
||||||
|
z-index: 0;
|
||||||
|
// Draw over the application without being clipped by the containing panel's `overflow: hidden`
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dropdown {
|
||||||
|
&.top {
|
||||||
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bottom {
|
||||||
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.left {
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.right {
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.topleft {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
margin-top: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.topright {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
margin-top: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.topleft {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
margin-bottom: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.topright {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
margin-bottom: -4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.top.dropdown .floating-menu-container,
|
||||||
|
&.bottom.dropdown .floating-menu-container {
|
||||||
|
justify-content: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.popover {
|
||||||
|
--floating-menu-content-offset: 10px;
|
||||||
|
--floating-menu-content-border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cursor .floating-menu-container .floating-menu-content {
|
||||||
|
background: none;
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.center {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> .floating-menu-container > .floating-menu-content {
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.top,
|
||||||
|
&.bottom {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.top .tail {
|
||||||
|
border-width: 8px 6px 0 6px;
|
||||||
|
border-color: rgba(var(--color-2-mildblack-rgb), 0.95) transparent transparent transparent;
|
||||||
|
margin-left: -6px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bottom .tail {
|
||||||
|
border-width: 0 6px 8px 6px;
|
||||||
|
border-color: transparent transparent rgba(var(--color-2-mildblack-rgb), 0.95) transparent;
|
||||||
|
margin-left: -6px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.left .tail {
|
||||||
|
border-width: 6px 0 6px 8px;
|
||||||
|
border-color: transparent transparent transparent rgba(var(--color-2-mildblack-rgb), 0.95);
|
||||||
|
margin-top: -6px;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.right .tail {
|
||||||
|
border-width: 6px 8px 6px 0;
|
||||||
|
border-color: transparent rgba(var(--color-2-mildblack-rgb), 0.95) transparent transparent;
|
||||||
|
margin-top: -6px;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.top .floating-menu-container {
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: var(--floating-menu-content-offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.bottom .floating-menu-container {
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: var(--floating-menu-content-offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.left .floating-menu-container {
|
||||||
|
align-items: center;
|
||||||
|
margin-right: var(--floating-menu-content-offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.right .floating-menu-container {
|
||||||
|
align-items: center;
|
||||||
|
margin-left: var(--floating-menu-content-offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
scrollableX: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
scrollableY: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="layout-col"
|
class="layout-col"
|
||||||
@@ -21,15 +33,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
scrollableX: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
scrollableY: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
scrollableX: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
scrollableY: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="layout-row"
|
class="layout-row"
|
||||||
@@ -21,15 +33,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
scrollableX: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
scrollableY: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,229 +1,3 @@
|
|||||||
<template>
|
|
||||||
<LayoutCol class="document">
|
|
||||||
<LayoutRow class="options-bar" :scrollableX="true">
|
|
||||||
<WidgetLayout :layout="document.state.documentModeLayout" />
|
|
||||||
<WidgetLayout :layout="document.state.toolOptionsLayout" />
|
|
||||||
|
|
||||||
<LayoutRow class="spacer"></LayoutRow>
|
|
||||||
|
|
||||||
<WidgetLayout :layout="document.state.documentBarLayout" />
|
|
||||||
</LayoutRow>
|
|
||||||
<LayoutRow class="shelf-and-viewport">
|
|
||||||
<LayoutCol class="shelf">
|
|
||||||
<LayoutCol class="tools" :scrollableY="true">
|
|
||||||
<WidgetLayout :layout="document.state.toolShelfLayout" />
|
|
||||||
</LayoutCol>
|
|
||||||
|
|
||||||
<LayoutCol class="spacer"></LayoutCol>
|
|
||||||
|
|
||||||
<LayoutCol class="working-colors">
|
|
||||||
<WidgetLayout :layout="document.state.workingColorsLayout" />
|
|
||||||
</LayoutCol>
|
|
||||||
</LayoutCol>
|
|
||||||
<LayoutCol class="viewport">
|
|
||||||
<LayoutRow class="bar-area">
|
|
||||||
<CanvasRuler :origin="rulerOrigin.x" :majorMarkSpacing="rulerSpacing" :numberInterval="rulerInterval" :direction="'Horizontal'" class="top-ruler" ref="rulerHorizontal" />
|
|
||||||
</LayoutRow>
|
|
||||||
<LayoutRow class="canvas-area">
|
|
||||||
<LayoutCol class="bar-area">
|
|
||||||
<CanvasRuler :origin="rulerOrigin.y" :majorMarkSpacing="rulerSpacing" :numberInterval="rulerInterval" :direction="'Vertical'" ref="rulerVertical" />
|
|
||||||
</LayoutCol>
|
|
||||||
<LayoutCol class="canvas-area" :style="{ cursor: canvasCursor }">
|
|
||||||
<EyedropperPreview
|
|
||||||
v-if="cursorEyedropper"
|
|
||||||
:colorChoice="cursorEyedropperPreviewColorChoice"
|
|
||||||
:primaryColor="cursorEyedropperPreviewColorPrimary"
|
|
||||||
:secondaryColor="cursorEyedropperPreviewColorSecondary"
|
|
||||||
:imageData="cursorEyedropperPreviewImageData"
|
|
||||||
:style="{ left: cursorLeft + 'px', top: cursorTop + 'px' }"
|
|
||||||
/>
|
|
||||||
<div class="canvas" @pointerdown="(e: PointerEvent) => canvasPointerDown(e)" @dragover="(e) => e.preventDefault()" @drop="(e) => pasteFile(e)" ref="canvasDiv" data-canvas>
|
|
||||||
<svg class="artboards" v-html="artboardSvg" :style="{ width: canvasWidthCSS, height: canvasHeightCSS }"></svg>
|
|
||||||
<svg
|
|
||||||
class="artwork"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
v-html="artworkSvg"
|
|
||||||
:style="{ width: canvasWidthCSS, height: canvasHeightCSS }"
|
|
||||||
></svg>
|
|
||||||
<svg class="overlays" v-html="overlaysSvg" :style="{ width: canvasWidthCSS, height: canvasHeightCSS }"></svg>
|
|
||||||
</div>
|
|
||||||
</LayoutCol>
|
|
||||||
<LayoutCol class="bar-area">
|
|
||||||
<PersistentScrollbar
|
|
||||||
:direction="'Vertical'"
|
|
||||||
:handlePosition="scrollbarPos.y"
|
|
||||||
@update:handlePosition="(newValue: number) => translateCanvasY(newValue)"
|
|
||||||
v-model:handleLength="scrollbarSize.y"
|
|
||||||
@pressTrack="(delta: number) => pageY(delta)"
|
|
||||||
class="right-scrollbar"
|
|
||||||
/>
|
|
||||||
</LayoutCol>
|
|
||||||
</LayoutRow>
|
|
||||||
<LayoutRow class="bar-area">
|
|
||||||
<PersistentScrollbar
|
|
||||||
:direction="'Horizontal'"
|
|
||||||
:handlePosition="scrollbarPos.x"
|
|
||||||
@update:handlePosition="(newValue: number) => translateCanvasX(newValue)"
|
|
||||||
v-model:handleLength="scrollbarSize.x"
|
|
||||||
@pressTrack="(delta: number) => pageX(delta)"
|
|
||||||
class="bottom-scrollbar"
|
|
||||||
/>
|
|
||||||
</LayoutRow>
|
|
||||||
</LayoutCol>
|
|
||||||
</LayoutRow>
|
|
||||||
</LayoutCol>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.document {
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.options-bar {
|
|
||||||
height: 32px;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
margin: 0 4px;
|
|
||||||
|
|
||||||
.spacer {
|
|
||||||
min-width: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.shelf-and-viewport {
|
|
||||||
.shelf {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
|
|
||||||
.tools {
|
|
||||||
flex: 0 1 auto;
|
|
||||||
|
|
||||||
.icon-button[title^="Coming Soon"] {
|
|
||||||
opacity: 0.25;
|
|
||||||
transition: opacity 0.25s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-button:not(.active) {
|
|
||||||
.color-solid {
|
|
||||||
fill: var(--color-f-white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-general {
|
|
||||||
fill: var(--color-data-general);
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-vector {
|
|
||||||
fill: var(--color-data-vector);
|
|
||||||
}
|
|
||||||
|
|
||||||
.color-raster {
|
|
||||||
fill: var(--color-data-raster);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.spacer {
|
|
||||||
flex: 1 0 auto;
|
|
||||||
min-height: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.working-colors {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
|
|
||||||
.widget-row {
|
|
||||||
min-height: 0;
|
|
||||||
|
|
||||||
.swatch-pair {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-button {
|
|
||||||
--widget-height: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.viewport {
|
|
||||||
flex: 1 1 100%;
|
|
||||||
|
|
||||||
.canvas-area {
|
|
||||||
flex: 1 1 100%;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar-area {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-ruler {
|
|
||||||
padding-left: 16px;
|
|
||||||
margin-right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-scrollbar {
|
|
||||||
margin-top: -16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-scrollbar {
|
|
||||||
margin-right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.canvas {
|
|
||||||
background: var(--color-2-mildblack);
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
// Allows the SVG to be placed at explicit integer values of width and height to prevent non-pixel-perfect SVG scaling
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
position: absolute;
|
|
||||||
// Fallback values if JS hasn't set these to integers yet
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
// Allows dev tools to select the artwork without being blocked by the SVG containers
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
// Prevent inheritance from reaching the child elements
|
|
||||||
> * {
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreignObject {
|
|
||||||
width: 10000px;
|
|
||||||
height: 10000px;
|
|
||||||
overflow: visible;
|
|
||||||
|
|
||||||
div {
|
|
||||||
cursor: text;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
overflow: visible;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
display: inline-block;
|
|
||||||
// Workaround to force Chrome to display the flashing text entry cursor when text is empty
|
|
||||||
padding-left: 1px;
|
|
||||||
margin-left: -1px;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border: none;
|
|
||||||
outline: none; // Ok for contenteditable element
|
|
||||||
margin: -1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, nextTick } from "vue";
|
import { defineComponent, nextTick } from "vue";
|
||||||
|
|
||||||
@@ -525,3 +299,229 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<LayoutCol class="document">
|
||||||
|
<LayoutRow class="options-bar" :scrollableX="true">
|
||||||
|
<WidgetLayout :layout="document.state.documentModeLayout" />
|
||||||
|
<WidgetLayout :layout="document.state.toolOptionsLayout" />
|
||||||
|
|
||||||
|
<LayoutRow class="spacer"></LayoutRow>
|
||||||
|
|
||||||
|
<WidgetLayout :layout="document.state.documentBarLayout" />
|
||||||
|
</LayoutRow>
|
||||||
|
<LayoutRow class="shelf-and-viewport">
|
||||||
|
<LayoutCol class="shelf">
|
||||||
|
<LayoutCol class="tools" :scrollableY="true">
|
||||||
|
<WidgetLayout :layout="document.state.toolShelfLayout" />
|
||||||
|
</LayoutCol>
|
||||||
|
|
||||||
|
<LayoutCol class="spacer"></LayoutCol>
|
||||||
|
|
||||||
|
<LayoutCol class="working-colors">
|
||||||
|
<WidgetLayout :layout="document.state.workingColorsLayout" />
|
||||||
|
</LayoutCol>
|
||||||
|
</LayoutCol>
|
||||||
|
<LayoutCol class="viewport">
|
||||||
|
<LayoutRow class="bar-area">
|
||||||
|
<CanvasRuler :origin="rulerOrigin.x" :majorMarkSpacing="rulerSpacing" :numberInterval="rulerInterval" :direction="'Horizontal'" class="top-ruler" ref="rulerHorizontal" />
|
||||||
|
</LayoutRow>
|
||||||
|
<LayoutRow class="canvas-area">
|
||||||
|
<LayoutCol class="bar-area">
|
||||||
|
<CanvasRuler :origin="rulerOrigin.y" :majorMarkSpacing="rulerSpacing" :numberInterval="rulerInterval" :direction="'Vertical'" ref="rulerVertical" />
|
||||||
|
</LayoutCol>
|
||||||
|
<LayoutCol class="canvas-area" :style="{ cursor: canvasCursor }">
|
||||||
|
<EyedropperPreview
|
||||||
|
v-if="cursorEyedropper"
|
||||||
|
:colorChoice="cursorEyedropperPreviewColorChoice"
|
||||||
|
:primaryColor="cursorEyedropperPreviewColorPrimary"
|
||||||
|
:secondaryColor="cursorEyedropperPreviewColorSecondary"
|
||||||
|
:imageData="cursorEyedropperPreviewImageData"
|
||||||
|
:style="{ left: cursorLeft + 'px', top: cursorTop + 'px' }"
|
||||||
|
/>
|
||||||
|
<div class="canvas" @pointerdown="(e: PointerEvent) => canvasPointerDown(e)" @dragover="(e) => e.preventDefault()" @drop="(e) => pasteFile(e)" ref="canvasDiv" data-canvas>
|
||||||
|
<svg class="artboards" v-html="artboardSvg" :style="{ width: canvasWidthCSS, height: canvasHeightCSS }"></svg>
|
||||||
|
<svg
|
||||||
|
class="artwork"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
v-html="artworkSvg"
|
||||||
|
:style="{ width: canvasWidthCSS, height: canvasHeightCSS }"
|
||||||
|
></svg>
|
||||||
|
<svg class="overlays" v-html="overlaysSvg" :style="{ width: canvasWidthCSS, height: canvasHeightCSS }"></svg>
|
||||||
|
</div>
|
||||||
|
</LayoutCol>
|
||||||
|
<LayoutCol class="bar-area">
|
||||||
|
<PersistentScrollbar
|
||||||
|
:direction="'Vertical'"
|
||||||
|
:handlePosition="scrollbarPos.y"
|
||||||
|
@update:handlePosition="(newValue: number) => translateCanvasY(newValue)"
|
||||||
|
v-model:handleLength="scrollbarSize.y"
|
||||||
|
@pressTrack="(delta: number) => pageY(delta)"
|
||||||
|
class="right-scrollbar"
|
||||||
|
/>
|
||||||
|
</LayoutCol>
|
||||||
|
</LayoutRow>
|
||||||
|
<LayoutRow class="bar-area">
|
||||||
|
<PersistentScrollbar
|
||||||
|
:direction="'Horizontal'"
|
||||||
|
:handlePosition="scrollbarPos.x"
|
||||||
|
@update:handlePosition="(newValue: number) => translateCanvasX(newValue)"
|
||||||
|
v-model:handleLength="scrollbarSize.x"
|
||||||
|
@pressTrack="(delta: number) => pageX(delta)"
|
||||||
|
class="bottom-scrollbar"
|
||||||
|
/>
|
||||||
|
</LayoutRow>
|
||||||
|
</LayoutCol>
|
||||||
|
</LayoutRow>
|
||||||
|
</LayoutCol>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.document {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.options-bar {
|
||||||
|
height: 32px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin: 0 4px;
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
min-width: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.shelf-and-viewport {
|
||||||
|
.shelf {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
|
.tools {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
|
||||||
|
.icon-button[title^="Coming Soon"] {
|
||||||
|
opacity: 0.25;
|
||||||
|
transition: opacity 0.25s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button:not(.active) {
|
||||||
|
.color-solid {
|
||||||
|
fill: var(--color-f-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-general {
|
||||||
|
fill: var(--color-data-general);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-vector {
|
||||||
|
fill: var(--color-data-vector);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-raster {
|
||||||
|
fill: var(--color-data-raster);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
min-height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.working-colors {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
|
.widget-row {
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
.swatch-pair {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
--widget-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewport {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
|
||||||
|
.canvas-area {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-area {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-ruler {
|
||||||
|
padding-left: 16px;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-scrollbar {
|
||||||
|
margin-top: -16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-scrollbar {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas {
|
||||||
|
background: var(--color-2-mildblack);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
// Allows the SVG to be placed at explicit integer values of width and height to prevent non-pixel-perfect SVG scaling
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
position: absolute;
|
||||||
|
// Fallback values if JS hasn't set these to integers yet
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
// Allows dev tools to select the artwork without being blocked by the SVG containers
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
// Prevent inheritance from reaching the child elements
|
||||||
|
> * {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreignObject {
|
||||||
|
width: 10000px;
|
||||||
|
height: 10000px;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
div {
|
||||||
|
cursor: text;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: visible;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
display: inline-block;
|
||||||
|
// Workaround to force Chrome to display the flashing text entry cursor when text is empty
|
||||||
|
padding-left: 1px;
|
||||||
|
margin-left: -1px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border: none;
|
||||||
|
outline: none; // Ok for contenteditable element
|
||||||
|
margin: -1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,272 +1,3 @@
|
|||||||
<template>
|
|
||||||
<LayoutCol class="layer-tree" @dragleave="dragInPanel = false">
|
|
||||||
<LayoutRow class="options-bar" :scrollableX="true">
|
|
||||||
<WidgetLayout :layout="layerTreeOptionsLayout" />
|
|
||||||
</LayoutRow>
|
|
||||||
<LayoutRow class="layer-tree-rows" :scrollableY="true">
|
|
||||||
<LayoutCol class="list" ref="list" @click="() => deselectAllLayers()" @dragover="(e: DragEvent) => draggable && updateInsertLine(e)" @dragend="() => draggable && drop()">
|
|
||||||
<LayoutRow
|
|
||||||
class="layer-row"
|
|
||||||
v-for="(listing, index) in layers"
|
|
||||||
:key="String(listing.entry.path.slice(-1))"
|
|
||||||
:class="{ 'insert-folder': draggingData?.highlightFolder && draggingData?.insertFolder === listing.entry.path }"
|
|
||||||
>
|
|
||||||
<LayoutRow class="visibility">
|
|
||||||
<IconButton
|
|
||||||
:action="(e?: MouseEvent) => (toggleLayerVisibility(listing.entry.path), e?.stopPropagation())"
|
|
||||||
:size="24"
|
|
||||||
:icon="listing.entry.visible ? 'EyeVisible' : 'EyeHidden'"
|
|
||||||
:title="listing.entry.visible ? 'Visible' : 'Hidden'"
|
|
||||||
/>
|
|
||||||
</LayoutRow>
|
|
||||||
|
|
||||||
<div class="indent" :style="{ marginLeft: layerIndent(listing.entry) }"></div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
v-if="listing.entry.layerType === 'Folder'"
|
|
||||||
class="expand-arrow"
|
|
||||||
:class="{ expanded: listing.entry.layerMetadata.expanded }"
|
|
||||||
@click.stop="handleExpandArrowClick(listing.entry.path)"
|
|
||||||
tabindex="0"
|
|
||||||
></button>
|
|
||||||
<LayoutRow
|
|
||||||
class="layer"
|
|
||||||
:class="{ selected: fakeHighlight ? fakeHighlight.includes(listing.entry.path) : listing.entry.layerMetadata.selected }"
|
|
||||||
:data-layer="String(listing.entry.path)"
|
|
||||||
:data-index="index"
|
|
||||||
:title="listing.entry.tooltip"
|
|
||||||
:draggable="draggable"
|
|
||||||
@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 :icon="layerTypeData(listing.entry.layerType).icon" :title="layerTypeData(listing.entry.layerType).name" />
|
|
||||||
</LayoutRow>
|
|
||||||
<LayoutRow class="layer-name" @dblclick="() => onEditLayerName(listing)">
|
|
||||||
<input
|
|
||||||
data-text-input
|
|
||||||
type="text"
|
|
||||||
:value="listing.entry.name"
|
|
||||||
:placeholder="layerTypeData(listing.entry.layerType).name"
|
|
||||||
:disabled="!listing.editingName"
|
|
||||||
@blur="() => onEditLayerNameDeselect(listing)"
|
|
||||||
@keydown.esc="onEditLayerNameDeselect(listing)"
|
|
||||||
@keydown.enter="(e) => onEditLayerNameChange(listing, e)"
|
|
||||||
@change="(e) => onEditLayerNameChange(listing, e)"
|
|
||||||
/>
|
|
||||||
</LayoutRow>
|
|
||||||
<div class="thumbnail" v-html="listing.entry.thumbnail"></div>
|
|
||||||
</LayoutRow>
|
|
||||||
</LayoutRow>
|
|
||||||
</LayoutCol>
|
|
||||||
<div
|
|
||||||
class="insert-mark"
|
|
||||||
v-if="draggingData && !draggingData.highlightFolder && dragInPanel"
|
|
||||||
:style="{ left: markIndent(draggingData.insertFolder), top: markTopOffset(draggingData.markerHeight) }"
|
|
||||||
></div>
|
|
||||||
</LayoutRow>
|
|
||||||
</LayoutCol>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.layer-tree {
|
|
||||||
// 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
|
|
||||||
margin-bottom: -1px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.layer-row {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
height: 32px;
|
|
||||||
margin: 0 4px;
|
|
||||||
border-bottom: 1px solid var(--color-4-dimgray);
|
|
||||||
|
|
||||||
.visibility {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
height: 100%;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.icon-button {
|
|
||||||
height: 100%;
|
|
||||||
width: calc(24px + 2 * 4px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.expand-arrow {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
margin-left: -16px;
|
|
||||||
width: 16px;
|
|
||||||
height: 100%;
|
|
||||||
border: none;
|
|
||||||
position: relative;
|
|
||||||
background: none;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 2px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--color-6-lowergray);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 3px 0 3px 6px;
|
|
||||||
border-color: transparent transparent transparent var(--color-e-nearwhite);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--color-f-white);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.expanded::after {
|
|
||||||
border-width: 6px 3px 0 3px;
|
|
||||||
border-color: var(--color-e-nearwhite) transparent transparent transparent;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--color-f-white);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.layer {
|
|
||||||
align-items: center;
|
|
||||||
z-index: 1;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
padding: 0 4px;
|
|
||||||
border-radius: 2px;
|
|
||||||
margin-right: 8px;
|
|
||||||
|
|
||||||
&.selected {
|
|
||||||
background: var(--color-5-dullgray);
|
|
||||||
color: var(--color-f-white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.layer-type-icon {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
margin: 0 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layer-name {
|
|
||||||
flex: 1 1 100%;
|
|
||||||
margin: 0 4px;
|
|
||||||
|
|
||||||
input {
|
|
||||||
color: inherit;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
outline: none; // Ok for input element
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 2px;
|
|
||||||
height: 24px;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
-webkit-user-select: none; // Required as of Safari 15.0 (Graphite's minimum version) through the latest release
|
|
||||||
user-select: none;
|
|
||||||
// Workaround for `user-select: none` not working on <input> elements
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: inherit;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
background: var(--color-1-nearblack);
|
|
||||||
padding: 0 4px;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumbnail {
|
|
||||||
width: 36px;
|
|
||||||
height: 24px;
|
|
||||||
margin: 2px 0;
|
|
||||||
margin-left: 4px;
|
|
||||||
background: white;
|
|
||||||
border-radius: 2px;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: calc(100% - 4px);
|
|
||||||
height: calc(100% - 4px);
|
|
||||||
margin: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.insert-folder .layer {
|
|
||||||
outline: 3px solid var(--color-e-nearwhite);
|
|
||||||
outline-offset: -3px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.insert-mark {
|
|
||||||
position: absolute;
|
|
||||||
// `left` is applied dynamically
|
|
||||||
right: 0;
|
|
||||||
background: var(--color-e-nearwhite);
|
|
||||||
margin-top: -2px;
|
|
||||||
height: 5px;
|
|
||||||
z-index: 1;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, nextTick } from "vue";
|
import { defineComponent, nextTick } from "vue";
|
||||||
|
|
||||||
@@ -548,3 +279,272 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<LayoutCol class="layer-tree" @dragleave="dragInPanel = false">
|
||||||
|
<LayoutRow class="options-bar" :scrollableX="true">
|
||||||
|
<WidgetLayout :layout="layerTreeOptionsLayout" />
|
||||||
|
</LayoutRow>
|
||||||
|
<LayoutRow class="layer-tree-rows" :scrollableY="true">
|
||||||
|
<LayoutCol class="list" ref="list" @click="() => deselectAllLayers()" @dragover="(e: DragEvent) => draggable && updateInsertLine(e)" @dragend="() => draggable && drop()">
|
||||||
|
<LayoutRow
|
||||||
|
class="layer-row"
|
||||||
|
v-for="(listing, index) in layers"
|
||||||
|
:key="String(listing.entry.path.slice(-1))"
|
||||||
|
:class="{ 'insert-folder': draggingData?.highlightFolder && draggingData?.insertFolder === listing.entry.path }"
|
||||||
|
>
|
||||||
|
<LayoutRow class="visibility">
|
||||||
|
<IconButton
|
||||||
|
:action="(e?: MouseEvent) => (toggleLayerVisibility(listing.entry.path), e?.stopPropagation())"
|
||||||
|
:size="24"
|
||||||
|
:icon="listing.entry.visible ? 'EyeVisible' : 'EyeHidden'"
|
||||||
|
:title="listing.entry.visible ? 'Visible' : 'Hidden'"
|
||||||
|
/>
|
||||||
|
</LayoutRow>
|
||||||
|
|
||||||
|
<div class="indent" :style="{ marginLeft: layerIndent(listing.entry) }"></div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="listing.entry.layerType === 'Folder'"
|
||||||
|
class="expand-arrow"
|
||||||
|
:class="{ expanded: listing.entry.layerMetadata.expanded }"
|
||||||
|
@click.stop="handleExpandArrowClick(listing.entry.path)"
|
||||||
|
tabindex="0"
|
||||||
|
></button>
|
||||||
|
<LayoutRow
|
||||||
|
class="layer"
|
||||||
|
:class="{ selected: fakeHighlight ? fakeHighlight.includes(listing.entry.path) : listing.entry.layerMetadata.selected }"
|
||||||
|
:data-layer="String(listing.entry.path)"
|
||||||
|
:data-index="index"
|
||||||
|
:title="listing.entry.tooltip"
|
||||||
|
:draggable="draggable"
|
||||||
|
@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 :icon="layerTypeData(listing.entry.layerType).icon" :title="layerTypeData(listing.entry.layerType).name" />
|
||||||
|
</LayoutRow>
|
||||||
|
<LayoutRow class="layer-name" @dblclick="() => onEditLayerName(listing)">
|
||||||
|
<input
|
||||||
|
data-text-input
|
||||||
|
type="text"
|
||||||
|
:value="listing.entry.name"
|
||||||
|
:placeholder="layerTypeData(listing.entry.layerType).name"
|
||||||
|
:disabled="!listing.editingName"
|
||||||
|
@blur="() => onEditLayerNameDeselect(listing)"
|
||||||
|
@keydown.esc="onEditLayerNameDeselect(listing)"
|
||||||
|
@keydown.enter="(e) => onEditLayerNameChange(listing, e)"
|
||||||
|
@change="(e) => onEditLayerNameChange(listing, e)"
|
||||||
|
/>
|
||||||
|
</LayoutRow>
|
||||||
|
<div class="thumbnail" v-html="listing.entry.thumbnail"></div>
|
||||||
|
</LayoutRow>
|
||||||
|
</LayoutRow>
|
||||||
|
</LayoutCol>
|
||||||
|
<div
|
||||||
|
class="insert-mark"
|
||||||
|
v-if="draggingData && !draggingData.highlightFolder && dragInPanel"
|
||||||
|
:style="{ left: markIndent(draggingData.insertFolder), top: markTopOffset(draggingData.markerHeight) }"
|
||||||
|
></div>
|
||||||
|
</LayoutRow>
|
||||||
|
</LayoutCol>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.layer-tree {
|
||||||
|
// 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
|
||||||
|
margin-bottom: -1px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.layer-row {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
height: 32px;
|
||||||
|
margin: 0 4px;
|
||||||
|
border-bottom: 1px solid var(--color-4-dimgray);
|
||||||
|
|
||||||
|
.visibility {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
height: 100%;
|
||||||
|
width: calc(24px + 2 * 4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-arrow {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
margin-left: -16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
position: relative;
|
||||||
|
background: none;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 2px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-6-lowergray);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 3px 0 3px 6px;
|
||||||
|
border-color: transparent transparent transparent var(--color-e-nearwhite);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-f-white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.expanded::after {
|
||||||
|
border-width: 6px 3px 0 3px;
|
||||||
|
border-color: var(--color-e-nearwhite) transparent transparent transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-f-white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer {
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin-right: 8px;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background: var(--color-5-dullgray);
|
||||||
|
color: var(--color-f-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-type-icon {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-name {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
margin: 0 4px;
|
||||||
|
|
||||||
|
input {
|
||||||
|
color: inherit;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
outline: none; // Ok for input element
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 2px;
|
||||||
|
height: 24px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
-webkit-user-select: none; // Required as of Safari 15.0 (Graphite's minimum version) through the latest release
|
||||||
|
user-select: none;
|
||||||
|
// Workaround for `user-select: none` not working on <input> elements
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: inherit;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background: var(--color-1-nearblack);
|
||||||
|
padding: 0 4px;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail {
|
||||||
|
width: 36px;
|
||||||
|
height: 24px;
|
||||||
|
margin: 2px 0;
|
||||||
|
margin-left: 4px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 2px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: calc(100% - 4px);
|
||||||
|
height: calc(100% - 4px);
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.insert-folder .layer {
|
||||||
|
outline: 3px solid var(--color-e-nearwhite);
|
||||||
|
outline-offset: -3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.insert-mark {
|
||||||
|
position: absolute;
|
||||||
|
// `left` is applied dynamically
|
||||||
|
right: 0;
|
||||||
|
background: var(--color-e-nearwhite);
|
||||||
|
margin-top: -2px;
|
||||||
|
height: 5px;
|
||||||
|
z-index: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,338 +1,3 @@
|
|||||||
<template>
|
|
||||||
<LayoutCol class="node-graph">
|
|
||||||
<LayoutRow class="options-bar"><WidgetLayout :layout="nodeGraphBarLayout" /></LayoutRow>
|
|
||||||
<LayoutRow
|
|
||||||
class="graph"
|
|
||||||
ref="graph"
|
|
||||||
@wheel="(e: WheelEvent) => scroll(e)"
|
|
||||||
@pointerdown="(e: PointerEvent) => pointerDown(e)"
|
|
||||||
@pointermove="(e: PointerEvent) => pointerMove(e)"
|
|
||||||
@pointerup="(e: PointerEvent) => pointerUp(e)"
|
|
||||||
@dblclick="(e: MouseEvent) => doubleClick(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`,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<LayoutCol class="node-list" data-node-list v-if="nodeListLocation" :style="{ marginLeft: `${nodeListX}px`, marginTop: `${nodeListY}px` }">
|
|
||||||
<TextInput placeholder="Search Nodes..." :value="searchTerm" @update:value="(val) => (searchTerm = val)" v-focus />
|
|
||||||
<LayoutCol v-for="nodeCategory in nodeCategories" :key="nodeCategory[0]">
|
|
||||||
<TextLabel>{{ nodeCategory[0] }}</TextLabel>
|
|
||||||
<TextButton v-for="nodeType in nodeCategory[1]" v-bind:key="String(nodeType)" :label="nodeType.name" :action="() => createNode(nodeType.name)" />
|
|
||||||
</LayoutCol>
|
|
||||||
<TextLabel v-if="nodeCategories.length === 0">No search results :(</TextLabel>
|
|
||||||
</LayoutCol>
|
|
||||||
<div
|
|
||||||
class="nodes"
|
|
||||||
ref="nodesContainer"
|
|
||||||
:style="{
|
|
||||||
transform: `scale(${transform.scale}) translate(${transform.x}px, ${transform.y}px)`,
|
|
||||||
transformOrigin: `0 0`,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="node in nodes"
|
|
||||||
:key="String(node.id)"
|
|
||||||
class="node"
|
|
||||||
:class="{ selected: selected.includes(node.id), previewed: node.previewed, disabled: node.disabled }"
|
|
||||||
:style="{
|
|
||||||
'--offset-left': (node.position?.x || 0) + (selected.includes(node.id) ? draggingNodes?.roundX || 0 : 0),
|
|
||||||
'--offset-top': (node.position?.y || 0) + (selected.includes(node.id) ? draggingNodes?.roundY || 0 : 0),
|
|
||||||
}"
|
|
||||||
:data-node="node.id"
|
|
||||||
>
|
|
||||||
<div class="primary">
|
|
||||||
<div class="ports">
|
|
||||||
<div
|
|
||||||
v-if="node.primaryInput"
|
|
||||||
class="input port"
|
|
||||||
data-port="input"
|
|
||||||
:data-datatype="node.primaryInput"
|
|
||||||
:style="{ '--data-color': `var(--color-data-${node.primaryInput})`, '--data-color-dim': `var(--color-data-${node.primaryInput}-dim)` }"
|
|
||||||
>
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="node.outputs.length > 0"
|
|
||||||
class="output port"
|
|
||||||
data-port="output"
|
|
||||||
:data-datatype="node.outputs[0].dataType"
|
|
||||||
:style="{ '--data-color': `var(--color-data-${node.outputs[0].dataType})`, '--data-color-dim': `var(--color-data-${node.outputs[0].dataType}-dim)` }"
|
|
||||||
>
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<IconLabel :icon="nodeIcon(node.displayName)" />
|
|
||||||
<TextLabel>{{ node.displayName }}</TextLabel>
|
|
||||||
</div>
|
|
||||||
<div v-if="[...node.exposedInputs, ...node.outputs.slice(1)].length > 0" class="parameters">
|
|
||||||
<div v-for="(parameter, index) in [...node.exposedInputs, ...node.outputs.slice(1)]" :key="index" class="parameter">
|
|
||||||
<div class="ports">
|
|
||||||
<div
|
|
||||||
v-if="index < node.exposedInputs.length"
|
|
||||||
class="input port"
|
|
||||||
data-port="input"
|
|
||||||
:data-datatype="parameter.dataType"
|
|
||||||
:style="{
|
|
||||||
'--data-color': `var(--color-data-${parameter.dataType})`,
|
|
||||||
'--data-color-dim': `var(--color-data-${parameter.dataType}-dim)`,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="output port"
|
|
||||||
data-port="output"
|
|
||||||
:data-datatype="parameter.dataType"
|
|
||||||
:style="{ '--data-color': `var(--color-data-${parameter.dataType})`, '--data-color-dim': `var(--color-data-${parameter.dataType}-dim)` }"
|
|
||||||
>
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<TextLabel :class="index < node.exposedInputs.length ? 'name' : 'output'">{{ parameter.name }}</TextLabel>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="wires"
|
|
||||||
:style="{
|
|
||||||
transform: `scale(${transform.scale}) translate(${transform.x}px, ${transform.y}px)`,
|
|
||||||
transformOrigin: `0 0`,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<svg>
|
|
||||||
<path
|
|
||||||
v-for="([pathString, dataType], index) in linkPaths"
|
|
||||||
:key="index"
|
|
||||||
:d="pathString"
|
|
||||||
:style="{ '--data-color': `var(--color-data-${dataType})`, '--data-color-dim': `var(--color-data-${dataType}-dim)` }"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</LayoutRow>
|
|
||||||
</LayoutCol>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.node-graph {
|
|
||||||
height: 100%;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.node-list {
|
|
||||||
width: max-content;
|
|
||||||
position: fixed;
|
|
||||||
padding: 5px;
|
|
||||||
z-index: 3;
|
|
||||||
background-color: var(--color-3-darkgray);
|
|
||||||
|
|
||||||
.text-button + .text-button {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.options-bar {
|
|
||||||
height: 32px;
|
|
||||||
margin: 0 4px;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.widget-layout {
|
|
||||||
flex-direction: row;
|
|
||||||
flex-grow: 1;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.graph {
|
|
||||||
position: relative;
|
|
||||||
background: var(--color-2-mildblack);
|
|
||||||
width: calc(100% - 8px);
|
|
||||||
margin-left: 4px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
border-radius: 2px;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
// We're displaying the dotted grid in a pseudo-element because `image-rendering` is an inherited property and we don't want it to apply to child elements
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-size: var(--grid-spacing) var(--grid-spacing);
|
|
||||||
background-position: calc(var(--grid-offset-x) - var(--dot-radius)) calc(var(--grid-offset-y) - var(--dot-radius));
|
|
||||||
background-image: radial-gradient(circle at var(--dot-radius) var(--dot-radius), var(--color-3-darkgray) var(--dot-radius), transparent 0);
|
|
||||||
image-rendering: pixelated;
|
|
||||||
mix-blend-mode: screen;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodes,
|
|
||||||
.wires {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
&.wires {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: visible;
|
|
||||||
|
|
||||||
path {
|
|
||||||
fill: none;
|
|
||||||
// stroke: var(--color-data-raster-dim);
|
|
||||||
stroke: var(--data-color-dim);
|
|
||||||
stroke-width: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.nodes {
|
|
||||||
.node {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-width: 120px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: var(--color-4-dimgray);
|
|
||||||
left: calc((var(--offset-left) + 0.5) * 24px);
|
|
||||||
top: calc((var(--offset-top) - 0.5) * 24px);
|
|
||||||
|
|
||||||
&.selected {
|
|
||||||
border: 1px solid var(--color-e-nearwhite);
|
|
||||||
margin: -1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
background: var(--color-3-darkgray);
|
|
||||||
color: var(--color-a-softgray);
|
|
||||||
|
|
||||||
.icon-label {
|
|
||||||
fill: var(--color-a-softgray);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.previewed {
|
|
||||||
outline: 3px solid var(--color-data-vector);
|
|
||||||
}
|
|
||||||
|
|
||||||
.primary {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
gap: 4px;
|
|
||||||
width: 100%;
|
|
||||||
height: 24px;
|
|
||||||
background: var(--color-5-dullgray);
|
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
.icon-label {
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-label {
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.parameters {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.parameter {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 24px;
|
|
||||||
width: calc(100% - 24px * 2);
|
|
||||||
margin-left: 24px;
|
|
||||||
margin-right: 24px;
|
|
||||||
|
|
||||||
.text-label {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&.output {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Squares to cover up the rounded corners of the primary area and make them have a straight edge
|
|
||||||
&::before,
|
|
||||||
&::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
background: var(--color-5-dullgray);
|
|
||||||
width: 4px;
|
|
||||||
height: 4px;
|
|
||||||
top: -4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ports {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
.port {
|
|
||||||
position: absolute;
|
|
||||||
margin: auto 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--data-color-dim);
|
|
||||||
// background: var(--color-data-raster-dim);
|
|
||||||
|
|
||||||
div {
|
|
||||||
background: var(--data-color);
|
|
||||||
// background: var(--color-data-raster);
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.input {
|
|
||||||
left: calc(-12px - 6px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.output {
|
|
||||||
right: calc(-12px - 6px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, nextTick } from "vue";
|
import { defineComponent, nextTick } from "vue";
|
||||||
|
|
||||||
@@ -812,3 +477,338 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<LayoutCol class="node-graph">
|
||||||
|
<LayoutRow class="options-bar"><WidgetLayout :layout="nodeGraphBarLayout" /></LayoutRow>
|
||||||
|
<LayoutRow
|
||||||
|
class="graph"
|
||||||
|
ref="graph"
|
||||||
|
@wheel="(e: WheelEvent) => scroll(e)"
|
||||||
|
@pointerdown="(e: PointerEvent) => pointerDown(e)"
|
||||||
|
@pointermove="(e: PointerEvent) => pointerMove(e)"
|
||||||
|
@pointerup="(e: PointerEvent) => pointerUp(e)"
|
||||||
|
@dblclick="(e: MouseEvent) => doubleClick(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`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<LayoutCol class="node-list" data-node-list v-if="nodeListLocation" :style="{ marginLeft: `${nodeListX}px`, marginTop: `${nodeListY}px` }">
|
||||||
|
<TextInput placeholder="Search Nodes..." :value="searchTerm" @update:value="(val) => (searchTerm = val)" v-focus />
|
||||||
|
<LayoutCol v-for="nodeCategory in nodeCategories" :key="nodeCategory[0]">
|
||||||
|
<TextLabel>{{ nodeCategory[0] }}</TextLabel>
|
||||||
|
<TextButton v-for="nodeType in nodeCategory[1]" v-bind:key="String(nodeType)" :label="nodeType.name" :action="() => createNode(nodeType.name)" />
|
||||||
|
</LayoutCol>
|
||||||
|
<TextLabel v-if="nodeCategories.length === 0">No search results :(</TextLabel>
|
||||||
|
</LayoutCol>
|
||||||
|
<div
|
||||||
|
class="nodes"
|
||||||
|
ref="nodesContainer"
|
||||||
|
:style="{
|
||||||
|
transform: `scale(${transform.scale}) translate(${transform.x}px, ${transform.y}px)`,
|
||||||
|
transformOrigin: `0 0`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="node in nodes"
|
||||||
|
:key="String(node.id)"
|
||||||
|
class="node"
|
||||||
|
:class="{ selected: selected.includes(node.id), previewed: node.previewed, disabled: node.disabled }"
|
||||||
|
:style="{
|
||||||
|
'--offset-left': (node.position?.x || 0) + (selected.includes(node.id) ? draggingNodes?.roundX || 0 : 0),
|
||||||
|
'--offset-top': (node.position?.y || 0) + (selected.includes(node.id) ? draggingNodes?.roundY || 0 : 0),
|
||||||
|
}"
|
||||||
|
:data-node="node.id"
|
||||||
|
>
|
||||||
|
<div class="primary">
|
||||||
|
<div class="ports">
|
||||||
|
<div
|
||||||
|
v-if="node.primaryInput"
|
||||||
|
class="input port"
|
||||||
|
data-port="input"
|
||||||
|
:data-datatype="node.primaryInput"
|
||||||
|
:style="{ '--data-color': `var(--color-data-${node.primaryInput})`, '--data-color-dim': `var(--color-data-${node.primaryInput}-dim)` }"
|
||||||
|
>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="node.outputs.length > 0"
|
||||||
|
class="output port"
|
||||||
|
data-port="output"
|
||||||
|
:data-datatype="node.outputs[0].dataType"
|
||||||
|
:style="{ '--data-color': `var(--color-data-${node.outputs[0].dataType})`, '--data-color-dim': `var(--color-data-${node.outputs[0].dataType}-dim)` }"
|
||||||
|
>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<IconLabel :icon="nodeIcon(node.displayName)" />
|
||||||
|
<TextLabel>{{ node.displayName }}</TextLabel>
|
||||||
|
</div>
|
||||||
|
<div v-if="[...node.exposedInputs, ...node.outputs.slice(1)].length > 0" class="parameters">
|
||||||
|
<div v-for="(parameter, index) in [...node.exposedInputs, ...node.outputs.slice(1)]" :key="index" class="parameter">
|
||||||
|
<div class="ports">
|
||||||
|
<div
|
||||||
|
v-if="index < node.exposedInputs.length"
|
||||||
|
class="input port"
|
||||||
|
data-port="input"
|
||||||
|
:data-datatype="parameter.dataType"
|
||||||
|
:style="{
|
||||||
|
'--data-color': `var(--color-data-${parameter.dataType})`,
|
||||||
|
'--data-color-dim': `var(--color-data-${parameter.dataType}-dim)`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="output port"
|
||||||
|
data-port="output"
|
||||||
|
:data-datatype="parameter.dataType"
|
||||||
|
:style="{ '--data-color': `var(--color-data-${parameter.dataType})`, '--data-color-dim': `var(--color-data-${parameter.dataType}-dim)` }"
|
||||||
|
>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TextLabel :class="index < node.exposedInputs.length ? 'name' : 'output'">{{ parameter.name }}</TextLabel>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="wires"
|
||||||
|
:style="{
|
||||||
|
transform: `scale(${transform.scale}) translate(${transform.x}px, ${transform.y}px)`,
|
||||||
|
transformOrigin: `0 0`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<svg>
|
||||||
|
<path
|
||||||
|
v-for="([pathString, dataType], index) in linkPaths"
|
||||||
|
:key="index"
|
||||||
|
:d="pathString"
|
||||||
|
:style="{ '--data-color': `var(--color-data-${dataType})`, '--data-color-dim': `var(--color-data-${dataType}-dim)` }"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</LayoutRow>
|
||||||
|
</LayoutCol>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.node-graph {
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.node-list {
|
||||||
|
width: max-content;
|
||||||
|
position: fixed;
|
||||||
|
padding: 5px;
|
||||||
|
z-index: 3;
|
||||||
|
background-color: var(--color-3-darkgray);
|
||||||
|
|
||||||
|
.text-button + .text-button {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-bar {
|
||||||
|
height: 32px;
|
||||||
|
margin: 0 4px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.widget-layout {
|
||||||
|
flex-direction: row;
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph {
|
||||||
|
position: relative;
|
||||||
|
background: var(--color-2-mildblack);
|
||||||
|
width: calc(100% - 8px);
|
||||||
|
margin-left: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
// We're displaying the dotted grid in a pseudo-element because `image-rendering` is an inherited property and we don't want it to apply to child elements
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-size: var(--grid-spacing) var(--grid-spacing);
|
||||||
|
background-position: calc(var(--grid-offset-x) - var(--dot-radius)) calc(var(--grid-offset-y) - var(--dot-radius));
|
||||||
|
background-image: radial-gradient(circle at var(--dot-radius) var(--dot-radius), var(--color-3-darkgray) var(--dot-radius), transparent 0);
|
||||||
|
image-rendering: pixelated;
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nodes,
|
||||||
|
.wires {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&.wires {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
path {
|
||||||
|
fill: none;
|
||||||
|
// stroke: var(--color-data-raster-dim);
|
||||||
|
stroke: var(--data-color-dim);
|
||||||
|
stroke-width: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.nodes {
|
||||||
|
.node {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 120px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--color-4-dimgray);
|
||||||
|
left: calc((var(--offset-left) + 0.5) * 24px);
|
||||||
|
top: calc((var(--offset-top) - 0.5) * 24px);
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
border: 1px solid var(--color-e-nearwhite);
|
||||||
|
margin: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
background: var(--color-3-darkgray);
|
||||||
|
color: var(--color-a-softgray);
|
||||||
|
|
||||||
|
.icon-label {
|
||||||
|
fill: var(--color-a-softgray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.previewed {
|
||||||
|
outline: 3px solid var(--color-data-vector);
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
gap: 4px;
|
||||||
|
width: 100%;
|
||||||
|
height: 24px;
|
||||||
|
background: var(--color-5-dullgray);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.icon-label {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-label {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameters {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.parameter {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 24px;
|
||||||
|
width: calc(100% - 24px * 2);
|
||||||
|
margin-left: 24px;
|
||||||
|
margin-right: 24px;
|
||||||
|
|
||||||
|
.text-label {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&.output {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Squares to cover up the rounded corners of the primary area and make them have a straight edge
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
background: var(--color-5-dullgray);
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
top: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ports {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.port {
|
||||||
|
position: absolute;
|
||||||
|
margin: auto 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--data-color-dim);
|
||||||
|
// background: var(--color-data-raster-dim);
|
||||||
|
|
||||||
|
div {
|
||||||
|
background: var(--data-color);
|
||||||
|
// background: var(--color-data-raster);
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.input {
|
||||||
|
left: calc(-12px - 6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.output {
|
||||||
|
right: calc(-12px - 6px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,3 +1,37 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
|
import { defaultWidgetLayout, patchWidgetLayout, UpdatePropertyPanelOptionsLayout, UpdatePropertyPanelSectionsLayout } from "@/wasm-communication/messages";
|
||||||
|
|
||||||
|
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||||
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
|
import WidgetLayout from "@/components/widgets/WidgetLayout.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
inject: ["editor", "dialog"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
propertiesOptionsLayout: defaultWidgetLayout(),
|
||||||
|
propertiesSectionsLayout: defaultWidgetLayout(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.editor.subscriptions.subscribeJsMessage(UpdatePropertyPanelOptionsLayout, (updatePropertyPanelOptionsLayout) => {
|
||||||
|
patchWidgetLayout(this.propertiesOptionsLayout, updatePropertyPanelOptionsLayout);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.editor.subscriptions.subscribeJsMessage(UpdatePropertyPanelSectionsLayout, (updatePropertyPanelSectionsLayout) => {
|
||||||
|
patchWidgetLayout(this.propertiesSectionsLayout, updatePropertyPanelSectionsLayout);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
LayoutCol,
|
||||||
|
LayoutRow,
|
||||||
|
WidgetLayout,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutCol class="properties">
|
<LayoutCol class="properties">
|
||||||
<LayoutRow class="options-bar">
|
<LayoutRow class="options-bar">
|
||||||
@@ -32,37 +66,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
import { defaultWidgetLayout, patchWidgetLayout, UpdatePropertyPanelOptionsLayout, UpdatePropertyPanelSectionsLayout } from "@/wasm-communication/messages";
|
|
||||||
|
|
||||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|
||||||
import WidgetLayout from "@/components/widgets/WidgetLayout.vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
inject: ["editor", "dialog"],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
propertiesOptionsLayout: defaultWidgetLayout(),
|
|
||||||
propertiesSectionsLayout: defaultWidgetLayout(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.editor.subscriptions.subscribeJsMessage(UpdatePropertyPanelOptionsLayout, (updatePropertyPanelOptionsLayout) => {
|
|
||||||
patchWidgetLayout(this.propertiesOptionsLayout, updatePropertyPanelOptionsLayout);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.editor.subscriptions.subscribeJsMessage(UpdatePropertyPanelSectionsLayout, (updatePropertyPanelSectionsLayout) => {
|
|
||||||
patchWidgetLayout(this.propertiesSectionsLayout, updatePropertyPanelSectionsLayout);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
LayoutCol,
|
|
||||||
LayoutRow,
|
|
||||||
WidgetLayout,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,20 +1,3 @@
|
|||||||
<!-- TODO: Refactor this component (together with `WidgetRow.vue`) to be more logically consistent with our layout definition goals, in terms of naming and capabilities -->
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="widget-layout">
|
|
||||||
<component :is="layoutGroupType(layoutRow)" :widgetData="layoutRow" :layoutTarget="layout.layoutTarget" v-for="(layoutRow, index) in layout.layout" :key="index" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.widget-layout {
|
|
||||||
height: 100%;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
@@ -42,3 +25,20 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- TODO: Refactor this component (together with `WidgetRow.vue`) to be more logically consistent with our layout definition goals, in terms of naming and capabilities -->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="widget-layout">
|
||||||
|
<component :is="layoutGroupType(layoutRow)" :widgetData="layoutRow" :layoutTarget="layout.layoutTarget" v-for="(layoutRow, index) in layout.layout" :key="index" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.widget-layout {
|
||||||
|
height: 100%;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,3 +1,102 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
import { debouncer } from "@/components/widgets/debounce";
|
||||||
|
import { isWidgetColumn, isWidgetRow, type WidgetColumn, type WidgetRow, type Widget } from "@/wasm-communication/messages";
|
||||||
|
|
||||||
|
import PivotAssist from "@/components/widgets/assists/PivotAssist.vue";
|
||||||
|
import BreadcrumbTrailButtons from "@/components/widgets/buttons/BreadcrumbTrailButtons.vue";
|
||||||
|
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
||||||
|
import ParameterExposeButton from "@/components/widgets/buttons/ParameterExposeButton.vue";
|
||||||
|
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
|
||||||
|
import TextButton from "@/components/widgets/buttons/TextButton.vue";
|
||||||
|
import CheckboxInput from "@/components/widgets/inputs/CheckboxInput.vue";
|
||||||
|
import ColorInput from "@/components/widgets/inputs/ColorInput.vue";
|
||||||
|
import DropdownInput from "@/components/widgets/inputs/DropdownInput.vue";
|
||||||
|
import FontInput from "@/components/widgets/inputs/FontInput.vue";
|
||||||
|
import LayerReferenceInput from "@/components/widgets/inputs/LayerReferenceInput.vue";
|
||||||
|
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
|
||||||
|
import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue";
|
||||||
|
import RadioInput from "@/components/widgets/inputs/RadioInput.vue";
|
||||||
|
import SwatchPairInput from "@/components/widgets/inputs/SwatchPairInput.vue";
|
||||||
|
import TextAreaInput from "@/components/widgets/inputs/TextAreaInput.vue";
|
||||||
|
import TextInput from "@/components/widgets/inputs/TextInput.vue";
|
||||||
|
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||||
|
import Separator from "@/components/widgets/labels/Separator.vue";
|
||||||
|
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||||
|
|
||||||
|
const SUFFIX_WIDGETS = ["PopoverButton"];
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
inject: ["editor"],
|
||||||
|
props: {
|
||||||
|
widgetData: { type: Object as PropType<WidgetColumn | WidgetRow>, required: true },
|
||||||
|
layoutTarget: { required: true },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
open: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
direction(): "column" | "row" | "ERROR" {
|
||||||
|
if (isWidgetColumn(this.widgetData)) return "column";
|
||||||
|
if (isWidgetRow(this.widgetData)) return "row";
|
||||||
|
return "ERROR";
|
||||||
|
},
|
||||||
|
widgets() {
|
||||||
|
let widgets: Widget[] = [];
|
||||||
|
if (isWidgetColumn(this.widgetData)) widgets = this.widgetData.columnWidgets;
|
||||||
|
if (isWidgetRow(this.widgetData)) widgets = this.widgetData.rowWidgets;
|
||||||
|
return widgets;
|
||||||
|
},
|
||||||
|
widgetsAndNextSiblingIsSuffix(): [Widget, boolean][] {
|
||||||
|
return this.widgets.map((widget, index): [Widget, boolean] => {
|
||||||
|
// A suffix widget is one that joins up with this widget at the end with only a 1px gap.
|
||||||
|
// It uses the CSS sibling selector to give its own left edge corners zero radius.
|
||||||
|
// But this JS is needed to set its preceding sibling widget's right edge corners to zero radius.
|
||||||
|
const nextSiblingIsSuffix = SUFFIX_WIDGETS.includes(this.widgets[index + 1]?.props.kind);
|
||||||
|
|
||||||
|
return [widget, nextSiblingIsSuffix];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateLayout(index: number, value: unknown) {
|
||||||
|
this.editor.instance.updateLayout(this.layoutTarget, this.widgets[index].widgetId, value);
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
withoutValue(props: Record<string, any>): Record<string, unknown> {
|
||||||
|
const { value: _, ...rest } = props;
|
||||||
|
return rest;
|
||||||
|
},
|
||||||
|
debouncer,
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
BreadcrumbTrailButtons,
|
||||||
|
CheckboxInput,
|
||||||
|
ColorInput,
|
||||||
|
DropdownInput,
|
||||||
|
FontInput,
|
||||||
|
IconButton,
|
||||||
|
IconLabel,
|
||||||
|
LayerReferenceInput,
|
||||||
|
NumberInput,
|
||||||
|
OptionalInput,
|
||||||
|
ParameterExposeButton,
|
||||||
|
PivotAssist,
|
||||||
|
PopoverButton,
|
||||||
|
RadioInput,
|
||||||
|
Separator,
|
||||||
|
SwatchPairInput,
|
||||||
|
TextAreaInput,
|
||||||
|
TextButton,
|
||||||
|
TextInput,
|
||||||
|
TextLabel,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- TODO: Refactor this component to use `<component :is="" v-bind="attributesObject"></component>` to avoid all the separate components with `v-if` -->
|
<!-- TODO: Refactor this component to use `<component :is="" v-bind="attributesObject"></component>` to avoid all the separate components with `v-if` -->
|
||||||
<!-- TODO: Also rename this component, and probably move the `widget-${direction}` wrapper to be part of `WidgetLayout.vue` as part of its refactor -->
|
<!-- TODO: Also rename this component, and probably move the `widget-${direction}` wrapper to be part of `WidgetLayout.vue` as part of its refactor -->
|
||||||
|
|
||||||
@@ -112,103 +211,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
import { debouncer } from "@/components/widgets/debounce";
|
|
||||||
import { isWidgetColumn, isWidgetRow, type WidgetColumn, type WidgetRow, type Widget } from "@/wasm-communication/messages";
|
|
||||||
|
|
||||||
import PivotAssist from "@/components/widgets/assists/PivotAssist.vue";
|
|
||||||
import BreadcrumbTrailButtons from "@/components/widgets/buttons/BreadcrumbTrailButtons.vue";
|
|
||||||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
|
||||||
import ParameterExposeButton from "@/components/widgets/buttons/ParameterExposeButton.vue";
|
|
||||||
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
|
|
||||||
import TextButton from "@/components/widgets/buttons/TextButton.vue";
|
|
||||||
import CheckboxInput from "@/components/widgets/inputs/CheckboxInput.vue";
|
|
||||||
import ColorInput from "@/components/widgets/inputs/ColorInput.vue";
|
|
||||||
import DropdownInput from "@/components/widgets/inputs/DropdownInput.vue";
|
|
||||||
import FontInput from "@/components/widgets/inputs/FontInput.vue";
|
|
||||||
import LayerReferenceInput from "@/components/widgets/inputs/LayerReferenceInput.vue";
|
|
||||||
import NumberInput from "@/components/widgets/inputs/NumberInput.vue";
|
|
||||||
import OptionalInput from "@/components/widgets/inputs/OptionalInput.vue";
|
|
||||||
import RadioInput from "@/components/widgets/inputs/RadioInput.vue";
|
|
||||||
import SwatchPairInput from "@/components/widgets/inputs/SwatchPairInput.vue";
|
|
||||||
import TextAreaInput from "@/components/widgets/inputs/TextAreaInput.vue";
|
|
||||||
import TextInput from "@/components/widgets/inputs/TextInput.vue";
|
|
||||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
|
||||||
import Separator from "@/components/widgets/labels/Separator.vue";
|
|
||||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
|
||||||
|
|
||||||
const SUFFIX_WIDGETS = ["PopoverButton"];
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
inject: ["editor"],
|
|
||||||
props: {
|
|
||||||
widgetData: { type: Object as PropType<WidgetColumn | WidgetRow>, required: true },
|
|
||||||
layoutTarget: { required: true },
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
open: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
direction(): "column" | "row" | "ERROR" {
|
|
||||||
if (isWidgetColumn(this.widgetData)) return "column";
|
|
||||||
if (isWidgetRow(this.widgetData)) return "row";
|
|
||||||
return "ERROR";
|
|
||||||
},
|
|
||||||
widgets() {
|
|
||||||
let widgets: Widget[] = [];
|
|
||||||
if (isWidgetColumn(this.widgetData)) widgets = this.widgetData.columnWidgets;
|
|
||||||
if (isWidgetRow(this.widgetData)) widgets = this.widgetData.rowWidgets;
|
|
||||||
return widgets;
|
|
||||||
},
|
|
||||||
widgetsAndNextSiblingIsSuffix(): [Widget, boolean][] {
|
|
||||||
return this.widgets.map((widget, index): [Widget, boolean] => {
|
|
||||||
// A suffix widget is one that joins up with this widget at the end with only a 1px gap.
|
|
||||||
// It uses the CSS sibling selector to give its own left edge corners zero radius.
|
|
||||||
// But this JS is needed to set its preceding sibling widget's right edge corners to zero radius.
|
|
||||||
const nextSiblingIsSuffix = SUFFIX_WIDGETS.includes(this.widgets[index + 1]?.props.kind);
|
|
||||||
|
|
||||||
return [widget, nextSiblingIsSuffix];
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
updateLayout(index: number, value: unknown) {
|
|
||||||
this.editor.instance.updateLayout(this.layoutTarget, this.widgets[index].widgetId, value);
|
|
||||||
},
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
withoutValue(props: Record<string, any>): Record<string, unknown> {
|
|
||||||
const { value: _, ...rest } = props;
|
|
||||||
return rest;
|
|
||||||
},
|
|
||||||
debouncer,
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
BreadcrumbTrailButtons,
|
|
||||||
CheckboxInput,
|
|
||||||
ColorInput,
|
|
||||||
DropdownInput,
|
|
||||||
FontInput,
|
|
||||||
IconButton,
|
|
||||||
IconLabel,
|
|
||||||
LayerReferenceInput,
|
|
||||||
NumberInput,
|
|
||||||
OptionalInput,
|
|
||||||
ParameterExposeButton,
|
|
||||||
PivotAssist,
|
|
||||||
PopoverButton,
|
|
||||||
RadioInput,
|
|
||||||
Separator,
|
|
||||||
SwatchPairInput,
|
|
||||||
TextAreaInput,
|
|
||||||
TextButton,
|
|
||||||
TextInput,
|
|
||||||
TextLabel,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
import { type PivotPosition } from "@/wasm-communication/messages";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
emits: ["update:position"],
|
||||||
|
props: {
|
||||||
|
position: { type: String as PropType<string>, required: true },
|
||||||
|
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setPosition(newPosition: PivotPosition) {
|
||||||
|
this.$emit("update:position", newPosition);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="pivot-assist" :class="{ disabled }">
|
<div class="pivot-assist" :class="{ disabled }">
|
||||||
<button @click="setPosition('TopLeft')" class="row-1 col-1" :class="{ active: position === 'TopLeft' }" tabindex="-1" :disabled="disabled"><div></div></button>
|
<button @click="setPosition('TopLeft')" class="row-1 col-1" :class="{ active: position === 'TopLeft' }" tabindex="-1" :disabled="disabled"><div></div></button>
|
||||||
@@ -101,21 +120,3 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
import { type PivotPosition } from "@/wasm-communication/messages";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
emits: ["update:position"],
|
|
||||||
props: {
|
|
||||||
position: { type: String as PropType<string>, required: true },
|
|
||||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
setPosition(newPosition: PivotPosition) {
|
|
||||||
this.$emit("update:position", newPosition);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,25 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
|
import TextButton from "@/components/widgets/buttons/TextButton.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
labels: { type: Array as PropType<string[]>, required: true },
|
||||||
|
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
action: { type: Function as PropType<(index: number) => void>, required: true },
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
LayoutRow,
|
||||||
|
TextButton,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutRow class="breadcrumb-trail-buttons" :title="tooltip">
|
<LayoutRow class="breadcrumb-trail-buttons" :title="tooltip">
|
||||||
<TextButton
|
<TextButton
|
||||||
@@ -57,25 +79,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|
||||||
import TextButton from "@/components/widgets/buttons/TextButton.vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
labels: { type: Array as PropType<string[]>, required: true },
|
|
||||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
|
||||||
|
|
||||||
// Callbacks
|
|
||||||
action: { type: Function as PropType<(index: number) => void>, required: true },
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
LayoutRow,
|
|
||||||
TextButton,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,26 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
import { type IconName, type IconSize } from "@/utility-functions/icons";
|
||||||
|
|
||||||
|
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
icon: { type: String as PropType<IconName>, required: true },
|
||||||
|
size: { type: Number as PropType<IconSize>, required: true },
|
||||||
|
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
active: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||||
|
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
action: { type: Function as PropType<(e?: MouseEvent) => void>, required: true },
|
||||||
|
},
|
||||||
|
components: { IconLabel },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
class="icon-button"
|
class="icon-button"
|
||||||
@@ -78,26 +101,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
import { type IconName, type IconSize } from "@/utility-functions/icons";
|
|
||||||
|
|
||||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
icon: { type: String as PropType<IconName>, required: true },
|
|
||||||
size: { type: Number as PropType<IconSize>, required: true },
|
|
||||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
active: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
|
||||||
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
|
|
||||||
// Callbacks
|
|
||||||
action: { type: Function as PropType<(e?: MouseEvent) => void>, required: true },
|
|
||||||
},
|
|
||||||
components: { IconLabel },
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
exposed: { type: Boolean as PropType<boolean>, required: true },
|
||||||
|
dataType: { type: String as PropType<string>, required: true },
|
||||||
|
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
action: { type: Function as PropType<(e?: MouseEvent) => void>, required: true },
|
||||||
|
},
|
||||||
|
components: { LayoutRow },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutRow class="parameter-expose-button">
|
<LayoutRow class="parameter-expose-button">
|
||||||
<button :class="{ exposed }" :style="{ '--data-type-color': `var(--color-data-${dataType})` }" @click="(e: MouseEvent) => action(e)" :title="tooltip" :tabindex="0"></button>
|
<button :class="{ exposed }" :style="{ '--data-type-color': `var(--color-data-${dataType})` }" @click="(e: MouseEvent) => action(e)" :title="tooltip" :tabindex="0"></button>
|
||||||
@@ -39,21 +57,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
exposed: { type: Boolean as PropType<boolean>, required: true },
|
|
||||||
dataType: { type: String as PropType<string>, required: true },
|
|
||||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
|
||||||
|
|
||||||
// Callbacks
|
|
||||||
action: { type: Function as PropType<(e?: MouseEvent) => void>, required: true },
|
|
||||||
},
|
|
||||||
components: { LayoutRow },
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,41 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
import { type IconName } from "@/utility-functions/icons";
|
||||||
|
|
||||||
|
import FloatingMenu from "@/components/layout/FloatingMenu.vue";
|
||||||
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
|
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
icon: { type: String as PropType<IconName>, default: "DropdownArrow" },
|
||||||
|
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||||
|
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
action: { type: Function as PropType<() => void>, required: false },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
open: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onClick() {
|
||||||
|
this.open = true;
|
||||||
|
|
||||||
|
this.action?.();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
FloatingMenu,
|
||||||
|
IconButton,
|
||||||
|
LayoutRow,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutRow class="popover-button">
|
<LayoutRow class="popover-button">
|
||||||
<IconButton :class="{ open }" :disabled="disabled" :action="() => onClick()" :icon="icon" :size="16" data-floating-menu-spawner :tooltip="tooltip" />
|
<IconButton :class="{ open }" :disabled="disabled" :action="() => onClick()" :icon="icon" :size="16" data-floating-menu-spawner :tooltip="tooltip" />
|
||||||
@@ -50,41 +88,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
import { type IconName } from "@/utility-functions/icons";
|
|
||||||
|
|
||||||
import FloatingMenu from "@/components/layout/FloatingMenu.vue";
|
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|
||||||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
icon: { type: String as PropType<IconName>, default: "DropdownArrow" },
|
|
||||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
|
||||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
|
|
||||||
// Callbacks
|
|
||||||
action: { type: Function as PropType<() => void>, required: false },
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
open: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onClick() {
|
|
||||||
this.open = true;
|
|
||||||
|
|
||||||
this.action?.();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
FloatingMenu,
|
|
||||||
IconButton,
|
|
||||||
LayoutRow,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
import { type IconName } from "@/utility-functions/icons";
|
||||||
|
|
||||||
|
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||||
|
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
label: { type: String as PropType<string>, required: true },
|
||||||
|
icon: { type: String as PropType<IconName | undefined>, required: false },
|
||||||
|
emphasized: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
minWidth: { type: Number as PropType<number>, default: 0 },
|
||||||
|
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||||
|
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
action: { type: Function as PropType<(e: MouseEvent) => void>, required: true },
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
IconLabel,
|
||||||
|
TextLabel,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
class="text-button"
|
class="text-button"
|
||||||
@@ -69,31 +97,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
import { type IconName } from "@/utility-functions/icons";
|
|
||||||
|
|
||||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
|
||||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
label: { type: String as PropType<string>, required: true },
|
|
||||||
icon: { type: String as PropType<IconName | undefined>, required: false },
|
|
||||||
emphasized: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
minWidth: { type: Number as PropType<number>, default: 0 },
|
|
||||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
|
||||||
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
|
|
||||||
// Callbacks
|
|
||||||
action: { type: Function as PropType<(e: MouseEvent) => void>, required: true },
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
IconLabel,
|
|
||||||
TextLabel,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,46 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
import { isWidgetRow, isWidgetSection, type LayoutGroup, type WidgetSection as WidgetSectionFromJsMessages } from "@/wasm-communication/messages";
|
||||||
|
|
||||||
|
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||||
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
|
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||||
|
import WidgetRow from "@/components/widgets/WidgetRow.vue";
|
||||||
|
|
||||||
|
const WidgetSection = defineComponent({
|
||||||
|
name: "WidgetSection",
|
||||||
|
inject: ["editor"],
|
||||||
|
props: {
|
||||||
|
widgetData: { type: Object as PropType<WidgetSectionFromJsMessages>, required: true },
|
||||||
|
layoutTarget: { required: true },
|
||||||
|
},
|
||||||
|
data: () => ({
|
||||||
|
isWidgetRow,
|
||||||
|
isWidgetSection,
|
||||||
|
expanded: true,
|
||||||
|
}),
|
||||||
|
methods: {
|
||||||
|
updateLayout(widgetId: bigint, value: unknown) {
|
||||||
|
this.editor.instance.updateLayout(this.layoutTarget, widgetId, value);
|
||||||
|
},
|
||||||
|
layoutGroupType(layoutGroup: LayoutGroup): unknown {
|
||||||
|
if (isWidgetRow(layoutGroup)) return WidgetRow;
|
||||||
|
if (isWidgetSection(layoutGroup)) return WidgetSection;
|
||||||
|
|
||||||
|
throw new Error("Layout row type does not exist");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
LayoutCol,
|
||||||
|
LayoutRow,
|
||||||
|
TextLabel,
|
||||||
|
WidgetRow,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export default WidgetSection;
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- TODO: Implement collapsable sections with properties system -->
|
<!-- TODO: Implement collapsable sections with properties system -->
|
||||||
<template>
|
<template>
|
||||||
<LayoutCol class="widget-section">
|
<LayoutCol class="widget-section">
|
||||||
@@ -120,46 +163,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
import { isWidgetRow, isWidgetSection, type LayoutGroup, type WidgetSection as WidgetSectionFromJsMessages } from "@/wasm-communication/messages";
|
|
||||||
|
|
||||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|
||||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
|
||||||
import WidgetRow from "@/components/widgets/WidgetRow.vue";
|
|
||||||
|
|
||||||
const WidgetSection = defineComponent({
|
|
||||||
name: "WidgetSection",
|
|
||||||
inject: ["editor"],
|
|
||||||
props: {
|
|
||||||
widgetData: { type: Object as PropType<WidgetSectionFromJsMessages>, required: true },
|
|
||||||
layoutTarget: { required: true },
|
|
||||||
},
|
|
||||||
data: () => ({
|
|
||||||
isWidgetRow,
|
|
||||||
isWidgetSection,
|
|
||||||
expanded: true,
|
|
||||||
}),
|
|
||||||
methods: {
|
|
||||||
updateLayout(widgetId: bigint, value: unknown) {
|
|
||||||
this.editor.instance.updateLayout(this.layoutTarget, widgetId, value);
|
|
||||||
},
|
|
||||||
layoutGroupType(layoutGroup: LayoutGroup): unknown {
|
|
||||||
if (isWidgetRow(layoutGroup)) return WidgetRow;
|
|
||||||
if (isWidgetSection(layoutGroup)) return WidgetSection;
|
|
||||||
|
|
||||||
throw new Error("Layout row type does not exist");
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
LayoutCol,
|
|
||||||
LayoutRow,
|
|
||||||
TextLabel,
|
|
||||||
WidgetRow,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
export default WidgetSection;
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,48 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
import { type IconName } from "@/utility-functions/icons";
|
||||||
|
|
||||||
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
|
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
emits: ["update:checked"],
|
||||||
|
props: {
|
||||||
|
checked: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
icon: { type: String as PropType<IconName>, default: "Checkmark" },
|
||||||
|
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
id: `${Math.random()}`.substring(2),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
displayIcon(): IconName {
|
||||||
|
if (!this.checked && this.icon === "Checkmark") return "Empty12px";
|
||||||
|
|
||||||
|
return this.icon;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isChecked() {
|
||||||
|
return this.checked;
|
||||||
|
},
|
||||||
|
toggleCheckboxFromLabel(e: KeyboardEvent) {
|
||||||
|
const target = (e.target || undefined) as HTMLLabelElement | undefined;
|
||||||
|
const previousSibling = (target?.previousSibling || undefined) as HTMLInputElement | undefined;
|
||||||
|
previousSibling?.click();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
IconLabel,
|
||||||
|
LayoutRow,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutRow class="checkbox-input">
|
<LayoutRow class="checkbox-input">
|
||||||
<input
|
<input
|
||||||
@@ -80,48 +125,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
import { type IconName } from "@/utility-functions/icons";
|
|
||||||
|
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|
||||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
emits: ["update:checked"],
|
|
||||||
props: {
|
|
||||||
checked: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
icon: { type: String as PropType<IconName>, default: "Checkmark" },
|
|
||||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
id: `${Math.random()}`.substring(2),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
displayIcon(): IconName {
|
|
||||||
if (!this.checked && this.icon === "Checkmark") return "Empty12px";
|
|
||||||
|
|
||||||
return this.icon;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
isChecked() {
|
|
||||||
return this.checked;
|
|
||||||
},
|
|
||||||
toggleCheckboxFromLabel(e: KeyboardEvent) {
|
|
||||||
const target = (e.target || undefined) as HTMLLabelElement | undefined;
|
|
||||||
const previousSibling = (target?.previousSibling || undefined) as HTMLInputElement | undefined;
|
|
||||||
previousSibling?.click();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
IconLabel,
|
|
||||||
LayoutRow,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,57 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
import { Color } from "@/wasm-communication/messages";
|
||||||
|
|
||||||
|
import ColorPicker from "@/components/floating-menus/ColorPicker.vue";
|
||||||
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
|
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
emits: ["update:value", "update:open"],
|
||||||
|
props: {
|
||||||
|
value: { type: Color as PropType<Color>, required: true },
|
||||||
|
noTransparency: { type: Boolean as PropType<boolean>, default: false }, // TODO: Rename to allowTransparency, also implement allowNone
|
||||||
|
disabled: { type: Boolean as PropType<boolean>, default: false }, // TODO: Design and implement
|
||||||
|
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||||
|
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
|
||||||
|
// Bound through `v-model`
|
||||||
|
// TODO: See if this should be made to follow the pattern of DropdownInput.vue so this could be removed
|
||||||
|
open: { type: Boolean as PropType<boolean>, required: true },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isOpen: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// Called only when `open` is changed from outside this component (with v-model)
|
||||||
|
open(newOpen: boolean) {
|
||||||
|
this.isOpen = newOpen;
|
||||||
|
},
|
||||||
|
isOpen(newIsOpen: boolean) {
|
||||||
|
this.$emit("update:open", newIsOpen);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
colorPickerUpdated(color: Color) {
|
||||||
|
this.$emit("update:value", color);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
chip() {
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
ColorPicker,
|
||||||
|
LayoutRow,
|
||||||
|
TextLabel,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutRow class="color-input" :class="{ 'sharp-right-corners': sharpRightCorners }" :title="tooltip">
|
<LayoutRow class="color-input" :class="{ 'sharp-right-corners': sharpRightCorners }" :title="tooltip">
|
||||||
<button
|
<button
|
||||||
@@ -77,57 +131,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
import { Color } from "@/wasm-communication/messages";
|
|
||||||
|
|
||||||
import ColorPicker from "@/components/floating-menus/ColorPicker.vue";
|
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|
||||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
emits: ["update:value", "update:open"],
|
|
||||||
props: {
|
|
||||||
value: { type: Color as PropType<Color>, required: true },
|
|
||||||
noTransparency: { type: Boolean as PropType<boolean>, default: false }, // TODO: Rename to allowTransparency, also implement allowNone
|
|
||||||
disabled: { type: Boolean as PropType<boolean>, default: false }, // TODO: Design and implement
|
|
||||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
|
||||||
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
|
|
||||||
// Bound through `v-model`
|
|
||||||
// TODO: See if this should be made to follow the pattern of DropdownInput.vue so this could be removed
|
|
||||||
open: { type: Boolean as PropType<boolean>, required: true },
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isOpen: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
// Called only when `open` is changed from outside this component (with v-model)
|
|
||||||
open(newOpen: boolean) {
|
|
||||||
this.isOpen = newOpen;
|
|
||||||
},
|
|
||||||
isOpen(newIsOpen: boolean) {
|
|
||||||
this.$emit("update:open", newIsOpen);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
colorPickerUpdated(color: Color) {
|
|
||||||
this.$emit("update:value", color);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
chip() {
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
ColorPicker,
|
|
||||||
LayoutRow,
|
|
||||||
TextLabel,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,80 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType, toRaw } from "vue";
|
||||||
|
|
||||||
|
import { type MenuListEntry } from "@/wasm-communication/messages";
|
||||||
|
|
||||||
|
import MenuList from "@/components/floating-menus/MenuList.vue";
|
||||||
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
|
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||||
|
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||||
|
|
||||||
|
const DASH_ENTRY = { label: "-" };
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
emits: ["update:selectedIndex"],
|
||||||
|
props: {
|
||||||
|
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 },
|
||||||
|
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||||
|
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
activeEntry: this.makeActiveEntry(this.selectedIndex),
|
||||||
|
activeEntrySkipWatcher: false,
|
||||||
|
open: false,
|
||||||
|
minWidth: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// Called only when `selectedIndex` is changed from outside this component (with v-model)
|
||||||
|
selectedIndex() {
|
||||||
|
this.activeEntrySkipWatcher = true;
|
||||||
|
this.activeEntry = this.makeActiveEntry();
|
||||||
|
},
|
||||||
|
// Called when `activeEntry` is changed by the `v-model` on this component's MenuList component, or by the `selectedIndex()` watcher above (but we want to skip that case)
|
||||||
|
activeEntry(newActiveEntry: MenuListEntry) {
|
||||||
|
if (this.activeEntrySkipWatcher) {
|
||||||
|
this.activeEntrySkipWatcher = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `toRaw()` pulls it out of the Vue proxy
|
||||||
|
if (toRaw(newActiveEntry) === DASH_ENTRY) return;
|
||||||
|
|
||||||
|
this.$emit("update:selectedIndex", this.entries.flat().indexOf(newActiveEntry));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
makeActiveEntry(): MenuListEntry {
|
||||||
|
const entries = this.entries.flat();
|
||||||
|
|
||||||
|
if (this.selectedIndex !== undefined && this.selectedIndex >= 0 && this.selectedIndex < entries.length) {
|
||||||
|
return entries[this.selectedIndex];
|
||||||
|
}
|
||||||
|
return DASH_ENTRY;
|
||||||
|
},
|
||||||
|
keydown(e: KeyboardEvent) {
|
||||||
|
(this.$refs.menuList as typeof MenuList | undefined)?.keydown(e, false);
|
||||||
|
},
|
||||||
|
unFocusDropdownBox(e: FocusEvent) {
|
||||||
|
const blurTarget = (e.target as HTMLDivElement | undefined)?.closest("[data-dropdown-input]");
|
||||||
|
const self: HTMLDivElement | undefined = this.$el;
|
||||||
|
if (blurTarget !== self) this.open = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
IconLabel,
|
||||||
|
LayoutRow,
|
||||||
|
MenuList,
|
||||||
|
TextLabel,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutRow class="dropdown-input" data-dropdown-input>
|
<LayoutRow class="dropdown-input" data-dropdown-input>
|
||||||
<LayoutRow
|
<LayoutRow
|
||||||
@@ -95,80 +172,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType, toRaw } from "vue";
|
|
||||||
|
|
||||||
import { type MenuListEntry } from "@/wasm-communication/messages";
|
|
||||||
|
|
||||||
import MenuList from "@/components/floating-menus/MenuList.vue";
|
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|
||||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
|
||||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
|
||||||
|
|
||||||
const DASH_ENTRY = { label: "-" };
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
emits: ["update:selectedIndex"],
|
|
||||||
props: {
|
|
||||||
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 },
|
|
||||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
|
||||||
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
activeEntry: this.makeActiveEntry(this.selectedIndex),
|
|
||||||
activeEntrySkipWatcher: false,
|
|
||||||
open: false,
|
|
||||||
minWidth: 0,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
// Called only when `selectedIndex` is changed from outside this component (with v-model)
|
|
||||||
selectedIndex() {
|
|
||||||
this.activeEntrySkipWatcher = true;
|
|
||||||
this.activeEntry = this.makeActiveEntry();
|
|
||||||
},
|
|
||||||
// Called when `activeEntry` is changed by the `v-model` on this component's MenuList component, or by the `selectedIndex()` watcher above (but we want to skip that case)
|
|
||||||
activeEntry(newActiveEntry: MenuListEntry) {
|
|
||||||
if (this.activeEntrySkipWatcher) {
|
|
||||||
this.activeEntrySkipWatcher = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// `toRaw()` pulls it out of the Vue proxy
|
|
||||||
if (toRaw(newActiveEntry) === DASH_ENTRY) return;
|
|
||||||
|
|
||||||
this.$emit("update:selectedIndex", this.entries.flat().indexOf(newActiveEntry));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
makeActiveEntry(): MenuListEntry {
|
|
||||||
const entries = this.entries.flat();
|
|
||||||
|
|
||||||
if (this.selectedIndex !== undefined && this.selectedIndex >= 0 && this.selectedIndex < entries.length) {
|
|
||||||
return entries[this.selectedIndex];
|
|
||||||
}
|
|
||||||
return DASH_ENTRY;
|
|
||||||
},
|
|
||||||
keydown(e: KeyboardEvent) {
|
|
||||||
(this.$refs.menuList as typeof MenuList | undefined)?.keydown(e, false);
|
|
||||||
},
|
|
||||||
unFocusDropdownBox(e: FocusEvent) {
|
|
||||||
const blurTarget = (e.target as HTMLDivElement | undefined)?.closest("[data-dropdown-input]");
|
|
||||||
const self: HTMLDivElement | undefined = this.$el;
|
|
||||||
if (blurTarget !== self) this.open = false;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
IconLabel,
|
|
||||||
LayoutRow,
|
|
||||||
MenuList,
|
|
||||||
TextLabel,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,64 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
import { platformIsMac } from "@/utility-functions/platform";
|
||||||
|
|
||||||
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
emits: ["update:value", "textFocused", "textChanged", "cancelTextChange"],
|
||||||
|
props: {
|
||||||
|
value: { type: String as PropType<string>, required: true },
|
||||||
|
label: { type: String as PropType<string>, required: false },
|
||||||
|
spellcheck: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
textarea: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||||
|
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
placeholder: { type: String as PropType<string>, required: false },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
id: `${Math.random()}`.substring(2),
|
||||||
|
macKeyboardLayout: platformIsMac(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// Select (highlight) all the text. For technical reasons, it is necessary to pass the current text.
|
||||||
|
selectAllText(currentText: string) {
|
||||||
|
const inputElement = this.$refs.input as HTMLInputElement | HTMLTextAreaElement | undefined;
|
||||||
|
if (!inputElement) return;
|
||||||
|
|
||||||
|
// Setting the value directly is required to make `inputElement.select()` work
|
||||||
|
inputElement.value = currentText;
|
||||||
|
|
||||||
|
inputElement.select();
|
||||||
|
},
|
||||||
|
unFocus() {
|
||||||
|
(this.$refs.input as HTMLInputElement | HTMLTextAreaElement | undefined)?.blur();
|
||||||
|
},
|
||||||
|
getInputElementValue(): string | undefined {
|
||||||
|
return (this.$refs.input as HTMLInputElement | HTMLTextAreaElement | undefined)?.value;
|
||||||
|
},
|
||||||
|
setInputElementValue(value: string) {
|
||||||
|
const inputElement = this.$refs.input as HTMLInputElement | HTMLTextAreaElement | undefined;
|
||||||
|
if (inputElement) inputElement.value = value;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
inputValue: {
|
||||||
|
get() {
|
||||||
|
return this.value;
|
||||||
|
},
|
||||||
|
set(value: string) {
|
||||||
|
this.$emit("update:value", value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: { LayoutRow },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- This is a base component, extended by others like NumberInput and TextInput. It should not be used directly. -->
|
<!-- This is a base component, extended by others like NumberInput and TextInput. It should not be used directly. -->
|
||||||
<template>
|
<template>
|
||||||
<LayoutRow class="field-input" :class="{ disabled, 'sharp-right-corners': sharpRightCorners }" :title="tooltip">
|
<LayoutRow class="field-input" :class="{ disabled, 'sharp-right-corners': sharpRightCorners }" :title="tooltip">
|
||||||
@@ -131,64 +192,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
import { platformIsMac } from "@/utility-functions/platform";
|
|
||||||
|
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
emits: ["update:value", "textFocused", "textChanged", "cancelTextChange"],
|
|
||||||
props: {
|
|
||||||
value: { type: String as PropType<string>, required: true },
|
|
||||||
label: { type: String as PropType<string>, required: false },
|
|
||||||
spellcheck: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
textarea: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
|
||||||
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
placeholder: { type: String as PropType<string>, required: false },
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
id: `${Math.random()}`.substring(2),
|
|
||||||
macKeyboardLayout: platformIsMac(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
// Select (highlight) all the text. For technical reasons, it is necessary to pass the current text.
|
|
||||||
selectAllText(currentText: string) {
|
|
||||||
const inputElement = this.$refs.input as HTMLInputElement | HTMLTextAreaElement | undefined;
|
|
||||||
if (!inputElement) return;
|
|
||||||
|
|
||||||
// Setting the value directly is required to make `inputElement.select()` work
|
|
||||||
inputElement.value = currentText;
|
|
||||||
|
|
||||||
inputElement.select();
|
|
||||||
},
|
|
||||||
unFocus() {
|
|
||||||
(this.$refs.input as HTMLInputElement | HTMLTextAreaElement | undefined)?.blur();
|
|
||||||
},
|
|
||||||
getInputElementValue(): string | undefined {
|
|
||||||
return (this.$refs.input as HTMLInputElement | HTMLTextAreaElement | undefined)?.value;
|
|
||||||
},
|
|
||||||
setInputElementValue(value: string) {
|
|
||||||
const inputElement = this.$refs.input as HTMLInputElement | HTMLTextAreaElement | undefined;
|
|
||||||
if (inputElement) inputElement.value = value;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
inputValue: {
|
|
||||||
get() {
|
|
||||||
return this.value;
|
|
||||||
},
|
|
||||||
set(value: string) {
|
|
||||||
this.$emit("update:value", value);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: { LayoutRow },
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,84 +1,3 @@
|
|||||||
<!-- TODO: Combine this widget into the DropdownInput widget -->
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<LayoutRow class="font-input">
|
|
||||||
<LayoutRow
|
|
||||||
class="dropdown-box"
|
|
||||||
:class="{ disabled, 'sharp-right-corners': sharpRightCorners }"
|
|
||||||
:style="{ minWidth: `${minWidth}px` }"
|
|
||||||
:title="tooltip"
|
|
||||||
:tabindex="disabled ? -1 : 0"
|
|
||||||
@click="toggleOpen"
|
|
||||||
@keydown="keydown"
|
|
||||||
data-floating-menu-spawner
|
|
||||||
>
|
|
||||||
<TextLabel class="dropdown-label">{{ activeEntry?.value || "" }}</TextLabel>
|
|
||||||
<IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" />
|
|
||||||
</LayoutRow>
|
|
||||||
<MenuList
|
|
||||||
v-model:activeEntry="activeEntry"
|
|
||||||
v-model:open="open"
|
|
||||||
:entries="[entries]"
|
|
||||||
:minWidth="isStyle ? 0 : minWidth"
|
|
||||||
:virtualScrollingEntryHeight="isStyle ? 0 : 20"
|
|
||||||
:scrollableY="true"
|
|
||||||
@naturalWidth="(newNaturalWidth: number) => (isStyle && (minWidth = newNaturalWidth))"
|
|
||||||
ref="menuList"
|
|
||||||
></MenuList>
|
|
||||||
</LayoutRow>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.font-input {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.dropdown-box {
|
|
||||||
align-items: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
background: var(--color-1-nearblack);
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 2px;
|
|
||||||
|
|
||||||
.dropdown-label {
|
|
||||||
margin: 0;
|
|
||||||
margin-left: 8px;
|
|
||||||
flex: 1 1 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-arrow {
|
|
||||||
margin: 6px 2px;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&.open {
|
|
||||||
background: var(--color-6-lowergray);
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: var(--color-f-white);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.open {
|
|
||||||
border-radius: 2px 2px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
background: var(--color-2-mildblack);
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: var(--color-8-uppergray);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu-list .floating-menu-container .floating-menu-content {
|
|
||||||
max-height: 400px;
|
|
||||||
padding: 4px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, nextTick, type PropType } from "vue";
|
import { defineComponent, nextTick, type PropType } from "vue";
|
||||||
|
|
||||||
@@ -187,3 +106,84 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- TODO: Combine this widget into the DropdownInput widget -->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<LayoutRow class="font-input">
|
||||||
|
<LayoutRow
|
||||||
|
class="dropdown-box"
|
||||||
|
:class="{ disabled, 'sharp-right-corners': sharpRightCorners }"
|
||||||
|
:style="{ minWidth: `${minWidth}px` }"
|
||||||
|
:title="tooltip"
|
||||||
|
:tabindex="disabled ? -1 : 0"
|
||||||
|
@click="toggleOpen"
|
||||||
|
@keydown="keydown"
|
||||||
|
data-floating-menu-spawner
|
||||||
|
>
|
||||||
|
<TextLabel class="dropdown-label">{{ activeEntry?.value || "" }}</TextLabel>
|
||||||
|
<IconLabel class="dropdown-arrow" :icon="'DropdownArrow'" />
|
||||||
|
</LayoutRow>
|
||||||
|
<MenuList
|
||||||
|
v-model:activeEntry="activeEntry"
|
||||||
|
v-model:open="open"
|
||||||
|
:entries="[entries]"
|
||||||
|
:minWidth="isStyle ? 0 : minWidth"
|
||||||
|
:virtualScrollingEntryHeight="isStyle ? 0 : 20"
|
||||||
|
:scrollableY="true"
|
||||||
|
@naturalWidth="(newNaturalWidth: number) => (isStyle && (minWidth = newNaturalWidth))"
|
||||||
|
ref="menuList"
|
||||||
|
></MenuList>
|
||||||
|
</LayoutRow>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.font-input {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.dropdown-box {
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: var(--color-1-nearblack);
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 2px;
|
||||||
|
|
||||||
|
.dropdown-label {
|
||||||
|
margin: 0;
|
||||||
|
margin-left: 8px;
|
||||||
|
flex: 1 1 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-arrow {
|
||||||
|
margin: 6px 2px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.open {
|
||||||
|
background: var(--color-6-lowergray);
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--color-f-white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
border-radius: 2px 2px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
background: var(--color-2-mildblack);
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--color-8-uppergray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-list .floating-menu-container .floating-menu-content {
|
||||||
|
max-height: 400px;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,3 +1,73 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
import { currentDraggingElement } from "@/io-managers/drag";
|
||||||
|
|
||||||
|
import type { LayerType, LayerTypeData } from "@/wasm-communication/messages";
|
||||||
|
import { layerTypeData } from "@/wasm-communication/messages";
|
||||||
|
|
||||||
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
|
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
||||||
|
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||||
|
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
emits: ["update:value"],
|
||||||
|
props: {
|
||||||
|
value: { type: String as PropType<string | undefined>, required: false },
|
||||||
|
layerName: { type: String as PropType<string | undefined>, required: false },
|
||||||
|
layerType: { type: String as PropType<LayerType | undefined>, required: false },
|
||||||
|
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||||
|
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hoveringDrop: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
droppable() {
|
||||||
|
return this.hoveringDrop && currentDraggingElement();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
dragOver(e: DragEvent): void {
|
||||||
|
this.hoveringDrop = true;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
|
dragLeave(): void {
|
||||||
|
this.hoveringDrop = false;
|
||||||
|
},
|
||||||
|
drop(e: DragEvent): void {
|
||||||
|
this.hoveringDrop = false;
|
||||||
|
|
||||||
|
const element = currentDraggingElement();
|
||||||
|
const layerPath = element?.getAttribute("data-layer") || undefined;
|
||||||
|
|
||||||
|
if (layerPath) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
this.$emit("update:value", layerPath);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clearLayer(): void {
|
||||||
|
this.$emit("update:value", undefined);
|
||||||
|
},
|
||||||
|
layerTypeData(layerType: LayerType): LayerTypeData {
|
||||||
|
return layerTypeData(layerType) || { name: "Error", icon: "Info" };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
IconButton,
|
||||||
|
IconLabel,
|
||||||
|
LayoutRow,
|
||||||
|
TextLabel,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutRow
|
<LayoutRow
|
||||||
class="layer-reference-input"
|
class="layer-reference-input"
|
||||||
@@ -89,73 +159,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
import { currentDraggingElement } from "@/io-managers/drag";
|
|
||||||
|
|
||||||
import type { LayerType, LayerTypeData } from "@/wasm-communication/messages";
|
|
||||||
import { layerTypeData } from "@/wasm-communication/messages";
|
|
||||||
|
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|
||||||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
|
||||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
|
||||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
emits: ["update:value"],
|
|
||||||
props: {
|
|
||||||
value: { type: String as PropType<string | undefined>, required: false },
|
|
||||||
layerName: { type: String as PropType<string | undefined>, required: false },
|
|
||||||
layerType: { type: String as PropType<LayerType | undefined>, required: false },
|
|
||||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
|
||||||
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
hoveringDrop: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
droppable() {
|
|
||||||
return this.hoveringDrop && currentDraggingElement();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
dragOver(e: DragEvent): void {
|
|
||||||
this.hoveringDrop = true;
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
},
|
|
||||||
dragLeave(): void {
|
|
||||||
this.hoveringDrop = false;
|
|
||||||
},
|
|
||||||
drop(e: DragEvent): void {
|
|
||||||
this.hoveringDrop = false;
|
|
||||||
|
|
||||||
const element = currentDraggingElement();
|
|
||||||
const layerPath = element?.getAttribute("data-layer") || undefined;
|
|
||||||
|
|
||||||
if (layerPath) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
this.$emit("update:value", layerPath);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
clearLayer(): void {
|
|
||||||
this.$emit("update:value", undefined);
|
|
||||||
},
|
|
||||||
layerTypeData(layerType: LayerType): LayerTypeData {
|
|
||||||
return layerTypeData(layerType) || { name: "Error", icon: "Info" };
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
IconButton,
|
|
||||||
IconLabel,
|
|
||||||
LayoutRow,
|
|
||||||
TextLabel,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,69 +1,3 @@
|
|||||||
<template>
|
|
||||||
<div class="menu-bar-input" data-menu-bar-input>
|
|
||||||
<div class="entry-container" v-for="(entry, index) in entries" :key="index">
|
|
||||||
<div
|
|
||||||
@click="(e: MouseEvent) => clickEntry(entry, e)"
|
|
||||||
@blur="(e: FocusEvent) => unFocusEntry(entry, e)"
|
|
||||||
@keydown="(e: KeyboardEvent) => entry.ref?.keydown(e, false)"
|
|
||||||
class="entry"
|
|
||||||
:class="{ open: entry.ref?.isOpen }"
|
|
||||||
tabindex="0"
|
|
||||||
:data-floating-menu-spawner="entry.children && entry.children.length > 0 ? '' : 'no-hover-transfer'"
|
|
||||||
>
|
|
||||||
<IconLabel v-if="entry.icon" :icon="entry.icon" />
|
|
||||||
<TextLabel v-if="entry.label">{{ entry.label }}</TextLabel>
|
|
||||||
</div>
|
|
||||||
<MenuList
|
|
||||||
v-if="entry.children && entry.children.length > 0"
|
|
||||||
:open="entry.ref?.open || false"
|
|
||||||
:entries="entry.children || []"
|
|
||||||
:direction="'Bottom'"
|
|
||||||
:minWidth="240"
|
|
||||||
:drawIcon="true"
|
|
||||||
:ref="(ref: MenuListInstance): void => (ref && (entry.ref = ref), undefined)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.menu-bar-input {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.entry-container {
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.entry {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
padding: 0 8px;
|
|
||||||
background: none;
|
|
||||||
border: 0;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
fill: var(--color-e-nearwhite);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&.open {
|
|
||||||
background: var(--color-6-lowergray);
|
|
||||||
|
|
||||||
svg {
|
|
||||||
fill: var(--color-f-white);
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: var(--color-f-white);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
@@ -152,3 +86,69 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="menu-bar-input" data-menu-bar-input>
|
||||||
|
<div class="entry-container" v-for="(entry, index) in entries" :key="index">
|
||||||
|
<div
|
||||||
|
@click="(e: MouseEvent) => clickEntry(entry, e)"
|
||||||
|
@blur="(e: FocusEvent) => unFocusEntry(entry, e)"
|
||||||
|
@keydown="(e: KeyboardEvent) => entry.ref?.keydown(e, false)"
|
||||||
|
class="entry"
|
||||||
|
:class="{ open: entry.ref?.isOpen }"
|
||||||
|
tabindex="0"
|
||||||
|
:data-floating-menu-spawner="entry.children && entry.children.length > 0 ? '' : 'no-hover-transfer'"
|
||||||
|
>
|
||||||
|
<IconLabel v-if="entry.icon" :icon="entry.icon" />
|
||||||
|
<TextLabel v-if="entry.label">{{ entry.label }}</TextLabel>
|
||||||
|
</div>
|
||||||
|
<MenuList
|
||||||
|
v-if="entry.children && entry.children.length > 0"
|
||||||
|
:open="entry.ref?.open || false"
|
||||||
|
:entries="entry.children || []"
|
||||||
|
:direction="'Bottom'"
|
||||||
|
:minWidth="240"
|
||||||
|
:drawIcon="true"
|
||||||
|
:ref="(ref: MenuListInstance): void => (ref && (entry.ref = ref), undefined)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.menu-bar-input {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.entry-container {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.entry {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0 8px;
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: var(--color-e-nearwhite);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.open {
|
||||||
|
background: var(--color-6-lowergray);
|
||||||
|
|
||||||
|
svg {
|
||||||
|
fill: var(--color-f-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--color-f-white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,3 +1,240 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
import { type NumberInputMode, type NumberInputIncrementBehavior } from "@/wasm-communication/messages";
|
||||||
|
|
||||||
|
import FieldInput from "@/components/widgets/inputs/FieldInput.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
emits: ["update:value"],
|
||||||
|
props: {
|
||||||
|
// Label
|
||||||
|
label: { type: String as PropType<string>, required: false },
|
||||||
|
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||||
|
|
||||||
|
// Disabled
|
||||||
|
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
|
||||||
|
// Value
|
||||||
|
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 },
|
||||||
|
isInteger: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
|
||||||
|
// Number presentation
|
||||||
|
displayDecimalPlaces: { type: Number as PropType<number>, default: 3 },
|
||||||
|
unit: { type: String as PropType<string>, default: "" },
|
||||||
|
unitIsHiddenWhenEditing: { type: Boolean as PropType<boolean>, default: true },
|
||||||
|
|
||||||
|
// Mode behavior
|
||||||
|
// "Increment" shows arrows and allows dragging left/right to change the value.
|
||||||
|
// "Range" shows a range slider between some minimum and maximum value.
|
||||||
|
mode: { type: String as PropType<NumberInputMode>, default: "Increment" },
|
||||||
|
// When `mode` is "Increment", `step` is the multiplier or addend used with `incrementBehavior`.
|
||||||
|
// When `mode` is "Range", `step` is the range slider's snapping increment if `isInteger` is `true`.
|
||||||
|
step: { type: Number as PropType<number>, default: 1 },
|
||||||
|
// `incrementBehavior` is only applicable with a `mode` of "Increment".
|
||||||
|
// "Add"/"Multiply": The value is added or multiplied by `step`.
|
||||||
|
// "None": the increment arrows are not shown.
|
||||||
|
// "Callback": the functions `incrementCallbackIncrease` and `incrementCallbackDecrease` call custom behavior.
|
||||||
|
incrementBehavior: { type: String as PropType<NumberInputIncrementBehavior>, default: "Add" },
|
||||||
|
// `rangeMin` and `rangeMax` are only applicable with a `mode` of "Range".
|
||||||
|
// They set the lower and upper values of the slider to drag between.
|
||||||
|
rangeMin: { type: Number as PropType<number>, default: 0 },
|
||||||
|
rangeMax: { type: Number as PropType<number>, default: 1 },
|
||||||
|
|
||||||
|
// Styling
|
||||||
|
minWidth: { type: Number as PropType<number>, default: 0 },
|
||||||
|
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
incrementCallbackIncrease: { type: Function as PropType<() => void>, required: false },
|
||||||
|
incrementCallbackDecrease: { type: Function as PropType<() => void>, required: false },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
text: this.displayText(this.value),
|
||||||
|
editing: false,
|
||||||
|
// Stays in sync with a binding to the actual input range slider element.
|
||||||
|
rangeSliderValue: this.value !== undefined ? this.value : 0,
|
||||||
|
// Value used to render the position of the fake slider when applicable, and length of the progress colored region to the slider's left.
|
||||||
|
// This is the same as `rangeSliderValue` except in the "mousedown" state, when it has the previous location before the user's mousedown.
|
||||||
|
rangeSliderValueAsRendered: this.value !== undefined ? this.value : 0,
|
||||||
|
// "default": no interaction is happening.
|
||||||
|
// "mousedown": the user has pressed down the mouse and might next decide to either drag left/right or release without dragging.
|
||||||
|
// "dragging": the user is dragging the slider left/right.
|
||||||
|
rangeSliderClickDragState: "default" as "default" | "mousedown" | "dragging",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
sliderStepValue() {
|
||||||
|
const step = this.step === undefined ? 1 : this.step;
|
||||||
|
return this.isInteger ? step : "any";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
sliderInput() {
|
||||||
|
// Keep only 4 digits after the decimal point
|
||||||
|
const ROUNDING_EXPONENT = 4;
|
||||||
|
const ROUNDING_MAGNITUDE = 10 ** ROUNDING_EXPONENT;
|
||||||
|
const roundedValue = Math.round(this.rangeSliderValue * ROUNDING_MAGNITUDE) / ROUNDING_MAGNITUDE;
|
||||||
|
|
||||||
|
// Exit if this is an extraneous event invocation that occurred after mouseup, which happens in Firefox
|
||||||
|
if (this.value !== undefined && Math.abs(this.value - roundedValue) < 1 / ROUNDING_MAGNITUDE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first event upon mousedown means we transition to a "mousedown" state
|
||||||
|
if (this.rangeSliderClickDragState === "default") {
|
||||||
|
this.rangeSliderClickDragState = "mousedown";
|
||||||
|
|
||||||
|
// Exit early because we don't want to use the value set by where on the track the user pressed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The second event upon mousedown that occurs by moving left or right means the user has committed to dragging the slider
|
||||||
|
if (this.rangeSliderClickDragState === "mousedown") {
|
||||||
|
this.rangeSliderClickDragState = "dragging";
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're in a dragging state, we want to use the new slider value
|
||||||
|
this.rangeSliderValueAsRendered = roundedValue;
|
||||||
|
this.updateValue(roundedValue);
|
||||||
|
},
|
||||||
|
sliderPointerDown() {
|
||||||
|
// We want to render the fake slider thumb at the old position, which is still the number held by `value`
|
||||||
|
this.rangeSliderValueAsRendered = this.value || 0;
|
||||||
|
|
||||||
|
// Because an `input` event is fired right before or after this (depending on browser), that first
|
||||||
|
// invocation will transition the state machine to `mousedown`. That's why we don't do it here.
|
||||||
|
},
|
||||||
|
sliderPointerUp() {
|
||||||
|
// User clicked but didn't drag, so we focus the text input element
|
||||||
|
if (this.rangeSliderClickDragState === "mousedown") {
|
||||||
|
const fieldInput = this.$refs.fieldInput as typeof FieldInput | undefined;
|
||||||
|
const inputElement = fieldInput?.$el.querySelector("[data-input-element]") as HTMLInputElement | undefined;
|
||||||
|
if (!inputElement) return;
|
||||||
|
|
||||||
|
// Set the slider position back to the original position to undo the user moving it
|
||||||
|
this.rangeSliderValue = this.rangeSliderValueAsRendered;
|
||||||
|
|
||||||
|
// Begin editing the number text field
|
||||||
|
inputElement.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Releasing the mouse means we can reset the state machine
|
||||||
|
this.rangeSliderClickDragState = "default";
|
||||||
|
},
|
||||||
|
onTextFocused() {
|
||||||
|
if (this.value === undefined) this.text = "";
|
||||||
|
else if (this.unitIsHiddenWhenEditing) this.text = `${this.value}`;
|
||||||
|
else this.text = `${this.value}${unPluralize(this.unit, this.value)}`;
|
||||||
|
|
||||||
|
this.editing = true;
|
||||||
|
|
||||||
|
(this.$refs.fieldInput as typeof FieldInput | undefined)?.selectAllText(this.text);
|
||||||
|
},
|
||||||
|
// Called only when `value` is changed from the <input> element via user input and committed, either with the
|
||||||
|
// enter key (via the `change` event) or when the <input> element is unfocused (with the `blur` event binding)
|
||||||
|
onTextChanged() {
|
||||||
|
// The `unFocus()` call at the bottom of this function and in `onCancelTextChange()` causes this function to be run again, so this check skips a second run
|
||||||
|
if (!this.editing) return;
|
||||||
|
|
||||||
|
const parsed = parseFloat(this.text);
|
||||||
|
const newValue = Number.isNaN(parsed) ? undefined : parsed;
|
||||||
|
|
||||||
|
this.updateValue(newValue);
|
||||||
|
|
||||||
|
this.editing = false;
|
||||||
|
|
||||||
|
(this.$refs.fieldInput as typeof FieldInput | undefined)?.unFocus();
|
||||||
|
},
|
||||||
|
onCancelTextChange() {
|
||||||
|
this.updateValue(undefined);
|
||||||
|
|
||||||
|
this.editing = false;
|
||||||
|
|
||||||
|
(this.$refs.fieldInput as typeof FieldInput | undefined)?.unFocus();
|
||||||
|
},
|
||||||
|
onIncrement(direction: "Decrease" | "Increase") {
|
||||||
|
if (this.value === undefined) return;
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
Add: (): void => {
|
||||||
|
const directionAddend = direction === "Increase" ? this.step : -this.step;
|
||||||
|
this.updateValue(this.value !== undefined ? this.value + directionAddend : undefined);
|
||||||
|
},
|
||||||
|
Multiply: (): void => {
|
||||||
|
const directionMultiplier = direction === "Increase" ? this.step : 1 / this.step;
|
||||||
|
this.updateValue(this.value !== undefined ? this.value * directionMultiplier : undefined);
|
||||||
|
},
|
||||||
|
Callback: (): void => {
|
||||||
|
if (direction === "Increase") this.incrementCallbackIncrease?.();
|
||||||
|
if (direction === "Decrease") this.incrementCallbackDecrease?.();
|
||||||
|
},
|
||||||
|
None: (): void => undefined,
|
||||||
|
};
|
||||||
|
const action = actions[this.incrementBehavior];
|
||||||
|
action();
|
||||||
|
},
|
||||||
|
updateValue(newValue: number | undefined) {
|
||||||
|
const nowValid = this.value !== undefined && this.isInteger ? Math.round(this.value) : this.value;
|
||||||
|
let cleaned = newValue !== undefined ? newValue : nowValid;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Required as the call to update:value can, not change the value
|
||||||
|
this.text = this.displayText(this.value);
|
||||||
|
|
||||||
|
if (newValue !== undefined) this.$emit("update:value", cleaned);
|
||||||
|
},
|
||||||
|
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
|
||||||
|
// 0.23 == 0 (Reason for the slightly more complicated code)
|
||||||
|
const absValueInt = Math.floor(Math.abs(value));
|
||||||
|
const leftSideDigits = absValueInt === 0 ? 0 : absValueInt.toString().length;
|
||||||
|
const roundingPower = 10 ** Math.max(this.displayDecimalPlaces - leftSideDigits, 0);
|
||||||
|
|
||||||
|
const displayValue = Math.round(value * roundingPower) / roundingPower;
|
||||||
|
|
||||||
|
return `${displayValue}${unPluralize(this.unit, value)}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// Called only when `value` is changed from outside this component (with v-model)
|
||||||
|
value(newValue: number | undefined) {
|
||||||
|
// Draw a dash if the value is undefined
|
||||||
|
if (newValue === undefined) {
|
||||||
|
this.text = "-";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the range slider with the new value
|
||||||
|
this.rangeSliderValue = newValue;
|
||||||
|
this.rangeSliderValueAsRendered = newValue;
|
||||||
|
|
||||||
|
// The simple `clamp()` function can't be used here since `undefined` values need to be boundless
|
||||||
|
let sanitized = newValue;
|
||||||
|
if (typeof this.min === "number") sanitized = Math.max(sanitized, this.min);
|
||||||
|
if (typeof this.max === "number") sanitized = Math.min(sanitized, this.max);
|
||||||
|
|
||||||
|
this.text = this.displayText(sanitized);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: { FieldInput },
|
||||||
|
});
|
||||||
|
|
||||||
|
function unPluralize(unit: string, value: number): string {
|
||||||
|
if (value === 1 && unit.endsWith("s")) return unit.slice(0, -1);
|
||||||
|
return unit;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FieldInput
|
<FieldInput
|
||||||
class="number-input"
|
class="number-input"
|
||||||
@@ -240,240 +477,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
import { type NumberInputMode, type NumberInputIncrementBehavior } from "@/wasm-communication/messages";
|
|
||||||
|
|
||||||
import FieldInput from "@/components/widgets/inputs/FieldInput.vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
emits: ["update:value"],
|
|
||||||
props: {
|
|
||||||
// Label
|
|
||||||
label: { type: String as PropType<string>, required: false },
|
|
||||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
|
||||||
|
|
||||||
// Disabled
|
|
||||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
|
|
||||||
// Value
|
|
||||||
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 },
|
|
||||||
isInteger: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
|
|
||||||
// Number presentation
|
|
||||||
displayDecimalPlaces: { type: Number as PropType<number>, default: 3 },
|
|
||||||
unit: { type: String as PropType<string>, default: "" },
|
|
||||||
unitIsHiddenWhenEditing: { type: Boolean as PropType<boolean>, default: true },
|
|
||||||
|
|
||||||
// Mode behavior
|
|
||||||
// "Increment" shows arrows and allows dragging left/right to change the value.
|
|
||||||
// "Range" shows a range slider between some minimum and maximum value.
|
|
||||||
mode: { type: String as PropType<NumberInputMode>, default: "Increment" },
|
|
||||||
// When `mode` is "Increment", `step` is the multiplier or addend used with `incrementBehavior`.
|
|
||||||
// When `mode` is "Range", `step` is the range slider's snapping increment if `isInteger` is `true`.
|
|
||||||
step: { type: Number as PropType<number>, default: 1 },
|
|
||||||
// `incrementBehavior` is only applicable with a `mode` of "Increment".
|
|
||||||
// "Add"/"Multiply": The value is added or multiplied by `step`.
|
|
||||||
// "None": the increment arrows are not shown.
|
|
||||||
// "Callback": the functions `incrementCallbackIncrease` and `incrementCallbackDecrease` call custom behavior.
|
|
||||||
incrementBehavior: { type: String as PropType<NumberInputIncrementBehavior>, default: "Add" },
|
|
||||||
// `rangeMin` and `rangeMax` are only applicable with a `mode` of "Range".
|
|
||||||
// They set the lower and upper values of the slider to drag between.
|
|
||||||
rangeMin: { type: Number as PropType<number>, default: 0 },
|
|
||||||
rangeMax: { type: Number as PropType<number>, default: 1 },
|
|
||||||
|
|
||||||
// Styling
|
|
||||||
minWidth: { type: Number as PropType<number>, default: 0 },
|
|
||||||
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
|
|
||||||
// Callbacks
|
|
||||||
incrementCallbackIncrease: { type: Function as PropType<() => void>, required: false },
|
|
||||||
incrementCallbackDecrease: { type: Function as PropType<() => void>, required: false },
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
text: this.displayText(this.value),
|
|
||||||
editing: false,
|
|
||||||
// Stays in sync with a binding to the actual input range slider element.
|
|
||||||
rangeSliderValue: this.value !== undefined ? this.value : 0,
|
|
||||||
// Value used to render the position of the fake slider when applicable, and length of the progress colored region to the slider's left.
|
|
||||||
// This is the same as `rangeSliderValue` except in the "mousedown" state, when it has the previous location before the user's mousedown.
|
|
||||||
rangeSliderValueAsRendered: this.value !== undefined ? this.value : 0,
|
|
||||||
// "default": no interaction is happening.
|
|
||||||
// "mousedown": the user has pressed down the mouse and might next decide to either drag left/right or release without dragging.
|
|
||||||
// "dragging": the user is dragging the slider left/right.
|
|
||||||
rangeSliderClickDragState: "default" as "default" | "mousedown" | "dragging",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
sliderStepValue() {
|
|
||||||
const step = this.step === undefined ? 1 : this.step;
|
|
||||||
return this.isInteger ? step : "any";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
sliderInput() {
|
|
||||||
// Keep only 4 digits after the decimal point
|
|
||||||
const ROUNDING_EXPONENT = 4;
|
|
||||||
const ROUNDING_MAGNITUDE = 10 ** ROUNDING_EXPONENT;
|
|
||||||
const roundedValue = Math.round(this.rangeSliderValue * ROUNDING_MAGNITUDE) / ROUNDING_MAGNITUDE;
|
|
||||||
|
|
||||||
// Exit if this is an extraneous event invocation that occurred after mouseup, which happens in Firefox
|
|
||||||
if (this.value !== undefined && Math.abs(this.value - roundedValue) < 1 / ROUNDING_MAGNITUDE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The first event upon mousedown means we transition to a "mousedown" state
|
|
||||||
if (this.rangeSliderClickDragState === "default") {
|
|
||||||
this.rangeSliderClickDragState = "mousedown";
|
|
||||||
|
|
||||||
// Exit early because we don't want to use the value set by where on the track the user pressed
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The second event upon mousedown that occurs by moving left or right means the user has committed to dragging the slider
|
|
||||||
if (this.rangeSliderClickDragState === "mousedown") {
|
|
||||||
this.rangeSliderClickDragState = "dragging";
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're in a dragging state, we want to use the new slider value
|
|
||||||
this.rangeSliderValueAsRendered = roundedValue;
|
|
||||||
this.updateValue(roundedValue);
|
|
||||||
},
|
|
||||||
sliderPointerDown() {
|
|
||||||
// We want to render the fake slider thumb at the old position, which is still the number held by `value`
|
|
||||||
this.rangeSliderValueAsRendered = this.value || 0;
|
|
||||||
|
|
||||||
// Because an `input` event is fired right before or after this (depending on browser), that first
|
|
||||||
// invocation will transition the state machine to `mousedown`. That's why we don't do it here.
|
|
||||||
},
|
|
||||||
sliderPointerUp() {
|
|
||||||
// User clicked but didn't drag, so we focus the text input element
|
|
||||||
if (this.rangeSliderClickDragState === "mousedown") {
|
|
||||||
const fieldInput = this.$refs.fieldInput as typeof FieldInput | undefined;
|
|
||||||
const inputElement = fieldInput?.$el.querySelector("[data-input-element]") as HTMLInputElement | undefined;
|
|
||||||
if (!inputElement) return;
|
|
||||||
|
|
||||||
// Set the slider position back to the original position to undo the user moving it
|
|
||||||
this.rangeSliderValue = this.rangeSliderValueAsRendered;
|
|
||||||
|
|
||||||
// Begin editing the number text field
|
|
||||||
inputElement.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Releasing the mouse means we can reset the state machine
|
|
||||||
this.rangeSliderClickDragState = "default";
|
|
||||||
},
|
|
||||||
onTextFocused() {
|
|
||||||
if (this.value === undefined) this.text = "";
|
|
||||||
else if (this.unitIsHiddenWhenEditing) this.text = `${this.value}`;
|
|
||||||
else this.text = `${this.value}${unPluralize(this.unit, this.value)}`;
|
|
||||||
|
|
||||||
this.editing = true;
|
|
||||||
|
|
||||||
(this.$refs.fieldInput as typeof FieldInput | undefined)?.selectAllText(this.text);
|
|
||||||
},
|
|
||||||
// Called only when `value` is changed from the <input> element via user input and committed, either with the
|
|
||||||
// enter key (via the `change` event) or when the <input> element is unfocused (with the `blur` event binding)
|
|
||||||
onTextChanged() {
|
|
||||||
// The `unFocus()` call at the bottom of this function and in `onCancelTextChange()` causes this function to be run again, so this check skips a second run
|
|
||||||
if (!this.editing) return;
|
|
||||||
|
|
||||||
const parsed = parseFloat(this.text);
|
|
||||||
const newValue = Number.isNaN(parsed) ? undefined : parsed;
|
|
||||||
|
|
||||||
this.updateValue(newValue);
|
|
||||||
|
|
||||||
this.editing = false;
|
|
||||||
|
|
||||||
(this.$refs.fieldInput as typeof FieldInput | undefined)?.unFocus();
|
|
||||||
},
|
|
||||||
onCancelTextChange() {
|
|
||||||
this.updateValue(undefined);
|
|
||||||
|
|
||||||
this.editing = false;
|
|
||||||
|
|
||||||
(this.$refs.fieldInput as typeof FieldInput | undefined)?.unFocus();
|
|
||||||
},
|
|
||||||
onIncrement(direction: "Decrease" | "Increase") {
|
|
||||||
if (this.value === undefined) return;
|
|
||||||
|
|
||||||
const actions = {
|
|
||||||
Add: (): void => {
|
|
||||||
const directionAddend = direction === "Increase" ? this.step : -this.step;
|
|
||||||
this.updateValue(this.value !== undefined ? this.value + directionAddend : undefined);
|
|
||||||
},
|
|
||||||
Multiply: (): void => {
|
|
||||||
const directionMultiplier = direction === "Increase" ? this.step : 1 / this.step;
|
|
||||||
this.updateValue(this.value !== undefined ? this.value * directionMultiplier : undefined);
|
|
||||||
},
|
|
||||||
Callback: (): void => {
|
|
||||||
if (direction === "Increase") this.incrementCallbackIncrease?.();
|
|
||||||
if (direction === "Decrease") this.incrementCallbackDecrease?.();
|
|
||||||
},
|
|
||||||
None: (): void => undefined,
|
|
||||||
};
|
|
||||||
const action = actions[this.incrementBehavior];
|
|
||||||
action();
|
|
||||||
},
|
|
||||||
updateValue(newValue: number | undefined) {
|
|
||||||
const nowValid = this.value !== undefined && this.isInteger ? Math.round(this.value) : this.value;
|
|
||||||
let cleaned = newValue !== undefined ? newValue : nowValid;
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Required as the call to update:value can, not change the value
|
|
||||||
this.text = this.displayText(this.value);
|
|
||||||
|
|
||||||
if (newValue !== undefined) this.$emit("update:value", cleaned);
|
|
||||||
},
|
|
||||||
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
|
|
||||||
// 0.23 == 0 (Reason for the slightly more complicated code)
|
|
||||||
const absValueInt = Math.floor(Math.abs(value));
|
|
||||||
const leftSideDigits = absValueInt === 0 ? 0 : absValueInt.toString().length;
|
|
||||||
const roundingPower = 10 ** Math.max(this.displayDecimalPlaces - leftSideDigits, 0);
|
|
||||||
|
|
||||||
const displayValue = Math.round(value * roundingPower) / roundingPower;
|
|
||||||
|
|
||||||
return `${displayValue}${unPluralize(this.unit, value)}`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
// Called only when `value` is changed from outside this component (with v-model)
|
|
||||||
value(newValue: number | undefined) {
|
|
||||||
// Draw a dash if the value is undefined
|
|
||||||
if (newValue === undefined) {
|
|
||||||
this.text = "-";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the range slider with the new value
|
|
||||||
this.rangeSliderValue = newValue;
|
|
||||||
this.rangeSliderValueAsRendered = newValue;
|
|
||||||
|
|
||||||
// The simple `clamp()` function can't be used here since `undefined` values need to be boundless
|
|
||||||
let sanitized = newValue;
|
|
||||||
if (typeof this.min === "number") sanitized = Math.max(sanitized, this.min);
|
|
||||||
if (typeof this.max === "number") sanitized = Math.min(sanitized, this.max);
|
|
||||||
|
|
||||||
this.text = this.displayText(sanitized);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: { FieldInput },
|
|
||||||
});
|
|
||||||
|
|
||||||
function unPluralize(unit: string, value: number): string {
|
|
||||||
if (value === 1 && unit.endsWith("s")) return unit.slice(0, -1);
|
|
||||||
return unit;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,26 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
import { type IconName } from "@/utility-functions/icons";
|
||||||
|
|
||||||
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
|
import CheckboxInput from "@/components/widgets/inputs/CheckboxInput.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
emits: ["update:checked"],
|
||||||
|
props: {
|
||||||
|
checked: { type: Boolean as PropType<boolean>, required: true },
|
||||||
|
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
icon: { type: String as PropType<IconName>, default: "Checkmark" },
|
||||||
|
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
CheckboxInput,
|
||||||
|
LayoutRow,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutRow class="optional-input" :class="disabled">
|
<LayoutRow class="optional-input" :class="disabled">
|
||||||
<CheckboxInput :checked="checked" :disabled="disabled" @input="(e: Event) => $emit('update:checked', (e.target as HTMLInputElement).checked)" :icon="icon" :tooltip="tooltip" />
|
<CheckboxInput :checked="checked" :disabled="disabled" @input="(e: Event) => $emit('update:checked', (e.target as HTMLInputElement).checked)" :icon="icon" :tooltip="tooltip" />
|
||||||
@@ -24,26 +47,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
import { type IconName } from "@/utility-functions/icons";
|
|
||||||
|
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|
||||||
import CheckboxInput from "@/components/widgets/inputs/CheckboxInput.vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
emits: ["update:checked"],
|
|
||||||
props: {
|
|
||||||
checked: { type: Boolean as PropType<boolean>, required: true },
|
|
||||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
icon: { type: String as PropType<IconName>, default: "Checkmark" },
|
|
||||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
CheckboxInput,
|
|
||||||
LayoutRow,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,36 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
import { type RadioEntries, type RadioEntryData } from "@/wasm-communication/messages";
|
||||||
|
|
||||||
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
|
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||||
|
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
emits: ["update:selectedIndex"],
|
||||||
|
props: {
|
||||||
|
entries: { type: Array as PropType<RadioEntries>, required: true },
|
||||||
|
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
selectedIndex: { type: Number as PropType<number>, required: true },
|
||||||
|
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleEntryClick(radioEntryData: RadioEntryData) {
|
||||||
|
const index = this.entries.indexOf(radioEntryData);
|
||||||
|
this.$emit("update:selectedIndex", index);
|
||||||
|
|
||||||
|
radioEntryData.action?.();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
IconLabel,
|
||||||
|
LayoutRow,
|
||||||
|
TextLabel,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutRow class="radio-input" :class="{ disabled }">
|
<LayoutRow class="radio-input" :class="{ disabled }">
|
||||||
<button
|
<button
|
||||||
@@ -88,36 +121,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
import { type RadioEntries, type RadioEntryData } from "@/wasm-communication/messages";
|
|
||||||
|
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|
||||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
|
||||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
emits: ["update:selectedIndex"],
|
|
||||||
props: {
|
|
||||||
entries: { type: Array as PropType<RadioEntries>, required: true },
|
|
||||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
selectedIndex: { type: Number as PropType<number>, required: true },
|
|
||||||
sharpRightCorners: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleEntryClick(radioEntryData: RadioEntryData) {
|
|
||||||
const index = this.entries.indexOf(radioEntryData);
|
|
||||||
this.$emit("update:selectedIndex", index);
|
|
||||||
|
|
||||||
radioEntryData.action?.();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
IconLabel,
|
|
||||||
LayoutRow,
|
|
||||||
TextLabel,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,48 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
import { type Color } from "@/wasm-communication/messages";
|
||||||
|
|
||||||
|
import ColorPicker from "@/components/floating-menus/ColorPicker.vue";
|
||||||
|
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||||
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
inject: ["editor"],
|
||||||
|
props: {
|
||||||
|
primary: { type: Object as PropType<Color>, required: true },
|
||||||
|
secondary: { type: Object as PropType<Color>, required: true },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
primaryOpen: false,
|
||||||
|
secondaryOpen: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickPrimarySwatch() {
|
||||||
|
this.primaryOpen = true;
|
||||||
|
this.secondaryOpen = false;
|
||||||
|
},
|
||||||
|
clickSecondarySwatch() {
|
||||||
|
this.primaryOpen = false;
|
||||||
|
this.secondaryOpen = true;
|
||||||
|
},
|
||||||
|
primaryColorChanged(color: Color) {
|
||||||
|
this.editor.instance.updatePrimaryColor(color.red, color.green, color.blue, color.alpha);
|
||||||
|
},
|
||||||
|
secondaryColorChanged(color: Color) {
|
||||||
|
this.editor.instance.updateSecondaryColor(color.red, color.green, color.blue, color.alpha);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
ColorPicker,
|
||||||
|
LayoutCol,
|
||||||
|
LayoutRow,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutCol class="swatch-pair">
|
<LayoutCol class="swatch-pair">
|
||||||
<LayoutRow class="primary swatch">
|
<LayoutRow class="primary swatch">
|
||||||
@@ -49,48 +94,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
import { type Color } from "@/wasm-communication/messages";
|
|
||||||
|
|
||||||
import ColorPicker from "@/components/floating-menus/ColorPicker.vue";
|
|
||||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
inject: ["editor"],
|
|
||||||
props: {
|
|
||||||
primary: { type: Object as PropType<Color>, required: true },
|
|
||||||
secondary: { type: Object as PropType<Color>, required: true },
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
primaryOpen: false,
|
|
||||||
secondaryOpen: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
clickPrimarySwatch() {
|
|
||||||
this.primaryOpen = true;
|
|
||||||
this.secondaryOpen = false;
|
|
||||||
},
|
|
||||||
clickSecondarySwatch() {
|
|
||||||
this.primaryOpen = false;
|
|
||||||
this.secondaryOpen = true;
|
|
||||||
},
|
|
||||||
primaryColorChanged(color: Color) {
|
|
||||||
this.editor.instance.updatePrimaryColor(color.red, color.green, color.blue, color.alpha);
|
|
||||||
},
|
|
||||||
secondaryColorChanged(color: Color) {
|
|
||||||
this.editor.instance.updateSecondaryColor(color.red, color.green, color.blue, color.alpha);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
ColorPicker,
|
|
||||||
LayoutCol,
|
|
||||||
LayoutRow,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,22 +1,3 @@
|
|||||||
<template>
|
|
||||||
<FieldInput
|
|
||||||
:textarea="true"
|
|
||||||
class="text-area-input"
|
|
||||||
:class="{ 'has-label': label }"
|
|
||||||
:label="label"
|
|
||||||
:spellcheck="true"
|
|
||||||
:disabled="disabled"
|
|
||||||
:tooltip="tooltip"
|
|
||||||
v-model:value="inputValue"
|
|
||||||
@textFocused="() => onTextFocused()"
|
|
||||||
@textChanged="() => onTextChanged()"
|
|
||||||
@cancelTextChange="() => onCancelTextChange()"
|
|
||||||
ref="fieldInput"
|
|
||||||
></FieldInput>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss"></style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
@@ -74,3 +55,22 @@ export default defineComponent({
|
|||||||
components: { FieldInput },
|
components: { FieldInput },
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FieldInput
|
||||||
|
:textarea="true"
|
||||||
|
class="text-area-input"
|
||||||
|
:class="{ 'has-label': label }"
|
||||||
|
:label="label"
|
||||||
|
:spellcheck="true"
|
||||||
|
:disabled="disabled"
|
||||||
|
:tooltip="tooltip"
|
||||||
|
v-model:value="inputValue"
|
||||||
|
@textFocused="() => onTextFocused()"
|
||||||
|
@textChanged="() => onTextChanged()"
|
||||||
|
@cancelTextChange="() => onCancelTextChange()"
|
||||||
|
ref="fieldInput"
|
||||||
|
></FieldInput>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
|
|||||||
@@ -1,36 +1,3 @@
|
|||||||
<template>
|
|
||||||
<FieldInput
|
|
||||||
class="text-input"
|
|
||||||
:class="{ centered }"
|
|
||||||
v-model:value="text"
|
|
||||||
:label="label"
|
|
||||||
:spellcheck="true"
|
|
||||||
:disabled="disabled"
|
|
||||||
:tooltip="tooltip"
|
|
||||||
:placeholder="placeholder"
|
|
||||||
:style="{ 'min-width': minWidth > 0 ? `${minWidth}px` : undefined }"
|
|
||||||
:sharpRightCorners="sharpRightCorners"
|
|
||||||
@textFocused="() => onTextFocused()"
|
|
||||||
@textChanged="() => onTextChanged()"
|
|
||||||
@cancelTextChange="() => onCancelTextChange()"
|
|
||||||
ref="fieldInput"
|
|
||||||
></FieldInput>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.text-input {
|
|
||||||
input {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.centered {
|
|
||||||
input:not(:focus) {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
@@ -101,3 +68,36 @@ export default defineComponent({
|
|||||||
components: { FieldInput },
|
components: { FieldInput },
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<FieldInput
|
||||||
|
class="text-input"
|
||||||
|
:class="{ centered }"
|
||||||
|
v-model:value="text"
|
||||||
|
:label="label"
|
||||||
|
:spellcheck="true"
|
||||||
|
:disabled="disabled"
|
||||||
|
:tooltip="tooltip"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:style="{ 'min-width': minWidth > 0 ? `${minWidth}px` : undefined }"
|
||||||
|
:sharpRightCorners="sharpRightCorners"
|
||||||
|
@textFocused="() => onTextFocused()"
|
||||||
|
@textChanged="() => onTextChanged()"
|
||||||
|
@cancelTextChange="() => onCancelTextChange()"
|
||||||
|
ref="fieldInput"
|
||||||
|
></FieldInput>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.text-input {
|
||||||
|
input {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.centered {
|
||||||
|
input:not(:focus) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,3 +1,28 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
import { type IconName, ICONS, ICON_COMPONENTS } from "@/utility-functions/icons";
|
||||||
|
|
||||||
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
icon: { type: String as PropType<IconName>, required: true },
|
||||||
|
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
iconSizeClass(): string {
|
||||||
|
return `size-${ICONS[this.icon].size}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
LayoutRow,
|
||||||
|
...ICON_COMPONENTS,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutRow :class="['icon-label', iconSizeClass, { disabled }]" :title="tooltip">
|
<LayoutRow :class="['icon-label', iconSizeClass, { disabled }]" :title="tooltip">
|
||||||
<component :is="icon" />
|
<component :is="icon" />
|
||||||
@@ -29,28 +54,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
import { type IconName, ICONS, ICON_COMPONENTS } from "@/utility-functions/icons";
|
|
||||||
|
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
icon: { type: String as PropType<IconName>, required: true },
|
|
||||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
iconSizeClass(): string {
|
|
||||||
return `size-${ICONS[this.icon].size}`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
LayoutRow,
|
|
||||||
...ICON_COMPONENTS,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
import { type SeparatorDirection, type SeparatorType } from "@/wasm-communication/messages";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
direction: { type: String as PropType<SeparatorDirection>, default: "Horizontal" },
|
||||||
|
type: { type: String as PropType<SeparatorType>, default: "Unrelated" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="separator" :class="[direction.toLowerCase(), type.toLowerCase()]">
|
<div class="separator" :class="[direction.toLowerCase(), type.toLowerCase()]">
|
||||||
<div v-if="['Section', 'List'].includes(type)"></div>
|
<div v-if="['Section', 'List'].includes(type)"></div>
|
||||||
@@ -71,16 +84,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
import { type SeparatorDirection, type SeparatorType } from "@/wasm-communication/messages";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
direction: { type: String as PropType<SeparatorDirection>, default: "Horizontal" },
|
|
||||||
type: { type: String as PropType<SeparatorType>, default: "Unrelated" },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
disabled: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
bold: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
italic: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
tableAlign: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
minWidth: { type: Number as PropType<number>, default: 0 },
|
||||||
|
multiline: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
tooltip: { type: String as PropType<string | undefined>, required: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<span class="text-label" :class="{ disabled, bold, italic, multiline, 'table-align': tableAlign }" :style="{ 'min-width': minWidth > 0 ? `${minWidth}px` : undefined }" :title="tooltip">
|
<span class="text-label" :class="{ disabled, bold, italic, multiline, 'table-align': tableAlign }" :style="{ 'min-width': minWidth > 0 ? `${minWidth}px` : undefined }" :title="tooltip">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
@@ -34,19 +50,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
disabled: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
bold: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
italic: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
tableAlign: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
minWidth: { type: Number as PropType<number>, default: 0 },
|
|
||||||
multiline: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
tooltip: { type: String as PropType<string | undefined>, required: false },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,127 +1,3 @@
|
|||||||
<template>
|
|
||||||
<IconLabel class="user-input-label keyboard-lock-notice" v-if="displayKeyboardLockNotice" :icon="'Info'" :title="keyboardLockInfoMessage" />
|
|
||||||
<LayoutRow class="user-input-label" v-else>
|
|
||||||
<template v-for="(keysWithLabels, i) in keysWithLabelsGroups" :key="i">
|
|
||||||
<Separator :type="'Related'" v-if="i > 0"></Separator>
|
|
||||||
<template v-for="(keyInfo, j) in keyTextOrIconList(keysWithLabels)" :key="j">
|
|
||||||
<div class="input-key" :class="keyInfo.width">
|
|
||||||
<IconLabel v-if="keyInfo.icon" :icon="keyInfo.icon" />
|
|
||||||
<TextLabel v-else-if="keyInfo.label !== undefined">{{ keyInfo.label }}</TextLabel>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
<div class="input-mouse" v-if="mouseMotion">
|
|
||||||
<IconLabel :icon="mouseHintIcon(mouseMotion)" />
|
|
||||||
</div>
|
|
||||||
<div class="hint-text" v-if="hasSlotContent">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</LayoutRow>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.user-input-label {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
height: 100%;
|
|
||||||
align-items: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
.input-key,
|
|
||||||
.input-mouse {
|
|
||||||
& + .input-key,
|
|
||||||
& + .input-mouse {
|
|
||||||
margin-left: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-key {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-family: "Inconsolata", monospace;
|
|
||||||
font-weight: 400;
|
|
||||||
text-align: center;
|
|
||||||
height: 16px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border: 1px solid;
|
|
||||||
border-radius: 4px;
|
|
||||||
border-color: var(--color-5-dullgray);
|
|
||||||
color: var(--color-e-nearwhite);
|
|
||||||
|
|
||||||
.text-label {
|
|
||||||
// Firefox renders the text 1px lower than Chrome (tested on Windows) with 16px line-height,
|
|
||||||
// so moving it up 1 pixel by using 15px makes them agree.
|
|
||||||
line-height: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.width-1 {
|
|
||||||
width: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.width-2 {
|
|
||||||
width: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.width-3 {
|
|
||||||
width: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.width-4 {
|
|
||||||
width: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.width-5 {
|
|
||||||
width: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-label {
|
|
||||||
margin: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-mouse {
|
|
||||||
.bright {
|
|
||||||
fill: var(--color-e-nearwhite);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dim {
|
|
||||||
fill: var(--color-8-uppergray);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.hint-text {
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.floating-menu-content .row > & {
|
|
||||||
.input-key {
|
|
||||||
border-color: var(--color-3-darkgray);
|
|
||||||
color: var(--color-8-uppergray);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-key .icon-label svg,
|
|
||||||
&.keyboard-lock-notice.keyboard-lock-notice svg,
|
|
||||||
.input-mouse .bright {
|
|
||||||
fill: var(--color-8-uppergray);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-mouse .dim {
|
|
||||||
fill: var(--color-3-darkgray);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.floating-menu-content .row:hover > & {
|
|
||||||
.input-key {
|
|
||||||
border-color: var(--color-7-middlegray);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-mouse .dim {
|
|
||||||
fill: var(--color-7-middlegray);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
@@ -247,3 +123,127 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<IconLabel class="user-input-label keyboard-lock-notice" v-if="displayKeyboardLockNotice" :icon="'Info'" :title="keyboardLockInfoMessage" />
|
||||||
|
<LayoutRow class="user-input-label" v-else>
|
||||||
|
<template v-for="(keysWithLabels, i) in keysWithLabelsGroups" :key="i">
|
||||||
|
<Separator :type="'Related'" v-if="i > 0"></Separator>
|
||||||
|
<template v-for="(keyInfo, j) in keyTextOrIconList(keysWithLabels)" :key="j">
|
||||||
|
<div class="input-key" :class="keyInfo.width">
|
||||||
|
<IconLabel v-if="keyInfo.icon" :icon="keyInfo.icon" />
|
||||||
|
<TextLabel v-else-if="keyInfo.label !== undefined">{{ keyInfo.label }}</TextLabel>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<div class="input-mouse" v-if="mouseMotion">
|
||||||
|
<IconLabel :icon="mouseHintIcon(mouseMotion)" />
|
||||||
|
</div>
|
||||||
|
<div class="hint-text" v-if="hasSlotContent">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</LayoutRow>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.user-input-label {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.input-key,
|
||||||
|
.input-mouse {
|
||||||
|
& + .input-key,
|
||||||
|
& + .input-mouse {
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-key {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-family: "Inconsolata", monospace;
|
||||||
|
font-weight: 400;
|
||||||
|
text-align: center;
|
||||||
|
height: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-color: var(--color-5-dullgray);
|
||||||
|
color: var(--color-e-nearwhite);
|
||||||
|
|
||||||
|
.text-label {
|
||||||
|
// Firefox renders the text 1px lower than Chrome (tested on Windows) with 16px line-height,
|
||||||
|
// so moving it up 1 pixel by using 15px makes them agree.
|
||||||
|
line-height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.width-1 {
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.width-2 {
|
||||||
|
width: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.width-3 {
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.width-4 {
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.width-5 {
|
||||||
|
width: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-label {
|
||||||
|
margin: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-mouse {
|
||||||
|
.bright {
|
||||||
|
fill: var(--color-e-nearwhite);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dim {
|
||||||
|
fill: var(--color-8-uppergray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-text {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-menu-content .row > & {
|
||||||
|
.input-key {
|
||||||
|
border-color: var(--color-3-darkgray);
|
||||||
|
color: var(--color-8-uppergray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-key .icon-label svg,
|
||||||
|
&.keyboard-lock-notice.keyboard-lock-notice svg,
|
||||||
|
.input-mouse .bright {
|
||||||
|
fill: var(--color-8-uppergray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-mouse .dim {
|
||||||
|
fill: var(--color-3-darkgray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-menu-content .row:hover > & {
|
||||||
|
.input-key {
|
||||||
|
border-color: var(--color-7-middlegray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-mouse .dim {
|
||||||
|
fill: var(--color-7-middlegray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,47 +1,3 @@
|
|||||||
<template>
|
|
||||||
<div class="canvas-ruler" :class="direction.toLowerCase()" ref="canvasRuler">
|
|
||||||
<svg :style="svgBounds">
|
|
||||||
<path :d="svgPath" />
|
|
||||||
<text v-for="(svgText, index) in svgTexts" :key="index" :transform="svgText.transform">{{ svgText.text }}</text>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.canvas-ruler {
|
|
||||||
flex: 1 1 100%;
|
|
||||||
background: var(--color-4-dimgray);
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&.horizontal {
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.vertical {
|
|
||||||
width: 16px;
|
|
||||||
|
|
||||||
svg text {
|
|
||||||
text-anchor: end;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
position: absolute;
|
|
||||||
|
|
||||||
path {
|
|
||||||
stroke-width: 1px;
|
|
||||||
stroke: var(--color-7-middlegray);
|
|
||||||
}
|
|
||||||
|
|
||||||
text {
|
|
||||||
font-size: 12px;
|
|
||||||
fill: var(--color-8-uppergray);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
@@ -146,3 +102,47 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="canvas-ruler" :class="direction.toLowerCase()" ref="canvasRuler">
|
||||||
|
<svg :style="svgBounds">
|
||||||
|
<path :d="svgPath" />
|
||||||
|
<text v-for="(svgText, index) in svgTexts" :key="index" :transform="svgText.transform">{{ svgText.text }}</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.canvas-ruler {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
background: var(--color-4-dimgray);
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.horizontal {
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.vertical {
|
||||||
|
width: 16px;
|
||||||
|
|
||||||
|
svg text {
|
||||||
|
text-anchor: end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
path {
|
||||||
|
stroke-width: 1px;
|
||||||
|
stroke: var(--color-7-middlegray);
|
||||||
|
}
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 12px;
|
||||||
|
fill: var(--color-8-uppergray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,111 +1,3 @@
|
|||||||
<template>
|
|
||||||
<div class="persistent-scrollbar" :class="direction.toLowerCase()">
|
|
||||||
<button class="arrow decrease" @pointerdown="() => changePosition(-50)" tabindex="-1"></button>
|
|
||||||
<div class="scroll-track" ref="scrollTrack" @pointerdown="(e) => grabArea(e)">
|
|
||||||
<div class="scroll-thumb" @pointerdown="(e) => grabHandle(e)" :class="{ dragging }" :style="[thumbStart, thumbEnd, sides]"></div>
|
|
||||||
</div>
|
|
||||||
<button class="arrow increase" @click="() => changePosition(50)" tabindex="-1"></button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.persistent-scrollbar {
|
|
||||||
display: flex;
|
|
||||||
flex: 1 1 100%;
|
|
||||||
|
|
||||||
.arrow {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
border-style: solid;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-track {
|
|
||||||
flex: 1 1 100%;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.scroll-thumb {
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: var(--color-5-dullgray);
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&.dragging {
|
|
||||||
background: var(--color-6-lowergray);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.scroll-click-area {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.vertical {
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.arrow.decrease {
|
|
||||||
margin: 4px 3px;
|
|
||||||
border-width: 0 5px 8px 5px;
|
|
||||||
border-color: transparent transparent var(--color-5-dullgray) transparent;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: transparent transparent var(--color-6-lowergray) transparent;
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
border-color: transparent transparent var(--color-c-brightgray) transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow.increase {
|
|
||||||
margin: 4px 3px;
|
|
||||||
border-width: 8px 5px 0 5px;
|
|
||||||
border-color: var(--color-5-dullgray) transparent transparent transparent;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: var(--color-6-lowergray) transparent transparent transparent;
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
border-color: var(--color-c-brightgray) transparent transparent transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.horizontal {
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
.arrow.decrease {
|
|
||||||
margin: 3px 4px;
|
|
||||||
border-width: 5px 8px 5px 0;
|
|
||||||
border-color: transparent var(--color-5-dullgray) transparent transparent;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: transparent var(--color-6-lowergray) transparent transparent;
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
border-color: transparent var(--color-c-brightgray) transparent transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow.increase {
|
|
||||||
margin: 3px 4px;
|
|
||||||
border-width: 5px 0 5px 8px;
|
|
||||||
border-color: transparent transparent transparent var(--color-5-dullgray);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: transparent transparent transparent var(--color-6-lowergray);
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
border-color: transparent transparent transparent var(--color-c-brightgray);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
@@ -217,3 +109,111 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="persistent-scrollbar" :class="direction.toLowerCase()">
|
||||||
|
<button class="arrow decrease" @pointerdown="() => changePosition(-50)" tabindex="-1"></button>
|
||||||
|
<div class="scroll-track" ref="scrollTrack" @pointerdown="(e) => grabArea(e)">
|
||||||
|
<div class="scroll-thumb" @pointerdown="(e) => grabHandle(e)" :class="{ dragging }" :style="[thumbStart, thumbEnd, sides]"></div>
|
||||||
|
</div>
|
||||||
|
<button class="arrow increase" @click="() => changePosition(50)" tabindex="-1"></button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.persistent-scrollbar {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 100%;
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-style: solid;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-track {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.scroll-thumb {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--color-5-dullgray);
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.dragging {
|
||||||
|
background: var(--color-6-lowergray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-click-area {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.vertical {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.arrow.decrease {
|
||||||
|
margin: 4px 3px;
|
||||||
|
border-width: 0 5px 8px 5px;
|
||||||
|
border-color: transparent transparent var(--color-5-dullgray) transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: transparent transparent var(--color-6-lowergray) transparent;
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
border-color: transparent transparent var(--color-c-brightgray) transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow.increase {
|
||||||
|
margin: 4px 3px;
|
||||||
|
border-width: 8px 5px 0 5px;
|
||||||
|
border-color: var(--color-5-dullgray) transparent transparent transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--color-6-lowergray) transparent transparent transparent;
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
border-color: var(--color-c-brightgray) transparent transparent transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.horizontal {
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.arrow.decrease {
|
||||||
|
margin: 3px 4px;
|
||||||
|
border-width: 5px 8px 5px 0;
|
||||||
|
border-color: transparent var(--color-5-dullgray) transparent transparent;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: transparent var(--color-6-lowergray) transparent transparent;
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
border-color: transparent var(--color-c-brightgray) transparent transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow.increase {
|
||||||
|
margin: 3px 4px;
|
||||||
|
border-width: 5px 0 5px 8px;
|
||||||
|
border-color: transparent transparent transparent var(--color-5-dullgray);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: transparent transparent transparent var(--color-6-lowergray);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
border-color: transparent transparent transparent var(--color-c-brightgray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,21 +1,3 @@
|
|||||||
<template>
|
|
||||||
<LayoutCol class="main-window">
|
|
||||||
<TitleBar :platform="platform" :maximized="maximized" />
|
|
||||||
|
|
||||||
<Workspace />
|
|
||||||
|
|
||||||
<StatusBar />
|
|
||||||
</LayoutCol>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.main-window {
|
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
touch-action: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
@@ -41,3 +23,21 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<LayoutCol class="main-window">
|
||||||
|
<TitleBar :platform="platform" :maximized="maximized" />
|
||||||
|
|
||||||
|
<Workspace />
|
||||||
|
|
||||||
|
<StatusBar />
|
||||||
|
</LayoutCol>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.main-window {
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,3 +1,39 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
|
import { platformIsMac } from "@/utility-functions/platform";
|
||||||
|
import { type HintData, type HintInfo, type LayoutKeysGroup, UpdateInputHints } from "@/wasm-communication/messages";
|
||||||
|
|
||||||
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
|
import Separator from "@/components/widgets/labels/Separator.vue";
|
||||||
|
import UserInputLabel from "@/components/widgets/labels/UserInputLabel.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
inject: ["editor"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hintData: [] as HintData,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
inputKeysForPlatform(hint: HintInfo): LayoutKeysGroup[] {
|
||||||
|
if (platformIsMac() && hint.keyGroupsMac) return hint.keyGroupsMac;
|
||||||
|
return hint.keyGroups;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.editor.subscriptions.subscribeJsMessage(UpdateInputHints, (updateInputHints) => {
|
||||||
|
this.hintData = updateInputHints.hintData;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
LayoutRow,
|
||||||
|
Separator,
|
||||||
|
UserInputLabel,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutRow class="status-bar">
|
<LayoutRow class="status-bar">
|
||||||
<LayoutRow class="hint-groups">
|
<LayoutRow class="hint-groups">
|
||||||
@@ -44,39 +80,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
import { platformIsMac } from "@/utility-functions/platform";
|
|
||||||
import { type HintData, type HintInfo, type LayoutKeysGroup, UpdateInputHints } from "@/wasm-communication/messages";
|
|
||||||
|
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|
||||||
import Separator from "@/components/widgets/labels/Separator.vue";
|
|
||||||
import UserInputLabel from "@/components/widgets/labels/UserInputLabel.vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
inject: ["editor"],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
hintData: [] as HintData,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
inputKeysForPlatform(hint: HintInfo): LayoutKeysGroup[] {
|
|
||||||
if (platformIsMac() && hint.keyGroupsMac) return hint.keyGroupsMac;
|
|
||||||
return hint.keyGroups;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.editor.subscriptions.subscribeJsMessage(UpdateInputHints, (updateInputHints) => {
|
|
||||||
this.hintData = updateInputHints.hintData;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
LayoutRow,
|
|
||||||
Separator,
|
|
||||||
UserInputLabel,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,40 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
|
import MenuBarInput from "@/components/widgets/inputs/MenuBarInput.vue";
|
||||||
|
import WindowButtonsMac from "@/components/window/title-bar/WindowButtonsMac.vue";
|
||||||
|
import WindowButtonsWeb from "@/components/window/title-bar/WindowButtonsWeb.vue";
|
||||||
|
import WindowButtonsWindows from "@/components/window/title-bar/WindowButtonsWindows.vue";
|
||||||
|
import WindowTitle from "@/components/window/title-bar/WindowTitle.vue";
|
||||||
|
|
||||||
|
export type Platform = "Windows" | "Mac" | "Linux" | "Web";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
inject: ["portfolio"],
|
||||||
|
props: {
|
||||||
|
platform: { type: String as PropType<Platform>, required: true },
|
||||||
|
maximized: { type: Boolean as PropType<boolean>, required: true },
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
windowTitle(): string {
|
||||||
|
const activeDocumentIndex = this.portfolio.state.activeDocumentIndex;
|
||||||
|
const activeDocumentDisplayName = this.portfolio.state.documents[activeDocumentIndex]?.displayName || "";
|
||||||
|
|
||||||
|
return `${activeDocumentDisplayName}${activeDocumentDisplayName && " - "}Graphite`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
LayoutRow,
|
||||||
|
MenuBarInput,
|
||||||
|
WindowButtonsMac,
|
||||||
|
WindowButtonsWeb,
|
||||||
|
WindowButtonsWindows,
|
||||||
|
WindowTitle,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutRow class="title-bar">
|
<LayoutRow class="title-bar">
|
||||||
<LayoutRow class="header-part">
|
<LayoutRow class="header-part">
|
||||||
@@ -36,40 +73,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|
||||||
import MenuBarInput from "@/components/widgets/inputs/MenuBarInput.vue";
|
|
||||||
import WindowButtonsMac from "@/components/window/title-bar/WindowButtonsMac.vue";
|
|
||||||
import WindowButtonsWeb from "@/components/window/title-bar/WindowButtonsWeb.vue";
|
|
||||||
import WindowButtonsWindows from "@/components/window/title-bar/WindowButtonsWindows.vue";
|
|
||||||
import WindowTitle from "@/components/window/title-bar/WindowTitle.vue";
|
|
||||||
|
|
||||||
export type Platform = "Windows" | "Mac" | "Linux" | "Web";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
inject: ["portfolio"],
|
|
||||||
props: {
|
|
||||||
platform: { type: String as PropType<Platform>, required: true },
|
|
||||||
maximized: { type: Boolean as PropType<boolean>, required: true },
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
windowTitle(): string {
|
|
||||||
const activeDocumentIndex = this.portfolio.state.activeDocumentIndex;
|
|
||||||
const activeDocumentDisplayName = this.portfolio.state.documents[activeDocumentIndex]?.displayName || "";
|
|
||||||
|
|
||||||
return `${activeDocumentDisplayName}${activeDocumentDisplayName && " - "}Graphite`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
LayoutRow,
|
|
||||||
MenuBarInput,
|
|
||||||
WindowButtonsMac,
|
|
||||||
WindowButtonsWeb,
|
|
||||||
WindowButtonsWindows,
|
|
||||||
WindowTitle,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,16 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
maximized: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
},
|
||||||
|
components: { LayoutRow },
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutRow class="window-buttons mac">
|
<LayoutRow class="window-buttons mac">
|
||||||
<div class="close" title="Close"></div>
|
<div class="close" title="Close"></div>
|
||||||
@@ -37,16 +50,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
maximized: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
},
|
|
||||||
components: { LayoutRow },
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
|
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||||
|
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
inject: ["fullscreen"],
|
||||||
|
methods: {
|
||||||
|
async handleClick() {
|
||||||
|
if (this.fullscreen.state.windowFullscreen) this.fullscreen.exitFullscreen();
|
||||||
|
else this.fullscreen.enterFullscreen();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
requestFullscreenHotkeys() {
|
||||||
|
return this.fullscreen.keyboardLockApiSupported && !this.fullscreen.state.keyboardLocked;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
IconLabel,
|
||||||
|
LayoutRow,
|
||||||
|
TextLabel,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutRow class="window-buttons-web" @click="() => handleClick()" :title="(fullscreen.state.windowFullscreen ? 'Exit' : 'Enter') + ' Fullscreen (F11)'">
|
<LayoutRow class="window-buttons-web" @click="() => handleClick()" :title="(fullscreen.state.windowFullscreen ? 'Exit' : 'Enter') + ' Fullscreen (F11)'">
|
||||||
<TextLabel v-if="requestFullscreenHotkeys" :italic="true">Go fullscreen to access all hotkeys</TextLabel>
|
<TextLabel v-if="requestFullscreenHotkeys" :italic="true">Go fullscreen to access all hotkeys</TextLabel>
|
||||||
@@ -29,31 +57,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from "vue";
|
|
||||||
|
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|
||||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
|
||||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
inject: ["fullscreen"],
|
|
||||||
methods: {
|
|
||||||
async handleClick() {
|
|
||||||
if (this.fullscreen.state.windowFullscreen) this.fullscreen.exitFullscreen();
|
|
||||||
else this.fullscreen.enterFullscreen();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
requestFullscreenHotkeys() {
|
|
||||||
return this.fullscreen.keyboardLockApiSupported && !this.fullscreen.state.keyboardLocked;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
IconLabel,
|
|
||||||
LayoutRow,
|
|
||||||
TextLabel,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,3 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
|
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
maximized: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
IconLabel,
|
||||||
|
LayoutRow,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutRow class="window-button windows minimize" title="Minimize">
|
<LayoutRow class="window-button windows minimize" title="Minimize">
|
||||||
<IconLabel :icon="'WindowButtonWinMinimize'" />
|
<IconLabel :icon="'WindowButtonWinMinimize'" />
|
||||||
@@ -36,20 +53,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, type PropType } from "vue";
|
|
||||||
|
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|
||||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
maximized: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
IconLabel,
|
|
||||||
LayoutRow,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,18 +1,3 @@
|
|||||||
<template>
|
|
||||||
<LayoutRow class="window-title">
|
|
||||||
<TextLabel>{{ text }}</TextLabel>
|
|
||||||
</LayoutRow>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.window-title {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
align-items: center;
|
|
||||||
white-space: nowrap;
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
|
||||||
@@ -29,3 +14,18 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<LayoutRow class="window-title">
|
||||||
|
<TextLabel>{{ text }}</TextLabel>
|
||||||
|
</LayoutRow>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.window-title {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,3 +1,83 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, nextTick, type PropType } from "vue";
|
||||||
|
|
||||||
|
import { platformIsMac } from "@/utility-functions/platform";
|
||||||
|
|
||||||
|
import { type LayoutKeysGroup, type Key } from "@/wasm-communication/messages";
|
||||||
|
|
||||||
|
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
||||||
|
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
||||||
|
import Document from "@/components/panels/Document.vue";
|
||||||
|
import LayerTree from "@/components/panels/LayerTree.vue";
|
||||||
|
import NodeGraph from "@/components/panels/NodeGraph.vue";
|
||||||
|
import Properties from "@/components/panels/Properties.vue";
|
||||||
|
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
||||||
|
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
|
||||||
|
import TextButton from "@/components/widgets/buttons/TextButton.vue";
|
||||||
|
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
||||||
|
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
||||||
|
import UserInputLabel from "@/components/widgets/labels/UserInputLabel.vue";
|
||||||
|
|
||||||
|
const PANEL_COMPONENTS = {
|
||||||
|
Document,
|
||||||
|
IconButton,
|
||||||
|
LayerTree,
|
||||||
|
NodeGraph,
|
||||||
|
PopoverButton,
|
||||||
|
Properties,
|
||||||
|
TextButton,
|
||||||
|
};
|
||||||
|
type PanelTypes = keyof typeof PANEL_COMPONENTS;
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
inject: ["editor"],
|
||||||
|
props: {
|
||||||
|
tabMinWidths: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
tabCloseButtons: { type: Boolean as PropType<boolean>, default: false },
|
||||||
|
tabLabels: { type: Array as PropType<{ name: string; tooltip?: string }[]>, required: true },
|
||||||
|
tabActiveIndex: { type: Number as PropType<number>, required: true },
|
||||||
|
panelType: { type: String as PropType<PanelTypes>, required: false },
|
||||||
|
clickAction: { type: Function as PropType<(index: number) => void>, required: false },
|
||||||
|
closeAction: { type: Function as PropType<(index: number) => void>, required: false },
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
newDocument() {
|
||||||
|
this.editor.instance.newDocumentDialog();
|
||||||
|
},
|
||||||
|
openDocument() {
|
||||||
|
this.editor.instance.documentOpen();
|
||||||
|
},
|
||||||
|
platformModifiers(reservedKey: boolean): LayoutKeysGroup {
|
||||||
|
// TODO: Remove this by properly feeding these keys from a layout provided by the backend
|
||||||
|
|
||||||
|
const ALT: Key = { key: "Alt", label: "Alt" };
|
||||||
|
const COMMAND: Key = { key: "Command", label: "Command" };
|
||||||
|
const CONTROL: Key = { key: "Control", label: "Ctrl" };
|
||||||
|
|
||||||
|
if (platformIsMac()) return reservedKey ? [ALT, COMMAND] : [COMMAND];
|
||||||
|
return reservedKey ? [CONTROL, ALT] : [CONTROL];
|
||||||
|
},
|
||||||
|
async scrollTabIntoView(newIndex: number) {
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const panel: HTMLDivElement | undefined = this.$el;
|
||||||
|
if (!panel) return;
|
||||||
|
|
||||||
|
const newActiveTab = panel.querySelectorAll("[data-tab]")[newIndex] as HTMLDivElement | undefined;
|
||||||
|
newActiveTab?.scrollIntoView();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
IconLabel,
|
||||||
|
LayoutCol,
|
||||||
|
LayoutRow,
|
||||||
|
TextLabel,
|
||||||
|
UserInputLabel,
|
||||||
|
...PANEL_COMPONENTS,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutCol class="panel">
|
<LayoutCol class="panel">
|
||||||
<LayoutRow class="tab-bar" :class="{ 'min-widths': tabMinWidths }">
|
<LayoutRow class="tab-bar" :class="{ 'min-widths': tabMinWidths }">
|
||||||
@@ -207,83 +287,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, nextTick, type PropType } from "vue";
|
|
||||||
|
|
||||||
import { platformIsMac } from "@/utility-functions/platform";
|
|
||||||
|
|
||||||
import { type LayoutKeysGroup, type Key } from "@/wasm-communication/messages";
|
|
||||||
|
|
||||||
import LayoutCol from "@/components/layout/LayoutCol.vue";
|
|
||||||
import LayoutRow from "@/components/layout/LayoutRow.vue";
|
|
||||||
import Document from "@/components/panels/Document.vue";
|
|
||||||
import LayerTree from "@/components/panels/LayerTree.vue";
|
|
||||||
import NodeGraph from "@/components/panels/NodeGraph.vue";
|
|
||||||
import Properties from "@/components/panels/Properties.vue";
|
|
||||||
import IconButton from "@/components/widgets/buttons/IconButton.vue";
|
|
||||||
import PopoverButton from "@/components/widgets/buttons/PopoverButton.vue";
|
|
||||||
import TextButton from "@/components/widgets/buttons/TextButton.vue";
|
|
||||||
import IconLabel from "@/components/widgets/labels/IconLabel.vue";
|
|
||||||
import TextLabel from "@/components/widgets/labels/TextLabel.vue";
|
|
||||||
import UserInputLabel from "@/components/widgets/labels/UserInputLabel.vue";
|
|
||||||
|
|
||||||
const PANEL_COMPONENTS = {
|
|
||||||
Document,
|
|
||||||
IconButton,
|
|
||||||
LayerTree,
|
|
||||||
NodeGraph,
|
|
||||||
PopoverButton,
|
|
||||||
Properties,
|
|
||||||
TextButton,
|
|
||||||
};
|
|
||||||
type PanelTypes = keyof typeof PANEL_COMPONENTS;
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
inject: ["editor"],
|
|
||||||
props: {
|
|
||||||
tabMinWidths: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
tabCloseButtons: { type: Boolean as PropType<boolean>, default: false },
|
|
||||||
tabLabels: { type: Array as PropType<{ name: string; tooltip?: string }[]>, required: true },
|
|
||||||
tabActiveIndex: { type: Number as PropType<number>, required: true },
|
|
||||||
panelType: { type: String as PropType<PanelTypes>, required: false },
|
|
||||||
clickAction: { type: Function as PropType<(index: number) => void>, required: false },
|
|
||||||
closeAction: { type: Function as PropType<(index: number) => void>, required: false },
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
newDocument() {
|
|
||||||
this.editor.instance.newDocumentDialog();
|
|
||||||
},
|
|
||||||
openDocument() {
|
|
||||||
this.editor.instance.documentOpen();
|
|
||||||
},
|
|
||||||
platformModifiers(reservedKey: boolean): LayoutKeysGroup {
|
|
||||||
// TODO: Remove this by properly feeding these keys from a layout provided by the backend
|
|
||||||
|
|
||||||
const ALT: Key = { key: "Alt", label: "Alt" };
|
|
||||||
const COMMAND: Key = { key: "Command", label: "Command" };
|
|
||||||
const CONTROL: Key = { key: "Control", label: "Ctrl" };
|
|
||||||
|
|
||||||
if (platformIsMac()) return reservedKey ? [ALT, COMMAND] : [COMMAND];
|
|
||||||
return reservedKey ? [CONTROL, ALT] : [CONTROL];
|
|
||||||
},
|
|
||||||
async scrollTabIntoView(newIndex: number) {
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
const panel: HTMLDivElement | undefined = this.$el;
|
|
||||||
if (!panel) return;
|
|
||||||
|
|
||||||
const newActiveTab = panel.querySelectorAll("[data-tab]")[newIndex] as HTMLDivElement | undefined;
|
|
||||||
newActiveTab?.scrollIntoView();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
IconLabel,
|
|
||||||
LayoutCol,
|
|
||||||
LayoutRow,
|
|
||||||
TextLabel,
|
|
||||||
UserInputLabel,
|
|
||||||
...PANEL_COMPONENTS,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,68 +1,3 @@
|
|||||||
<template>
|
|
||||||
<LayoutRow class="workspace" data-workspace>
|
|
||||||
<LayoutRow class="workspace-grid-subdivision" :style="{ 'flex-grow': panelSizes['root'] }" data-subdivision-name="root">
|
|
||||||
<LayoutCol class="workspace-grid-subdivision" :style="{ 'flex-grow': panelSizes['content'] }" data-subdivision-name="content">
|
|
||||||
<LayoutRow class="workspace-grid-subdivision" :style="{ 'flex-grow': panelSizes['document'] }" data-subdivision-name="document">
|
|
||||||
<Panel
|
|
||||||
:panelType="portfolio.state.documents.length > 0 ? 'Document' : undefined"
|
|
||||||
:tabCloseButtons="true"
|
|
||||||
:tabMinWidths="true"
|
|
||||||
:tabLabels="documentTabLabels"
|
|
||||||
: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="documentPanel"
|
|
||||||
/>
|
|
||||||
</LayoutRow>
|
|
||||||
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical @pointerdown="(e: PointerEvent) => resizePanel(e)" v-if="nodeGraphVisible"></LayoutRow>
|
|
||||||
<LayoutRow class="workspace-grid-subdivision" v-if="nodeGraphVisible" :style="{ 'flex-grow': panelSizes['graph'] }" data-subdivision-name="graph">
|
|
||||||
<Panel :panelType="'NodeGraph'" :tabLabels="[{ name: 'Node Graph' }]" :tabActiveIndex="0" />
|
|
||||||
</LayoutRow>
|
|
||||||
</LayoutCol>
|
|
||||||
<LayoutCol class="workspace-grid-resize-gutter" data-gutter-horizontal @pointerdown="(e: PointerEvent) => resizePanel(e)"></LayoutCol>
|
|
||||||
<LayoutCol class="workspace-grid-subdivision" :style="{ 'flex-grow': panelSizes['details'] }" data-subdivision-name="details">
|
|
||||||
<LayoutRow class="workspace-grid-subdivision" :style="{ 'flex-grow': panelSizes['properties'] }" data-subdivision-name="properties">
|
|
||||||
<Panel :panelType="'Properties'" :tabLabels="[{ name: 'Properties' }]" :tabActiveIndex="0" />
|
|
||||||
</LayoutRow>
|
|
||||||
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical @pointerdown="(e: PointerEvent) => resizePanel(e)"></LayoutRow>
|
|
||||||
<LayoutRow class="workspace-grid-subdivision" :style="{ 'flex-grow': panelSizes['layers'] }" data-subdivision-name="layers">
|
|
||||||
<Panel :panelType="'LayerTree'" :tabLabels="[{ name: 'Layer Tree' }]" :tabActiveIndex="0" />
|
|
||||||
</LayoutRow>
|
|
||||||
</LayoutCol>
|
|
||||||
</LayoutRow>
|
|
||||||
<DialogModal v-if="dialog.state.visible" />
|
|
||||||
</LayoutRow>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.workspace {
|
|
||||||
position: relative;
|
|
||||||
flex: 1 1 100%;
|
|
||||||
|
|
||||||
.workspace-grid-subdivision {
|
|
||||||
min-height: 28px;
|
|
||||||
flex: 1 1 0;
|
|
||||||
|
|
||||||
&.folded {
|
|
||||||
flex-grow: 0;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-grid-resize-gutter {
|
|
||||||
flex: 0 0 4px;
|
|
||||||
|
|
||||||
&.layout-row {
|
|
||||||
cursor: ns-resize;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.layout-col {
|
|
||||||
cursor: ew-resize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
@@ -176,3 +111,68 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<LayoutRow class="workspace" data-workspace>
|
||||||
|
<LayoutRow class="workspace-grid-subdivision" :style="{ 'flex-grow': panelSizes['root'] }" data-subdivision-name="root">
|
||||||
|
<LayoutCol class="workspace-grid-subdivision" :style="{ 'flex-grow': panelSizes['content'] }" data-subdivision-name="content">
|
||||||
|
<LayoutRow class="workspace-grid-subdivision" :style="{ 'flex-grow': panelSizes['document'] }" data-subdivision-name="document">
|
||||||
|
<Panel
|
||||||
|
:panelType="portfolio.state.documents.length > 0 ? 'Document' : undefined"
|
||||||
|
:tabCloseButtons="true"
|
||||||
|
:tabMinWidths="true"
|
||||||
|
:tabLabels="documentTabLabels"
|
||||||
|
: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="documentPanel"
|
||||||
|
/>
|
||||||
|
</LayoutRow>
|
||||||
|
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical @pointerdown="(e: PointerEvent) => resizePanel(e)" v-if="nodeGraphVisible"></LayoutRow>
|
||||||
|
<LayoutRow class="workspace-grid-subdivision" v-if="nodeGraphVisible" :style="{ 'flex-grow': panelSizes['graph'] }" data-subdivision-name="graph">
|
||||||
|
<Panel :panelType="'NodeGraph'" :tabLabels="[{ name: 'Node Graph' }]" :tabActiveIndex="0" />
|
||||||
|
</LayoutRow>
|
||||||
|
</LayoutCol>
|
||||||
|
<LayoutCol class="workspace-grid-resize-gutter" data-gutter-horizontal @pointerdown="(e: PointerEvent) => resizePanel(e)"></LayoutCol>
|
||||||
|
<LayoutCol class="workspace-grid-subdivision" :style="{ 'flex-grow': panelSizes['details'] }" data-subdivision-name="details">
|
||||||
|
<LayoutRow class="workspace-grid-subdivision" :style="{ 'flex-grow': panelSizes['properties'] }" data-subdivision-name="properties">
|
||||||
|
<Panel :panelType="'Properties'" :tabLabels="[{ name: 'Properties' }]" :tabActiveIndex="0" />
|
||||||
|
</LayoutRow>
|
||||||
|
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical @pointerdown="(e: PointerEvent) => resizePanel(e)"></LayoutRow>
|
||||||
|
<LayoutRow class="workspace-grid-subdivision" :style="{ 'flex-grow': panelSizes['layers'] }" data-subdivision-name="layers">
|
||||||
|
<Panel :panelType="'LayerTree'" :tabLabels="[{ name: 'Layer Tree' }]" :tabActiveIndex="0" />
|
||||||
|
</LayoutRow>
|
||||||
|
</LayoutCol>
|
||||||
|
</LayoutRow>
|
||||||
|
<DialogModal v-if="dialog.state.visible" />
|
||||||
|
</LayoutRow>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.workspace {
|
||||||
|
position: relative;
|
||||||
|
flex: 1 1 100%;
|
||||||
|
|
||||||
|
.workspace-grid-subdivision {
|
||||||
|
min-height: 28px;
|
||||||
|
flex: 1 1 0;
|
||||||
|
|
||||||
|
&.folded {
|
||||||
|
flex-grow: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-grid-resize-gutter {
|
||||||
|
flex: 0 0 4px;
|
||||||
|
|
||||||
|
&.layout-row {
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.layout-col {
|
||||||
|
cursor: ew-resize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user