mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Move JS script to the top of each Vue file
This commit is contained in:
35
.vscode/vuecomp.code-snippets
vendored
35
.vscode/vuecomp.code-snippets
vendored
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
<MainWindow />
|
||||
</template>
|
||||
@@ -232,104 +333,3 @@ img {
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<FloatingMenu class="color-picker" :open="open" @update:open="(isOpen) => emitOpenState(isOpen)" :strayCloses="strayCloses" :direction="direction" :type="'Popover'">
|
||||
<LayoutRow
|
||||
@@ -354,268 +619,3 @@
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<FloatingMenu :open="true" class="dialog-modal" :type="'Dialog'" :direction="'Center'" data-dialog-modal>
|
||||
<LayoutRow>
|
||||
@@ -65,37 +99,3 @@
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<FloatingMenu
|
||||
:open="true"
|
||||
@@ -79,61 +137,3 @@
|
||||
}
|
||||
}
|
||||
</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">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
@@ -357,3 +194,166 @@ const MenuList = defineComponent({
|
||||
});
|
||||
export default MenuList;
|
||||
</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">
|
||||
import { defineComponent, nextTick, type PropType } from "vue";
|
||||
|
||||
@@ -555,3 +372,186 @@ export default defineComponent({
|
||||
components: { LayoutCol },
|
||||
});
|
||||
</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>
|
||||
<div
|
||||
class="layout-col"
|
||||
@@ -21,15 +33,3 @@
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<div
|
||||
class="layout-row"
|
||||
@@ -21,15 +33,3 @@
|
||||
}
|
||||
}
|
||||
</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">
|
||||
import { defineComponent, nextTick } from "vue";
|
||||
|
||||
@@ -525,3 +299,229 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</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">
|
||||
import { defineComponent, nextTick } from "vue";
|
||||
|
||||
@@ -548,3 +279,272 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</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">
|
||||
import { defineComponent, nextTick } from "vue";
|
||||
|
||||
@@ -812,3 +477,338 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</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>
|
||||
<LayoutCol class="properties">
|
||||
<LayoutRow class="options-bar">
|
||||
@@ -32,37 +66,3 @@
|
||||
}
|
||||
}
|
||||
</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">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
@@ -42,3 +25,20 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</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: 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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
@@ -101,21 +120,3 @@
|
||||
}
|
||||
</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>
|
||||
<LayoutRow class="breadcrumb-trail-buttons" :title="tooltip">
|
||||
<TextButton
|
||||
@@ -57,25 +79,3 @@
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<button
|
||||
class="icon-button"
|
||||
@@ -78,26 +101,3 @@
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<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>
|
||||
@@ -39,21 +57,3 @@
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<LayoutRow class="popover-button">
|
||||
<IconButton :class="{ open }" :disabled="disabled" :action="() => onClick()" :icon="icon" :size="16" data-floating-menu-spawner :tooltip="tooltip" />
|
||||
@@ -50,41 +88,3 @@
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<button
|
||||
class="text-button"
|
||||
@@ -69,31 +97,3 @@
|
||||
}
|
||||
}
|
||||
</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 -->
|
||||
<template>
|
||||
<LayoutCol class="widget-section">
|
||||
@@ -120,46 +163,3 @@
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<LayoutRow class="checkbox-input">
|
||||
<input
|
||||
@@ -80,48 +125,3 @@
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<LayoutRow class="color-input" :class="{ 'sharp-right-corners': sharpRightCorners }" :title="tooltip">
|
||||
<button
|
||||
@@ -77,57 +131,3 @@
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<LayoutRow class="dropdown-input" data-dropdown-input>
|
||||
<LayoutRow
|
||||
@@ -95,80 +172,3 @@
|
||||
}
|
||||
}
|
||||
</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. -->
|
||||
<template>
|
||||
<LayoutRow class="field-input" :class="{ disabled, 'sharp-right-corners': sharpRightCorners }" :title="tooltip">
|
||||
@@ -131,64 +192,3 @@
|
||||
}
|
||||
}
|
||||
</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">
|
||||
import { defineComponent, nextTick, type PropType } from "vue";
|
||||
|
||||
@@ -187,3 +106,84 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</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>
|
||||
<LayoutRow
|
||||
class="layer-reference-input"
|
||||
@@ -89,73 +159,3 @@
|
||||
}
|
||||
}
|
||||
</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">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
@@ -152,3 +86,69 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</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>
|
||||
<FieldInput
|
||||
class="number-input"
|
||||
@@ -240,240 +477,3 @@
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<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" />
|
||||
@@ -24,26 +47,3 @@
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<LayoutRow class="radio-input" :class="{ disabled }">
|
||||
<button
|
||||
@@ -88,36 +121,3 @@
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<LayoutCol class="swatch-pair">
|
||||
<LayoutRow class="primary swatch">
|
||||
@@ -49,48 +94,3 @@
|
||||
}
|
||||
}
|
||||
</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">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
@@ -74,3 +55,22 @@ export default defineComponent({
|
||||
components: { FieldInput },
|
||||
});
|
||||
</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">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
@@ -101,3 +68,36 @@ export default defineComponent({
|
||||
components: { FieldInput },
|
||||
});
|
||||
</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>
|
||||
<LayoutRow :class="['icon-label', iconSizeClass, { disabled }]" :title="tooltip">
|
||||
<component :is="icon" />
|
||||
@@ -29,28 +54,3 @@
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<div class="separator" :class="[direction.toLowerCase(), type.toLowerCase()]">
|
||||
<div v-if="['Section', 'List'].includes(type)"></div>
|
||||
@@ -71,16 +84,3 @@
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<span class="text-label" :class="{ disabled, bold, italic, multiline, 'table-align': tableAlign }" :style="{ 'min-width': minWidth > 0 ? `${minWidth}px` : undefined }" :title="tooltip">
|
||||
<slot></slot>
|
||||
@@ -34,19 +50,3 @@
|
||||
}
|
||||
}
|
||||
</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">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
@@ -247,3 +123,127 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</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">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
@@ -146,3 +102,47 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</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">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
@@ -217,3 +109,111 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</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">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
@@ -41,3 +23,21 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</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>
|
||||
<LayoutRow class="status-bar">
|
||||
<LayoutRow class="hint-groups">
|
||||
@@ -44,39 +80,3 @@
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<LayoutRow class="title-bar">
|
||||
<LayoutRow class="header-part">
|
||||
@@ -36,40 +73,3 @@
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<LayoutRow class="window-buttons mac">
|
||||
<div class="close" title="Close"></div>
|
||||
@@ -37,16 +50,3 @@
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<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>
|
||||
@@ -29,31 +57,3 @@
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<LayoutRow class="window-button windows minimize" title="Minimize">
|
||||
<IconLabel :icon="'WindowButtonWinMinimize'" />
|
||||
@@ -36,20 +53,3 @@
|
||||
}
|
||||
}
|
||||
</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">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
|
||||
@@ -29,3 +14,18 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</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>
|
||||
<LayoutCol class="panel">
|
||||
<LayoutRow class="tab-bar" :class="{ 'min-widths': tabMinWidths }">
|
||||
@@ -207,83 +287,3 @@
|
||||
}
|
||||
}
|
||||
</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">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
@@ -176,3 +111,68 @@ export default defineComponent({
|
||||
},
|
||||
});
|
||||
</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