mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 21:37:59 +00:00
Merge d0c34e0ca9
into 24c6281644
This commit is contained in:
commit
98e5339500
73 changed files with 2358 additions and 1711 deletions
833
frontend/package-lock.json
generated
833
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -36,7 +36,7 @@
|
|||
"reflect-metadata": "^0.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.1.0",
|
||||
"@types/node": "^22.6.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.7.0",
|
||||
"@typescript-eslint/parser": "^8.7.0",
|
||||
|
@ -46,18 +46,19 @@
|
|||
"eslint-import-resolver-typescript": "^3.6.3",
|
||||
"eslint-plugin-import": "^2.30.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-svelte": "^2.44.0",
|
||||
"eslint-plugin-svelte": "^3.9.2",
|
||||
"minimatch": "^10.0.3",
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"process": "^0.11.10",
|
||||
"rollup-plugin-license": "^3.5.3",
|
||||
"sass": "1.78.0",
|
||||
"svelte": "^4.2.19",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-preprocess": "^6.0.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.6.2",
|
||||
"vite": "^5.4.14",
|
||||
"vite": "^6.3.5",
|
||||
"vite-multiple-assets": "1.3.1"
|
||||
},
|
||||
"//": "The dev dependency for `sass` can be removed once <https://github.com/sveltejs/svelte-preprocess/issues/656> is fixed, but meanwhile we have to",
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import Editor from "@graphite/components/Editor.svelte";
|
||||
|
||||
let editor: GraphiteEditor | undefined = undefined;
|
||||
let editor: GraphiteEditor | undefined = $state(undefined);
|
||||
|
||||
onMount(async () => {
|
||||
await initWasm();
|
||||
|
|
11
frontend/src/app.d.ts
vendored
Normal file
11
frontend/src/app.d.ts
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
declare global {
|
||||
namespace Graphite {
|
||||
type Platform = "Windows" | "Mac" | "Linux" | "Web";
|
||||
type MenuType = "Popover" | "Dropdown" | "Dialog" | "Cursor";
|
||||
type Axis = "Horizontal" | "Vertical";
|
||||
|
||||
// interface Error {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
|
@ -10,7 +10,7 @@
|
|||
import { createPanicManager } from "@graphite/io-managers/panic";
|
||||
import { createPersistenceManager } from "@graphite/io-managers/persistence";
|
||||
import { createDialogState } from "@graphite/state-providers/dialog";
|
||||
import { createDocumentState } from "@graphite/state-providers/document";
|
||||
import { createDocumentState } from "@graphite/state-providers/document.svelte";
|
||||
import { createFontsState } from "@graphite/state-providers/fonts";
|
||||
import { createFullscreenState } from "@graphite/state-providers/fullscreen";
|
||||
import { createNodeGraphState } from "@graphite/state-providers/node-graph";
|
||||
|
@ -19,8 +19,13 @@
|
|||
|
||||
import MainWindow from "@graphite/components/window/MainWindow.svelte";
|
||||
|
||||
// Graphite WASM editor
|
||||
export let editor: Editor;
|
||||
|
||||
interface Props {
|
||||
// Graphite WASM editor
|
||||
editor: Editor;
|
||||
}
|
||||
|
||||
let { editor }: Props = $props();
|
||||
setContext("editor", editor);
|
||||
|
||||
// State provider systems
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { onDestroy, createEventDispatcher, getContext } from "svelte";
|
||||
import { onDestroy, getContext } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { HSV, RGB, FillChoice } from "@graphite/messages";
|
||||
import type { MenuDirection } from "@graphite/messages";
|
||||
import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages";
|
||||
import type { HSV, RGB, FillChoice } from "@graphite/messages.svelte";
|
||||
import type { MenuDirection } from "@graphite/messages.svelte";
|
||||
import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages.svelte";
|
||||
import { clamp } from "@graphite/utility-functions/math";
|
||||
|
||||
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
|
@ -33,43 +33,48 @@
|
|||
};
|
||||
|
||||
const editor = getContext<Editor>("editor");
|
||||
interface Props {
|
||||
colorOrGradient: FillChoice;
|
||||
allowNone?: boolean;
|
||||
// export let allowTransparency = false; // TODO: Implement
|
||||
direction?: MenuDirection;
|
||||
// TODO: See if this should be made to follow the pattern of DropdownInput.svelte so this could be removed
|
||||
open: boolean;
|
||||
oncolorOrGradient?: (colorOrGradient: FillChoice) => void;
|
||||
onstartHistoryTransaction?: () => void;
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher<{ colorOrGradient: FillChoice; startHistoryTransaction: undefined }>();
|
||||
|
||||
export let colorOrGradient: FillChoice;
|
||||
export let allowNone = false;
|
||||
// export let allowTransparency = false; // TODO: Implement
|
||||
export let direction: MenuDirection = "Bottom";
|
||||
// TODO: See if this should be made to follow the pattern of DropdownInput.svelte so this could be removed
|
||||
export let open: boolean;
|
||||
let {
|
||||
colorOrGradient,
|
||||
allowNone = false,
|
||||
direction = "Bottom",
|
||||
open = $bindable(),
|
||||
oncolorOrGradient,
|
||||
onstartHistoryTransaction,
|
||||
}: Props = $props();
|
||||
|
||||
const hsvaOrNone = colorOrGradient instanceof Color ? colorOrGradient.toHSVA() : colorOrGradient.firstColor()?.toHSVA();
|
||||
const hsva = hsvaOrNone || { h: 0, s: 0, v: 0, a: 1 };
|
||||
const hsva = hsvaOrNone ?? { h: 0, s: 0, v: 0, a: 1 };
|
||||
|
||||
// Gradient color stops
|
||||
$: gradient = colorOrGradient instanceof Gradient ? colorOrGradient : undefined;
|
||||
let activeIndex = 0 as number | undefined;
|
||||
$: selectedGradientColor = (activeIndex !== undefined && gradient?.atIndex(activeIndex)?.color) || (Color.fromCSS("black") as Color);
|
||||
// Currently viewed color
|
||||
$: color = colorOrGradient instanceof Color ? colorOrGradient : selectedGradientColor;
|
||||
let activeIndex = $state<number | undefined>(0);
|
||||
// New color components
|
||||
let hue = hsva.h;
|
||||
let saturation = hsva.s;
|
||||
let value = hsva.v;
|
||||
let alpha = hsva.a;
|
||||
let isNone = hsvaOrNone === undefined;
|
||||
let hue = $state(hsva.h);
|
||||
let saturation = $state(hsva.s);
|
||||
let value = $state(hsva.v);
|
||||
let alpha = $state(hsva.a);
|
||||
let isNone = $state(hsvaOrNone === undefined);
|
||||
// Old color components
|
||||
let oldHue = hsva.h;
|
||||
let oldSaturation = hsva.s;
|
||||
let oldValue = hsva.v;
|
||||
let oldAlpha = hsva.a;
|
||||
let oldIsNone = hsvaOrNone === undefined;
|
||||
let oldHue = $state(hsva.h);
|
||||
let oldSaturation = $state(hsva.s);
|
||||
let oldValue = $state(hsva.v);
|
||||
let oldAlpha = $state(hsva.a);
|
||||
let oldIsNone = $state(hsvaOrNone === undefined);
|
||||
// Transient state
|
||||
let draggingPickerTrack: HTMLDivElement | undefined = undefined;
|
||||
let strayCloses = true;
|
||||
let gradientSpectrumDragging = false;
|
||||
let strayCloses = $state(true);
|
||||
let gradientSpectrumDragging = $state(false);
|
||||
let shiftPressed = false;
|
||||
let alignedAxis: "saturation" | "value" | undefined = undefined;
|
||||
let alignedAxis: "saturation" | "value" | undefined = $state(undefined);
|
||||
let hueBeforeDrag = 0;
|
||||
let saturationBeforeDrag = 0;
|
||||
let valueBeforeDrag = 0;
|
||||
|
@ -79,21 +84,9 @@
|
|||
let saturationRestoreWhenShiftReleased: number | undefined = undefined;
|
||||
let valueRestoreWhenShiftReleased: number | undefined = undefined;
|
||||
|
||||
let self: FloatingMenu | undefined;
|
||||
let hexCodeInputWidget: TextInput | undefined;
|
||||
let gradientSpectrumInputWidget: SpectrumInput | undefined;
|
||||
|
||||
$: watchOpen(open);
|
||||
$: watchColor(color);
|
||||
|
||||
$: oldColor = generateColor(oldHue, oldSaturation, oldValue, oldAlpha, oldIsNone);
|
||||
$: newColor = generateColor(hue, saturation, value, alpha, isNone);
|
||||
$: rgbChannels = Object.entries(newColor.toRgb255() || { r: undefined, g: undefined, b: undefined }) as [keyof RGB, number | undefined][];
|
||||
$: hsvChannels = Object.entries(!isNone ? { h: hue * 360, s: saturation * 100, v: value * 100 } : { h: undefined, s: undefined, v: undefined }) as [keyof HSV, number | undefined][];
|
||||
$: opaqueHueColor = new Color({ h: hue, s: 1, v: 1, a: 1 });
|
||||
$: outlineFactor = Math.max(contrastingOutlineFactor(newColor, "--color-2-mildblack", 0.01), contrastingOutlineFactor(oldColor, "--color-2-mildblack", 0.01));
|
||||
$: outlined = outlineFactor > 0.0001;
|
||||
$: transparency = newColor.alpha < 1 || oldColor.alpha < 1;
|
||||
let self: FloatingMenu | undefined = $state();
|
||||
let hexCodeInputWidget: TextInput | undefined = $state();
|
||||
let gradientSpectrumInputWidget: SpectrumInput | undefined = $state();
|
||||
|
||||
function generateColor(h: number, s: number, v: number, a: number, none: boolean) {
|
||||
if (none) return new Color("none");
|
||||
|
@ -174,6 +167,7 @@
|
|||
}
|
||||
|
||||
const color = new Color({ h: hue, s: saturation, v: value, a: alpha });
|
||||
|
||||
setColor(color);
|
||||
|
||||
if (!e.shiftKey) {
|
||||
|
@ -226,7 +220,7 @@
|
|||
document.addEventListener("keydown", onKeyDown);
|
||||
document.addEventListener("keyup", onKeyUp);
|
||||
|
||||
dispatch("startHistoryTransaction");
|
||||
onstartHistoryTransaction?.();
|
||||
}
|
||||
|
||||
function removeEvents() {
|
||||
|
@ -274,15 +268,14 @@
|
|||
}
|
||||
|
||||
function setColor(color?: Color) {
|
||||
const colorToEmit = color || new Color({ h: hue, s: saturation, v: value, a: alpha });
|
||||
const colorToEmit = color ?? new Color({ h: hue, s: saturation, v: value, a: alpha });
|
||||
|
||||
const stop = gradientSpectrumInputWidget && activeIndex !== undefined && gradient?.atIndex(activeIndex);
|
||||
if (stop && gradientSpectrumInputWidget instanceof SpectrumInput) {
|
||||
if (stop && gradientSpectrumInputWidget !== undefined) {
|
||||
stop.color = colorToEmit;
|
||||
gradient = gradient;
|
||||
}
|
||||
|
||||
dispatch("colorOrGradient", gradient || colorToEmit);
|
||||
oncolorOrGradient?.(gradient ?? colorToEmit);
|
||||
}
|
||||
|
||||
function swapNewWithOld() {
|
||||
|
@ -338,7 +331,7 @@
|
|||
}
|
||||
|
||||
function setColorPreset(preset: PresetColors) {
|
||||
dispatch("startHistoryTransaction");
|
||||
onstartHistoryTransaction?.()
|
||||
if (preset === "none") {
|
||||
setNewHSVA(0, 0, 0, 1, true);
|
||||
setColor(new Color("none"));
|
||||
|
@ -379,14 +372,14 @@
|
|||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result = await new (window as any).EyeDropper().open();
|
||||
dispatch("startHistoryTransaction");
|
||||
onstartHistoryTransaction?.();
|
||||
setColorCode(result.sRGBHex);
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
function gradientActiveMarkerIndexChange({ detail: index }: CustomEvent<number | undefined>) {
|
||||
function gradientActiveMarkerIndexChange(index: number | undefined) {
|
||||
activeIndex = index;
|
||||
const color = index === undefined ? undefined : gradient?.colorAtIndex(index);
|
||||
const hsva = color?.toHSVA();
|
||||
|
@ -401,9 +394,34 @@
|
|||
onDestroy(() => {
|
||||
removeEvents();
|
||||
});
|
||||
// Gradient color stops
|
||||
let gradient = $state<Gradient | undefined>(undefined);
|
||||
let selectedGradientColor = $derived((activeIndex !== undefined && gradient?.atIndex(activeIndex)?.color) || (Color.fromCSS("black") as Color));
|
||||
// Currently viewed color
|
||||
let color = $derived(colorOrGradient instanceof Color ? colorOrGradient : selectedGradientColor);
|
||||
$effect(() => {
|
||||
watchOpen(open);
|
||||
});
|
||||
$effect(() => {
|
||||
watchColor(color);
|
||||
});
|
||||
// gradient changed from outside
|
||||
$effect(() => {
|
||||
if (colorOrGradient instanceof Gradient) {
|
||||
gradient = colorOrGradient;
|
||||
}
|
||||
})
|
||||
let oldColor = $derived(generateColor(oldHue, oldSaturation, oldValue, oldAlpha, oldIsNone));
|
||||
let newColor = $derived(generateColor(hue, saturation, value, alpha, isNone));
|
||||
let rgbChannels = $derived(Object.entries(newColor.toRgb255() || { r: undefined, g: undefined, b: undefined }) as [keyof RGB, number | undefined][]);
|
||||
let hsvChannels = $derived(Object.entries(!isNone ? { h: hue * 360, s: saturation * 100, v: value * 100 } : { h: undefined, s: undefined, v: undefined }) as [keyof HSV, number | undefined][]);
|
||||
let opaqueHueColor = $derived(new Color({ h: hue, s: 1, v: 1, a: 1 }));
|
||||
let outlineFactor = $derived(Math.max(contrastingOutlineFactor(newColor, "--color-2-mildblack", 0.01), contrastingOutlineFactor(oldColor, "--color-2-mildblack", 0.01)));
|
||||
let outlined = $derived(outlineFactor > 0.0001);
|
||||
let transparency = $derived(newColor.alpha < 1 || oldColor.alpha < 1);
|
||||
</script>
|
||||
|
||||
<FloatingMenu class="color-picker" {open} on:open {strayCloses} escapeCloses={strayCloses && !gradientSpectrumDragging} {direction} type="Popover" bind:this={self}>
|
||||
<FloatingMenu class="color-picker" bind:open={open} {strayCloses} escapeCloses={strayCloses && !gradientSpectrumDragging} {direction} type="Popover" bind:this={self}>
|
||||
<LayoutRow
|
||||
styles={{
|
||||
"--new-color": newColor.toHexOptionalAlpha(),
|
||||
|
@ -418,9 +436,9 @@
|
|||
>
|
||||
<LayoutCol class="pickers-and-gradient">
|
||||
<LayoutRow class="pickers">
|
||||
<LayoutCol class="saturation-value-picker" on:pointerdown={onPointerDown} data-saturation-value-picker>
|
||||
<LayoutCol class="saturation-value-picker" onpointerdown={onPointerDown} data-saturation-value-picker>
|
||||
{#if !isNone}
|
||||
<div class="selection-circle" style:top={`${(1 - value) * 100}%`} style:left={`${saturation * 100}%`} />
|
||||
<div class="selection-circle" style:top={`${(1 - value) * 100}%`} style:left={`${saturation * 100}%`}></div>
|
||||
{/if}
|
||||
{#if alignedAxis}
|
||||
<div
|
||||
|
@ -429,37 +447,37 @@
|
|||
class:value={alignedAxis === "value"}
|
||||
style:top={`${(1 - value) * 100}%`}
|
||||
style:left={`${saturation * 100}%`}
|
||||
/>
|
||||
></div>
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
<LayoutCol class="hue-picker" on:pointerdown={onPointerDown} data-hue-picker>
|
||||
<LayoutCol class="hue-picker" onpointerdown={onPointerDown} data-hue-picker>
|
||||
{#if !isNone}
|
||||
<div class="selection-needle" style:top={`${(1 - hue) * 100}%`} />
|
||||
<div class="selection-needle" style:top={`${(1 - hue) * 100}%`}></div>
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
<LayoutCol class="alpha-picker" on:pointerdown={onPointerDown} data-alpha-picker>
|
||||
<LayoutCol class="alpha-picker" onpointerdown={onPointerDown} data-alpha-picker>
|
||||
{#if !isNone}
|
||||
<div class="selection-needle" style:top={`${(1 - alpha) * 100}%`} />
|
||||
<div class="selection-needle" style:top={`${(1 - alpha) * 100}%`}></div>
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
</LayoutRow>
|
||||
{#if gradient}
|
||||
<LayoutRow class="gradient">
|
||||
<SpectrumInput
|
||||
{gradient}
|
||||
on:gradient={() => {
|
||||
gradient = gradient;
|
||||
if (gradient) dispatch("colorOrGradient", gradient);
|
||||
gradient={gradient}
|
||||
ongradient={(detail) => {
|
||||
gradient = detail;
|
||||
if (gradient) oncolorOrGradient?.(gradient);
|
||||
}}
|
||||
on:activeMarkerIndexChange={gradientActiveMarkerIndexChange}
|
||||
onactiveMarkerIndexChange={gradientActiveMarkerIndexChange}
|
||||
activeMarkerIndex={activeIndex}
|
||||
on:dragging={({ detail }) => (gradientSpectrumDragging = detail)}
|
||||
bind:drag={gradientSpectrumDragging}
|
||||
bind:this={gradientSpectrumInputWidget}
|
||||
/>
|
||||
{#if gradientSpectrumInputWidget && activeIndex !== undefined}
|
||||
<NumberInput
|
||||
value={(gradient.positionAtIndex(activeIndex) || 0) * 100}
|
||||
on:value={({ detail }) => {
|
||||
onvalue={(detail) => {
|
||||
if (gradientSpectrumInputWidget && activeIndex !== undefined && detail !== undefined) gradientSpectrumInputWidget.setPosition(activeIndex, detail / 100);
|
||||
}}
|
||||
displayDecimalPlaces={0}
|
||||
|
@ -480,7 +498,7 @@
|
|||
>
|
||||
{#if !newColor.equals(oldColor)}
|
||||
<div class="swap-button-background"></div>
|
||||
<IconButton class="swap-button" icon="SwapHorizontal" size={16} action={swapNewWithOld} tooltip="Swap" />
|
||||
<IconButton class="swap-button" icon="SwapHorizontal" size={16} onclick={swapNewWithOld} tooltip="Swap" />
|
||||
{/if}
|
||||
<LayoutCol class="new-color" classes={{ none: isNone }}>
|
||||
{#if !newColor.equals(oldColor)}
|
||||
|
@ -499,9 +517,9 @@
|
|||
<Separator type="Related" />
|
||||
<LayoutRow>
|
||||
<TextInput
|
||||
value={newColor.toHexOptionalAlpha() || "-"}
|
||||
on:commitText={({ detail }) => {
|
||||
dispatch("startHistoryTransaction");
|
||||
value={newColor.toHexOptionalAlpha() ?? "-"}
|
||||
oncommitText={(detail ) => {
|
||||
onstartHistoryTransaction?.();
|
||||
setColorCode(detail);
|
||||
}}
|
||||
centered={true}
|
||||
|
@ -519,14 +537,12 @@
|
|||
<Separator type="Related" />
|
||||
{/if}
|
||||
<NumberInput
|
||||
value={strength}
|
||||
on:value={({ detail }) => {
|
||||
strength = detail;
|
||||
value={rgbChannels[index][1]}
|
||||
onvalue={(detail) => {
|
||||
rgbChannels[index][1] = detail;
|
||||
setColorRGB(channel, detail);
|
||||
}}
|
||||
on:startHistoryTransaction={() => {
|
||||
dispatch("startHistoryTransaction");
|
||||
}}
|
||||
{onstartHistoryTransaction}
|
||||
min={0}
|
||||
max={255}
|
||||
minWidth={1}
|
||||
|
@ -546,14 +562,12 @@
|
|||
<Separator type="Related" />
|
||||
{/if}
|
||||
<NumberInput
|
||||
value={strength}
|
||||
on:value={({ detail }) => {
|
||||
strength = detail;
|
||||
value={hsvChannels[index][1]}
|
||||
onvalue={(detail) => {
|
||||
hsvChannels[index][1] = detail;
|
||||
setColorHSV(channel, detail);
|
||||
}}
|
||||
on:startHistoryTransaction={() => {
|
||||
dispatch("startHistoryTransaction");
|
||||
}}
|
||||
{onstartHistoryTransaction}
|
||||
min={0}
|
||||
max={channel === "h" ? 360 : 100}
|
||||
unit={channel === "h" ? "°" : "%"}
|
||||
|
@ -573,13 +587,11 @@
|
|||
<Separator type="Related" />
|
||||
<NumberInput
|
||||
value={!isNone ? alpha * 100 : undefined}
|
||||
on:value={({ detail }) => {
|
||||
onvalue={(detail) => {
|
||||
if (detail !== undefined) alpha = detail / 100;
|
||||
setColorAlphaPercent(detail);
|
||||
}}
|
||||
on:startHistoryTransaction={() => {
|
||||
dispatch("startHistoryTransaction");
|
||||
}}
|
||||
{onstartHistoryTransaction}
|
||||
min={0}
|
||||
max={100}
|
||||
rangeMin={0}
|
||||
|
@ -593,23 +605,23 @@
|
|||
<LayoutRow class="leftover-space" />
|
||||
<LayoutRow>
|
||||
{#if allowNone && !gradient}
|
||||
<button class="preset-color none" on:click={() => setColorPreset("none")} title="Set to no color" tabindex="0"></button>
|
||||
<button class="preset-color none" onclick={() => setColorPreset("none")} title="Set to no color" tabindex="0"></button>
|
||||
<Separator type="Related" />
|
||||
{/if}
|
||||
<button class="preset-color black" on:click={() => setColorPreset("black")} title="Set to black" tabindex="0"></button>
|
||||
<button class="preset-color black" onclick={() => setColorPreset("black")} title="Set to black" tabindex="0"></button>
|
||||
<Separator type="Related" />
|
||||
<button class="preset-color white" on:click={() => setColorPreset("white")} title="Set to white" tabindex="0"></button>
|
||||
<button class="preset-color white" onclick={() => setColorPreset("white")} title="Set to white" tabindex="0"></button>
|
||||
<Separator type="Related" />
|
||||
<button class="preset-color pure" on:click={setColorPresetSubtile} tabindex="-1">
|
||||
<div data-pure-tile="red" style="--pure-color: #ff0000; --pure-color-gray: #4c4c4c" title="Set to red" />
|
||||
<div data-pure-tile="yellow" style="--pure-color: #ffff00; --pure-color-gray: #e3e3e3" title="Set to yellow" />
|
||||
<div data-pure-tile="green" style="--pure-color: #00ff00; --pure-color-gray: #969696" title="Set to green" />
|
||||
<div data-pure-tile="cyan" style="--pure-color: #00ffff; --pure-color-gray: #b2b2b2" title="Set to cyan" />
|
||||
<div data-pure-tile="blue" style="--pure-color: #0000ff; --pure-color-gray: #1c1c1c" title="Set to blue" />
|
||||
<div data-pure-tile="magenta" style="--pure-color: #ff00ff; --pure-color-gray: #696969" title="Set to magenta" />
|
||||
<button class="preset-color pure" onclick={setColorPresetSubtile} tabindex="-1">
|
||||
<div data-pure-tile="red" style="--pure-color: #ff0000; --pure-color-gray: #4c4c4c" title="Set to red"></div>
|
||||
<div data-pure-tile="yellow" style="--pure-color: #ffff00; --pure-color-gray: #e3e3e3" title="Set to yellow"></div>
|
||||
<div data-pure-tile="green" style="--pure-color: #00ff00; --pure-color-gray: #969696" title="Set to green"></div>
|
||||
<div data-pure-tile="cyan" style="--pure-color: #00ffff; --pure-color-gray: #b2b2b2" title="Set to cyan"></div>
|
||||
<div data-pure-tile="blue" style="--pure-color: #0000ff; --pure-color-gray: #1c1c1c" title="Set to blue"></div>
|
||||
<div data-pure-tile="magenta" style="--pure-color: #ff00ff; --pure-color-gray: #696969" title="Set to magenta"></div>
|
||||
</button>
|
||||
<Separator type="Related" />
|
||||
<IconButton icon="Eyedropper" size={24} action={activateEyedropperSample} tooltip="Sample a pixel color from the document" />
|
||||
<IconButton icon="Eyedropper" size={24} onclick={activateEyedropperSample} tooltip="Sample a pixel color from the document" />
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
</LayoutRow>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
const dialog = getContext<DialogState>("dialog");
|
||||
|
||||
let self: FloatingMenu | undefined;
|
||||
let self: FloatingMenu | undefined = $state();
|
||||
|
||||
onMount(() => {
|
||||
// Focus the button which is marked as emphasized, or otherwise the first button, in the popup
|
||||
|
@ -41,14 +41,14 @@
|
|||
<div class="widget-layout details">
|
||||
<div class="widget-span row"><TextLabel bold={true}>The editor crashed — sorry about that</TextLabel></div>
|
||||
<div class="widget-span row"><TextLabel>Please report this by filing an issue on GitHub:</TextLabel></div>
|
||||
<div class="widget-span row"><TextButton label="Report Bug" icon="Warning" flush={true} action={() => window.open(githubUrl($dialog.panicDetails), "_blank")} /></div>
|
||||
<div class="widget-span row"><TextButton label="Report Bug" icon="Warning" flush={true} onclick={() => window.open(githubUrl($dialog.panicDetails), "_blank")} /></div>
|
||||
<div class="widget-span row"><TextLabel multiline={true}>Reload the editor to continue. If this occurs<br />immediately on repeated reloads, clear storage:</TextLabel></div>
|
||||
<div class="widget-span row">
|
||||
<TextButton
|
||||
label="Clear Saved Documents"
|
||||
icon="Trash"
|
||||
flush={true}
|
||||
action={async () => {
|
||||
onclick={async () => {
|
||||
await wipeDocuments();
|
||||
window.location.reload();
|
||||
}}
|
||||
|
@ -68,8 +68,8 @@
|
|||
<WidgetLayout layout={$dialog.buttons} class="details" />
|
||||
{/if}
|
||||
{#if $dialog.panicDetails}
|
||||
<TextButton label="Copy Error Log" action={() => navigator.clipboard.writeText($dialog.panicDetails)} />
|
||||
<TextButton label="Reload" emphasized={true} action={() => window.location.reload()} />
|
||||
<TextButton label="Copy Error Log" onclick={() => navigator.clipboard.writeText($dialog.panicDetails)} />
|
||||
<TextButton label="Reload" emphasized={true} onclick={() => window.location.reload()} />
|
||||
{/if}
|
||||
</LayoutRow>
|
||||
</FloatingMenu>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<script lang="ts" context="module">
|
||||
<script lang="ts" module>
|
||||
// Should be equal to the width and height of the zoom preview canvas in the CSS
|
||||
const ZOOM_WINDOW_DIMENSIONS_EXPANDED = 110;
|
||||
// Should be equal to the width and height of the `.pixel-outline` div in the CSS, and should be evenly divisible into the number above
|
||||
|
@ -8,24 +8,32 @@
|
|||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
|
||||
const temporaryCanvas = document.createElement("canvas");
|
||||
temporaryCanvas.width = ZOOM_WINDOW_DIMENSIONS;
|
||||
temporaryCanvas.height = ZOOM_WINDOW_DIMENSIONS;
|
||||
|
||||
let zoomPreviewCanvas: HTMLCanvasElement | undefined;
|
||||
let zoomPreviewCanvas: HTMLCanvasElement | undefined = $state();
|
||||
|
||||
export let imageData: ImageData | undefined = undefined;
|
||||
export let colorChoice: string;
|
||||
export let primaryColor: string;
|
||||
export let secondaryColor: string;
|
||||
export let x: number;
|
||||
export let y: number;
|
||||
interface Props {
|
||||
imageData?: ImageData | undefined;
|
||||
colorChoice: string;
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
let {
|
||||
imageData = undefined,
|
||||
colorChoice,
|
||||
primaryColor,
|
||||
secondaryColor,
|
||||
x,
|
||||
y
|
||||
}: Props = $props();
|
||||
|
||||
$: displayImageDataPreview(imageData);
|
||||
|
||||
function displayImageDataPreview(imageData: ImageData | undefined) {
|
||||
if (!zoomPreviewCanvas) return;
|
||||
|
@ -43,7 +51,7 @@
|
|||
context.drawImage(temporaryCanvas, 0, 0);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
$effect(() => {
|
||||
displayImageDataPreview(imageData);
|
||||
});
|
||||
</script>
|
||||
|
@ -56,8 +64,8 @@
|
|||
>
|
||||
<div class="ring">
|
||||
<div class="canvas-container">
|
||||
<canvas width={ZOOM_WINDOW_DIMENSIONS} height={ZOOM_WINDOW_DIMENSIONS} bind:this={zoomPreviewCanvas} />
|
||||
<div class="pixel-outline" />
|
||||
<canvas width={ZOOM_WINDOW_DIMENSIONS} height={ZOOM_WINDOW_DIMENSIONS} bind:this={zoomPreviewCanvas}></canvas>
|
||||
<div class="pixel-outline"></div>
|
||||
</div>
|
||||
</div>
|
||||
</FloatingMenu>
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
<svelte:options accessors={true} />
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, tick, onDestroy, onMount } from "svelte";
|
||||
import { tick, onDestroy, onMount } from "svelte";
|
||||
|
||||
import type { MenuListEntry, MenuDirection } from "@graphite/messages";
|
||||
import type { MenuListEntry, MenuDirection } from "@graphite/messages.svelte";
|
||||
|
||||
import MenuList from "@graphite/components/floating-menus/MenuList.svelte";
|
||||
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
|
@ -15,41 +13,51 @@
|
|||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
import UserInputLabel from "@graphite/components/widgets/labels/UserInputLabel.svelte";
|
||||
|
||||
let self: FloatingMenu | undefined;
|
||||
let scroller: LayoutCol | undefined;
|
||||
let searchTextInput: TextInput | undefined;
|
||||
let self: FloatingMenu | undefined = $state();
|
||||
let scroller: LayoutCol | undefined = $state();
|
||||
let searchTextInput: TextInput | undefined = $state();
|
||||
|
||||
const dispatch = createEventDispatcher<{ open: boolean; activeEntry: MenuListEntry; hoverInEntry: MenuListEntry; hoverOutEntry: undefined; naturalWidth: number }>();
|
||||
interface Props {
|
||||
entries: MenuListEntry[][];
|
||||
activeEntry?: MenuListEntry | undefined;
|
||||
open: boolean;
|
||||
direction?: MenuDirection;
|
||||
minWidth?: number;
|
||||
drawIcon?: boolean;
|
||||
interactive?: boolean;
|
||||
scrollableY?: boolean;
|
||||
virtualScrollingEntryHeight?: number;
|
||||
tooltip?: string | undefined;
|
||||
onhoverOutEntry?: () => void;
|
||||
onhoverInEntry?: (entry: MenuListEntry) => void;
|
||||
onnaturalWidth?: (width: number) => void;
|
||||
onactiveEntry?: (activeEntry: MenuListEntry) => void;
|
||||
}
|
||||
|
||||
export let entries: MenuListEntry[][];
|
||||
export let activeEntry: MenuListEntry | undefined = undefined;
|
||||
export let open: boolean;
|
||||
export let direction: MenuDirection = "Bottom";
|
||||
export let minWidth = 0;
|
||||
export let drawIcon = false;
|
||||
export let interactive = false;
|
||||
export let scrollableY = false;
|
||||
export let virtualScrollingEntryHeight = 0;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
let {
|
||||
entries = $bindable(),
|
||||
activeEntry = undefined,
|
||||
open = $bindable(false),
|
||||
direction = "Bottom",
|
||||
minWidth = 0,
|
||||
drawIcon = false,
|
||||
interactive = false,
|
||||
scrollableY = false,
|
||||
virtualScrollingEntryHeight = 0,
|
||||
tooltip = undefined,
|
||||
onhoverOutEntry,
|
||||
onhoverInEntry,
|
||||
onnaturalWidth,
|
||||
onactiveEntry,
|
||||
}: Props = $props();
|
||||
|
||||
// Keep the child references outside of the entries array so as to avoid infinite recursion.
|
||||
let childReferences: MenuList[][] = [];
|
||||
let search = "";
|
||||
let childReferences: MenuList[][] = $state([]);
|
||||
let search = $state("");
|
||||
|
||||
let highlighted = activeEntry as MenuListEntry | undefined;
|
||||
let virtualScrollingEntriesStart = 0;
|
||||
let highlighted = $state(activeEntry as MenuListEntry | undefined);
|
||||
let virtualScrollingEntriesStart = $state(0);
|
||||
|
||||
// Called only when `open` is changed from outside this component
|
||||
$: watchOpen(open);
|
||||
$: watchEntries(entries);
|
||||
$: watchRemeasureWidth(filteredEntries, drawIcon);
|
||||
$: watchHighlightedWithSearch(filteredEntries, open);
|
||||
|
||||
$: filteredEntries = entries.map((section) => section.filter((entry) => inSearch(search, entry)));
|
||||
$: virtualScrollingTotalHeight = filteredEntries.length === 0 ? 0 : filteredEntries[0].length * virtualScrollingEntryHeight;
|
||||
$: virtualScrollingStartIndex = Math.floor(virtualScrollingEntriesStart / virtualScrollingEntryHeight) || 0;
|
||||
$: virtualScrollingEndIndex = filteredEntries.length === 0 ? 0 : Math.min(filteredEntries[0].length, virtualScrollingStartIndex + 1 + 400 / virtualScrollingEntryHeight);
|
||||
$: startIndex = virtualScrollingEntryHeight ? virtualScrollingStartIndex : 0;
|
||||
|
||||
// TODO: Move keyboard input handling entirely to the unified system in `input.ts`.
|
||||
// TODO: The current approach is hacky and blocks the allowances for shortcuts like the key to open the browser's dev tools.
|
||||
|
@ -116,12 +124,13 @@
|
|||
return !search || entry.label.toLowerCase().includes(search.toLowerCase());
|
||||
}
|
||||
|
||||
function watchOpen(open: boolean) {
|
||||
function watchOpen(value: boolean) {
|
||||
if (open && !inNestedMenuList()) addEventListener("keydown", keydown);
|
||||
else if (!inNestedMenuList()) removeEventListener("keydown", keydown);
|
||||
|
||||
highlighted = activeEntry;
|
||||
dispatch("open", open);
|
||||
// dispatch("open", value);
|
||||
// open = value;
|
||||
|
||||
search = "";
|
||||
}
|
||||
|
@ -151,42 +160,43 @@
|
|||
if (menuListEntry.action) menuListEntry.action();
|
||||
|
||||
// Notify the parent about the clicked entry as the new active entry
|
||||
dispatch("activeEntry", menuListEntry);
|
||||
onactiveEntry?.(menuListEntry);
|
||||
|
||||
// Close the containing menu
|
||||
let childReference = getChildReference(menuListEntry);
|
||||
if (childReference) {
|
||||
childReference.open = false;
|
||||
entries = entries;
|
||||
// entries = entries;
|
||||
}
|
||||
dispatch("open", false);
|
||||
// dispatch("open", false);
|
||||
open = false;
|
||||
}
|
||||
|
||||
function onEntryPointerEnter(menuListEntry: MenuListEntry) {
|
||||
if (!menuListEntry.children?.length) {
|
||||
dispatch("hoverInEntry", menuListEntry);
|
||||
onhoverInEntry?.(menuListEntry);
|
||||
return;
|
||||
}
|
||||
|
||||
let childReference = getChildReference(menuListEntry);
|
||||
if (childReference) {
|
||||
childReference.open = true;
|
||||
entries = entries;
|
||||
} else dispatch("open", true);
|
||||
// entries = entries;
|
||||
} else open = true;
|
||||
}
|
||||
|
||||
function onEntryPointerLeave(menuListEntry: MenuListEntry) {
|
||||
if (!menuListEntry.children?.length) {
|
||||
dispatch("hoverOutEntry");
|
||||
// dispatch("hoverOutEntry");
|
||||
onhoverOutEntry?.();
|
||||
return;
|
||||
}
|
||||
|
||||
let childReference = getChildReference(menuListEntry);
|
||||
if (childReference) {
|
||||
childReference.open = false;
|
||||
entries = entries;
|
||||
} else dispatch("open", false);
|
||||
// entries = entries;
|
||||
} else open = false;
|
||||
}
|
||||
|
||||
function isEntryOpen(menuListEntry: MenuListEntry): boolean {
|
||||
|
@ -385,30 +395,60 @@
|
|||
export function scrollViewTo(distanceDown: number) {
|
||||
scroller?.div?.()?.scrollTo(0, distanceDown);
|
||||
}
|
||||
// Called only when `open` is changed from outside this component
|
||||
$effect(() => {
|
||||
watchOpen(open);
|
||||
});
|
||||
$effect(() => {
|
||||
watchEntries(entries);
|
||||
});
|
||||
let filteredEntries = $derived(entries.map((section) => section.filter((entry) => inSearch(search, entry))));
|
||||
$effect(() => {
|
||||
watchRemeasureWidth(filteredEntries, drawIcon);
|
||||
});
|
||||
$effect(() => {
|
||||
watchHighlightedWithSearch(filteredEntries, open);
|
||||
});
|
||||
let virtualScrollingTotalHeight = $derived(filteredEntries.length === 0 ? 0 : filteredEntries[0].length * virtualScrollingEntryHeight);
|
||||
let virtualScrollingStartIndex = $derived(Math.floor(virtualScrollingEntriesStart / virtualScrollingEntryHeight) || 0);
|
||||
let virtualScrollingEndIndex = $derived(filteredEntries.length === 0 ? 0 : Math.min(filteredEntries[0].length, virtualScrollingStartIndex + 1 + 400 / virtualScrollingEntryHeight));
|
||||
let startIndex = $derived(virtualScrollingEntryHeight ? virtualScrollingStartIndex : 0);
|
||||
|
||||
export {
|
||||
entries,
|
||||
activeEntry,
|
||||
open,
|
||||
direction,
|
||||
minWidth,
|
||||
drawIcon,
|
||||
interactive,
|
||||
scrollableY,
|
||||
virtualScrollingEntryHeight,
|
||||
tooltip,
|
||||
}
|
||||
</script>
|
||||
|
||||
<FloatingMenu
|
||||
class="menu-list"
|
||||
{open}
|
||||
on:open={({ detail }) => (open = detail)}
|
||||
on:naturalWidth
|
||||
bind:open
|
||||
type="Dropdown"
|
||||
windowEdgeMargin={0}
|
||||
escapeCloses={false}
|
||||
{direction}
|
||||
{minWidth}
|
||||
scrollableY={scrollableY && virtualScrollingEntryHeight === 0}
|
||||
{onnaturalWidth}
|
||||
bind:this={self}
|
||||
>
|
||||
{#if search.length > 0}
|
||||
<TextInput class="search" value={search} on:value={({ detail }) => (search = detail)} bind:this={searchTextInput}></TextInput>
|
||||
<TextInput class="search" bind:value={search} bind:this={searchTextInput}></TextInput>
|
||||
{/if}
|
||||
<!-- 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 `self`. -->
|
||||
<LayoutCol
|
||||
bind:this={scroller}
|
||||
scrollableY={scrollableY && virtualScrollingEntryHeight !== 0}
|
||||
on:scroll={onScroll}
|
||||
onscroll={onScroll}
|
||||
styles={{ "min-width": virtualScrollingEntryHeight ? `${minWidth}px` : `inherit` }}
|
||||
>
|
||||
{#if virtualScrollingEntryHeight}
|
||||
|
@ -424,14 +464,14 @@
|
|||
classes={{ open: isEntryOpen(entry), active: entry.label === highlighted?.label, disabled: Boolean(entry.disabled) }}
|
||||
styles={{ height: virtualScrollingEntryHeight || "20px" }}
|
||||
{tooltip}
|
||||
on:click={() => !entry.disabled && onEntryClick(entry)}
|
||||
on:pointerenter={() => !entry.disabled && onEntryPointerEnter(entry)}
|
||||
on:pointerleave={() => !entry.disabled && onEntryPointerLeave(entry)}
|
||||
onclick={() => !entry.disabled && onEntryClick(entry)}
|
||||
onpointerenter={() => !entry.disabled && onEntryPointerEnter(entry)}
|
||||
onpointerleave={() => !entry.disabled && onEntryPointerLeave(entry)}
|
||||
>
|
||||
{#if entry.icon && drawIcon}
|
||||
<IconLabel icon={entry.icon} iconSizeOverride={16} class="entry-icon" />
|
||||
{:else if drawIcon}
|
||||
<div class="no-icon" />
|
||||
<div class="no-icon"></div>
|
||||
{/if}
|
||||
|
||||
{#if entry.font}
|
||||
|
@ -447,18 +487,13 @@
|
|||
{#if entry.children?.length}
|
||||
<IconLabel class="submenu-arrow" icon="DropdownArrow" />
|
||||
{:else}
|
||||
<div class="no-submenu-arrow" />
|
||||
<div class="no-submenu-arrow"></div>
|
||||
{/if}
|
||||
|
||||
{#if entry.children}
|
||||
<MenuList
|
||||
on:naturalWidth={({ detail }) => {
|
||||
// We do a manual dispatch here instead of just `on:naturalWidth` as a workaround for the <script> tag
|
||||
// at the top of this file displaying a "'render' implicitly has return type 'any' because..." error.
|
||||
// See explanation at <https://github.com/sveltejs/language-tools/issues/452#issuecomment-723148184>.
|
||||
dispatch("naturalWidth", detail);
|
||||
}}
|
||||
open={getChildReference(entry)?.open || false}
|
||||
{onnaturalWidth}
|
||||
open={entry?.open}
|
||||
direction="TopRight"
|
||||
entries={entry.children}
|
||||
{minWidth}
|
||||
|
|
|
@ -1,30 +1,35 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher, getContext, onMount } from "svelte";
|
||||
import { getContext, onMount } from "svelte";
|
||||
|
||||
import type { FrontendNodeType } from "@graphite/messages";
|
||||
import type { FrontendNodeType } from "@graphite/messages.svelte";
|
||||
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
|
||||
|
||||
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
|
||||
import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
import type { WheelEventHandler } from 'svelte/elements';
|
||||
|
||||
const dispatch = createEventDispatcher<{ selectNodeType: string }>();
|
||||
const nodeGraph = getContext<NodeGraphState>("nodeGraph");
|
||||
let nodeTypes: FrontendNodeType[] = [ ...$nodeGraph.nodeTypes ];
|
||||
|
||||
export let disabled = false;
|
||||
export let initialSearchTerm = "";
|
||||
interface Props {
|
||||
disabled?: boolean;
|
||||
initialSearchTerm?: string;
|
||||
onselectNodeType?: (selectNodeType: string) => void;
|
||||
onwheel?: WheelEventHandler<HTMLDivElement>;
|
||||
}
|
||||
|
||||
let nodeSearchInput: TextInput | undefined = undefined;
|
||||
let searchTerm = initialSearchTerm;
|
||||
let { disabled = false, initialSearchTerm = "", onselectNodeType, onwheel }: Props = $props();
|
||||
|
||||
$: nodeCategories = buildNodeCategories($nodeGraph.nodeTypes, searchTerm);
|
||||
let nodeSearchInput: TextInput | undefined = $state();
|
||||
let searchTerm = $state(initialSearchTerm);
|
||||
|
||||
type NodeCategoryDetails = {
|
||||
nodes: FrontendNodeType[];
|
||||
open: boolean;
|
||||
};
|
||||
|
||||
function buildNodeCategories(nodeTypes: FrontendNodeType[], searchTerm: string): [string, NodeCategoryDetails][] {
|
||||
function buildNodeCategories(searchTerm: string): [string, NodeCategoryDetails][] {
|
||||
const categories = new Map<string, NodeCategoryDetails>();
|
||||
const isTypeSearch = searchTerm.toLowerCase().startsWith("type:");
|
||||
let typeSearchTerm = "";
|
||||
|
@ -107,18 +112,30 @@
|
|||
onMount(() => {
|
||||
setTimeout(() => nodeSearchInput?.focus(), 0);
|
||||
});
|
||||
|
||||
let nodeCategories = $state<[string, NodeCategoryDetails][]>([]);
|
||||
|
||||
$effect.pre(() => {
|
||||
nodeCategories = buildNodeCategories(searchTerm);
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<div class="node-catalog">
|
||||
<TextInput placeholder="Search Nodes..." value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} />
|
||||
<div class="list-results" on:wheel|passive|stopPropagation>
|
||||
<TextInput placeholder="Search Nodes..." bind:value={searchTerm} bind:this={nodeSearchInput} />
|
||||
<div class="list-results" onwheel={(event) => {
|
||||
// onwheel events are passive by default
|
||||
// https://svelte.dev/docs/svelte/v5-migration-guide#Breaking-changes-in-runes-mode-Touch-and-wheel-events-are-passive
|
||||
event.stopPropagation();
|
||||
onwheel?.(event);
|
||||
}}>
|
||||
{#each nodeCategories as nodeCategory}
|
||||
<details open={nodeCategory[1].open}>
|
||||
<summary>
|
||||
<TextLabel>{nodeCategory[0]}</TextLabel>
|
||||
</summary>
|
||||
{#each nodeCategory[1].nodes as nodeType}
|
||||
<TextButton {disabled} label={nodeType.name} tooltip={$nodeGraph.nodeDescriptions.get(nodeType.name)} action={() => dispatch("selectNodeType", nodeType.name)} />
|
||||
<TextButton {disabled} label={nodeType.name} tooltip={$nodeGraph.nodeDescriptions.get(nodeType.name)} onclick={() => onselectNodeType?.(nodeType.name)} />
|
||||
{/each}
|
||||
</details>
|
||||
{:else}
|
||||
|
|
|
@ -1,14 +1,21 @@
|
|||
<script lang="ts">
|
||||
export let condition: boolean;
|
||||
export let wrapperClass = "";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
interface Props {
|
||||
condition: boolean;
|
||||
wrapperClass?: string;
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let { condition, wrapperClass = "", children }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if condition}
|
||||
<div class={wrapperClass}>
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{:else}
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
{/if}
|
||||
|
||||
<style lang="scss" global></style>
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
<script lang="ts" context="module">
|
||||
export type MenuType = "Popover" | "Dropdown" | "Dialog" | "Cursor";
|
||||
|
||||
<script lang="ts" module>
|
||||
/// Prevents the escape key from closing the parent floating menu of the given element.
|
||||
/// This works by momentarily setting the `data-escape-does-not-close` attribute on the parent floating menu element.
|
||||
/// After checking for the Escape key, it checks (in one `setTimeout`) for the attribute and ignores the key if it's present.
|
||||
|
@ -17,11 +15,13 @@
|
|||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount, afterUpdate, createEventDispatcher, tick } from "svelte";
|
||||
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||
|
||||
import type { MenuDirection } from "@graphite/messages";
|
||||
|
||||
import { onMount, tick } from "svelte";
|
||||
|
||||
import type { MenuDirection } from "@graphite/messages.svelte";
|
||||
import { browserVersion } from "@graphite/utility-functions/platform";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
|
@ -29,27 +29,47 @@
|
|||
const BUTTON_LEFT = 0;
|
||||
const POINTER_STRAY_DISTANCE = 100;
|
||||
|
||||
const dispatch = createEventDispatcher<{ open: boolean; naturalWidth: number }>();
|
||||
type DivHTMLElementProps = SvelteHTMLElements["div"];
|
||||
|
||||
interface Props extends DivHTMLElementProps {
|
||||
class?: string;
|
||||
classes?: Record<string, boolean>;
|
||||
style?: string;
|
||||
styles?: Record<string, string | number | undefined>;
|
||||
open: boolean;
|
||||
type: Graphite.MenuType;
|
||||
direction?: MenuDirection;
|
||||
windowEdgeMargin?: number;
|
||||
scrollableY?: boolean;
|
||||
minWidth?: number;
|
||||
escapeCloses?: boolean;
|
||||
strayCloses?: boolean;
|
||||
children?: import('svelte').Snippet;
|
||||
onnaturalWidth?: (naturalWidht: number) => void;
|
||||
}
|
||||
|
||||
let className = "";
|
||||
export { className as class };
|
||||
export let classes: Record<string, boolean> = {};
|
||||
let styleName = "";
|
||||
export { styleName as style };
|
||||
export let styles: Record<string, string | number | undefined> = {};
|
||||
export let open: boolean;
|
||||
export let type: MenuType;
|
||||
export let direction: MenuDirection = "Bottom";
|
||||
export let windowEdgeMargin = 6;
|
||||
export let scrollableY = false;
|
||||
export let minWidth = 0;
|
||||
export let escapeCloses = true;
|
||||
export let strayCloses = true;
|
||||
let {
|
||||
class: className = "",
|
||||
classes = {},
|
||||
style: styleName = "",
|
||||
styles = {},
|
||||
open = $bindable(),
|
||||
type,
|
||||
direction = "Bottom",
|
||||
windowEdgeMargin = 6,
|
||||
scrollableY = false,
|
||||
minWidth = 0,
|
||||
escapeCloses = true,
|
||||
strayCloses = true,
|
||||
children,
|
||||
onnaturalWidth,
|
||||
...rest
|
||||
}: Props = $props();
|
||||
|
||||
let tail: HTMLDivElement | undefined;
|
||||
let self: HTMLDivElement | undefined;
|
||||
let floatingMenuContainer: HTMLDivElement | undefined;
|
||||
let floatingMenuContent: LayoutCol | undefined;
|
||||
let tail: HTMLDivElement | undefined = $state();
|
||||
let self: HTMLDivElement | undefined = $state();
|
||||
let floatingMenuContainer: HTMLDivElement | undefined = $state();
|
||||
let floatingMenuContent: LayoutCol | undefined = $state();
|
||||
|
||||
// The resize observer is attached to the floating menu container, which is the zero-height div of the width of the parent element's floating menu spawner.
|
||||
// Since CSS doesn't let us make the floating menu (with `position: fixed`) have a 100% width of this container, we need to use JS to observe its size and
|
||||
|
@ -60,25 +80,15 @@
|
|||
resizeObserverCallback(entries);
|
||||
});
|
||||
let wasOpen = open;
|
||||
let measuringOngoing = false;
|
||||
let measuringOngoing = $state(false);
|
||||
let measuringOngoingGuard = false;
|
||||
let minWidthParentWidth = 0;
|
||||
let minWidthParentWidth = $state(0);
|
||||
let pointerStillDown = false;
|
||||
let workspaceBounds = new DOMRect();
|
||||
let floatingMenuBounds = new DOMRect();
|
||||
let floatingMenuContentBounds = new DOMRect();
|
||||
|
||||
$: watchOpenChange(open);
|
||||
|
||||
$: minWidthStyleValue = measuringOngoing ? "0" : `${Math.max(minWidth, minWidthParentWidth)}px`;
|
||||
$: displayTail = open && type === "Popover";
|
||||
$: displayContainer = open || measuringOngoing;
|
||||
$: extraClasses = Object.entries(classes)
|
||||
.flatMap(([className, stateName]) => (stateName ? [className] : []))
|
||||
.join(" ");
|
||||
$: extraStyles = Object.entries(styles)
|
||||
.flatMap((styleAndValue) => (styleAndValue[1] !== undefined ? [`${styleAndValue[0]}: ${styleAndValue[1]};`] : []))
|
||||
.join(" ");
|
||||
|
||||
// Called only when `open` is changed from outside this component
|
||||
async function watchOpenChange(isOpen: boolean) {
|
||||
|
@ -157,20 +167,6 @@
|
|||
}
|
||||
});
|
||||
|
||||
afterUpdate(() => {
|
||||
// Remove the size constraint after the content updates so the resize observer can measure the content and reapply a newly calculated one
|
||||
const floatingMenuContentDiv = floatingMenuContent?.div?.();
|
||||
if (type === "Dialog" && floatingMenuContentDiv) {
|
||||
// We have to set the style properties directly because attempting to do it through a Svelte bound property results in `afterUpdate()` being triggered
|
||||
floatingMenuContentDiv.style.setProperty("min-width", "unset");
|
||||
floatingMenuContentDiv.style.setProperty("min-height", "unset");
|
||||
}
|
||||
|
||||
// Gets the client bounds of the elements and apply relevant styles to them.
|
||||
// TODO: Use DOM attribute bindings more whilst not causing recursive updates. Turning measuring on and off both causes the component to change,
|
||||
// TODO: which causes the `afterUpdate()` Svelte event to fire extraneous times (hurting performance and sometimes causing an infinite loop).
|
||||
if (!measuringOngoingGuard) positionAndStyleFloatingMenu();
|
||||
});
|
||||
|
||||
function resizeObserverCallback(entries: ResizeObserverEntry[]) {
|
||||
minWidthParentWidth = entries[0].contentRect.width;
|
||||
|
@ -292,7 +288,7 @@
|
|||
|
||||
// Notify the parent about the measured natural width
|
||||
if (naturalWidth !== undefined && naturalWidth >= 0) {
|
||||
dispatch("naturalWidth", naturalWidth);
|
||||
onnaturalWidth?.(naturalWidth);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -316,7 +312,7 @@
|
|||
if (strayCloses && notHoveringOverOwnSpawner && isPointerEventOutsideFloatingMenu(e, POINTER_STRAY_DISTANCE)) {
|
||||
// TODO: Extend this rectangle bounds check to all submenu bounds up the DOM tree since currently submenus disappear
|
||||
// TODO: with zero stray distance if the cursor is further than the stray distance from only the top-level menu
|
||||
dispatch("open", false);
|
||||
open = false
|
||||
}
|
||||
|
||||
// Clean up any messes from lost pointerup events
|
||||
|
@ -385,7 +381,7 @@
|
|||
const foundTarget = filteredListOfDescendantSpawners.find((item: Element): boolean => item === targetSpawner);
|
||||
// If the currently hovered spawner is one of the found valid hover-transferrable spawners, swap to it by clicking on it
|
||||
if (foundTarget) {
|
||||
dispatch("open", false);
|
||||
open = false;
|
||||
(foundTarget as HTMLElement).click();
|
||||
}
|
||||
|
||||
|
@ -399,7 +395,7 @@
|
|||
if (escapeCloses && e.key === "Escape") {
|
||||
setTimeout(() => {
|
||||
if (!floatingMenuContainer?.querySelector("[data-floating-menu-content][data-escape-does-not-close]")) {
|
||||
dispatch("open", false);
|
||||
open = false;
|
||||
}
|
||||
}, 0);
|
||||
|
||||
|
@ -411,7 +407,7 @@
|
|||
function pointerDownHandler(e: PointerEvent) {
|
||||
// Close the floating menu if the pointer clicked outside the floating menu (but within stray distance)
|
||||
if (isPointerEventOutsideFloatingMenu(e)) {
|
||||
dispatch("open", false);
|
||||
open = false;
|
||||
|
||||
// Track if the left pointer button is now down so its later click event can be canceled
|
||||
const eventIsForLmb = e.button === BUTTON_LEFT;
|
||||
|
@ -454,21 +450,50 @@
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
// Remove the size constraint after the content updates so the resize observer can measure the content and reapply a newly calculated one
|
||||
const floatingMenuContentDiv = floatingMenuContent?.div?.();
|
||||
if (type === "Dialog" && floatingMenuContentDiv) {
|
||||
// We have to set the style properties directly because attempting to do it through a Svelte bound property results in `afterUpdate()` being triggered
|
||||
floatingMenuContentDiv.style.setProperty("min-width", "unset");
|
||||
floatingMenuContentDiv.style.setProperty("min-height", "unset");
|
||||
}
|
||||
|
||||
// Gets the client bounds of the elements and apply relevant styles to them.
|
||||
// TODO: Use DOM attribute bindings more whilst not causing recursive updates. Turning measuring on and off both causes the component to change,
|
||||
// TODO: which causes the `afterUpdate()` Svelte event to fire extraneous times (hurting performance and sometimes causing an infinite loop).
|
||||
if (!measuringOngoingGuard) positionAndStyleFloatingMenu();
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
watchOpenChange(open);
|
||||
});
|
||||
|
||||
let minWidthStyleValue = $derived(measuringOngoing ? "0" : `${Math.max(minWidth, minWidthParentWidth)}px`);
|
||||
let displayTail = $derived(open && type === "Popover");
|
||||
let displayContainer = $derived(open || measuringOngoing);
|
||||
let extraClasses = $derived(Object.entries(classes)
|
||||
.flatMap(([className, stateName]) => (stateName ? [className] : []))
|
||||
.join(" "));
|
||||
let extraStyles = $derived(Object.entries(styles)
|
||||
.flatMap((styleAndValue) => (styleAndValue[1] !== undefined ? [`${styleAndValue[0]}: ${styleAndValue[1]};`] : []))
|
||||
.join(" "));
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={`floating-menu ${direction.toLowerCase()} ${type.toLowerCase()} ${className} ${extraClasses}`.trim()}
|
||||
style={`${styleName} ${extraStyles}`.trim() || undefined}
|
||||
bind:this={self}
|
||||
{...$$restProps}
|
||||
{...rest}
|
||||
>
|
||||
{#if displayTail}
|
||||
<div class="tail" bind:this={tail} />
|
||||
<div class="tail" bind:this={tail}></div>
|
||||
{/if}
|
||||
{#if displayContainer}
|
||||
<div class="floating-menu-container" bind:this={floatingMenuContainer}>
|
||||
<LayoutCol class="floating-menu-content" styles={{ "min-width": minWidthStyleValue }} {scrollableY} bind:this={floatingMenuContent} data-floating-menu-content>
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</LayoutCol>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -1,23 +1,40 @@
|
|||
<script lang="ts">
|
||||
let className = "";
|
||||
export { className as class };
|
||||
export let classes: Record<string, boolean> = {};
|
||||
let styleName = "";
|
||||
export { styleName as style };
|
||||
export let styles: Record<string, string | number | undefined> = {};
|
||||
export let tooltip: string | undefined = undefined;
|
||||
// TODO: Add middle-click drag scrolling
|
||||
export let scrollableX = false;
|
||||
export let scrollableY = false;
|
||||
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||
|
||||
let self: HTMLDivElement | undefined;
|
||||
type DivHTMLElementProps = SvelteHTMLElements["div"];
|
||||
|
||||
interface Props extends DivHTMLElementProps {
|
||||
class?: string;
|
||||
classes?: Record<string, boolean>;
|
||||
style?: string;
|
||||
styles?: Record<string, string | number | undefined>;
|
||||
tooltip?: string | undefined;
|
||||
// TODO: Add middle-click drag scrolling
|
||||
scrollableX?: boolean;
|
||||
scrollableY?: boolean;
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
$: extraClasses = Object.entries(classes)
|
||||
let {
|
||||
class: className = "",
|
||||
classes = {},
|
||||
style: styleName = "",
|
||||
styles = {},
|
||||
tooltip = undefined,
|
||||
scrollableX = false,
|
||||
scrollableY = false,
|
||||
children,
|
||||
...rest
|
||||
}: Props = $props();
|
||||
|
||||
let self: HTMLDivElement | undefined = $state();
|
||||
|
||||
let extraClasses = $derived(Object.entries(classes)
|
||||
.flatMap(([className, stateName]) => (stateName ? [className] : []))
|
||||
.join(" ");
|
||||
$: extraStyles = Object.entries(styles)
|
||||
.join(" "));
|
||||
let extraStyles = $derived(Object.entries(styles)
|
||||
.flatMap((styleAndValue) => (styleAndValue[1] !== undefined ? [`${styleAndValue[0]}: ${styleAndValue[1]};`] : []))
|
||||
.join(" ");
|
||||
.join(" "));
|
||||
|
||||
export function div(): HTMLDivElement | undefined {
|
||||
return self;
|
||||
|
@ -34,26 +51,26 @@
|
|||
style={`${styleName} ${extraStyles}`.trim() || undefined}
|
||||
title={tooltip}
|
||||
bind:this={self}
|
||||
on:auxclick
|
||||
on:blur
|
||||
on:click
|
||||
on:dblclick
|
||||
on:dragend
|
||||
on:dragleave
|
||||
on:dragover
|
||||
on:dragstart
|
||||
on:drop
|
||||
on:mouseup
|
||||
on:pointerdown
|
||||
on:pointerenter
|
||||
on:pointerleave
|
||||
on:scroll
|
||||
{...$$restProps}
|
||||
{...rest}
|
||||
>
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
<!-- Unused (each impacts performance, see <https://github.com/GraphiteEditor/Graphite/issues/1877>):
|
||||
onauxclick={bubble('auxclick')}
|
||||
onblur={bubble('blur')}
|
||||
onclick={bubble('click')}
|
||||
ondblclick={bubble('dblclick')}
|
||||
ondragend={bubble('dragend')}
|
||||
ondragleave={bubble('dragleave')}
|
||||
ondragover={bubble('dragover')}
|
||||
ondragstart={bubble('dragstart')}
|
||||
ondrop={bubble('drop')}
|
||||
onmouseup={bubble('mouseup')}
|
||||
onpointerdown={bubble('pointerdown')}
|
||||
onpointerenter={bubble('pointerenter')}
|
||||
onpointerleave={bubble('pointerleave')}
|
||||
onscroll={bubble('scroll')}
|
||||
on:contextmenu
|
||||
on:copy
|
||||
on:cut
|
||||
|
|
|
@ -1,23 +1,41 @@
|
|||
<script lang="ts">
|
||||
let className = "";
|
||||
export { className as class };
|
||||
export let classes: Record<string, boolean> = {};
|
||||
let styleName = "";
|
||||
export { styleName as style };
|
||||
export let styles: Record<string, string | number | undefined> = {};
|
||||
export let tooltip: string | undefined = undefined;
|
||||
// TODO: Add middle-click drag scrolling
|
||||
export let scrollableX = false;
|
||||
export let scrollableY = false;
|
||||
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||
|
||||
let self: HTMLDivElement | undefined;
|
||||
type DivHTMLElementProps = SvelteHTMLElements["div"];
|
||||
|
||||
$: extraClasses = Object.entries(classes)
|
||||
interface Props extends DivHTMLElementProps {
|
||||
class?: string;
|
||||
classes?: Record<string, boolean>;
|
||||
style?: string;
|
||||
styles?: Record<string, string | number | undefined>;
|
||||
tooltip?: string | undefined;
|
||||
// TODO: Add middle-click drag scrolling
|
||||
scrollableX?: boolean;
|
||||
scrollableY?: boolean;
|
||||
children?: import('svelte').Snippet;
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
let {
|
||||
class: className = "",
|
||||
classes = {},
|
||||
style: styleName = "",
|
||||
styles = {},
|
||||
tooltip = undefined,
|
||||
scrollableX = false,
|
||||
scrollableY = false,
|
||||
children,
|
||||
...rest
|
||||
}: Props = $props();
|
||||
|
||||
let self: HTMLDivElement | undefined = $state();
|
||||
|
||||
let extraClasses = $derived(Object.entries(classes)
|
||||
.flatMap(([className, stateName]) => (stateName ? [className] : []))
|
||||
.join(" ");
|
||||
$: extraStyles = Object.entries(styles)
|
||||
.join(" "));
|
||||
let extraStyles = $derived(Object.entries(styles)
|
||||
.flatMap((styleAndValue) => (styleAndValue[1] !== undefined ? [`${styleAndValue[0]}: ${styleAndValue[1]};`] : []))
|
||||
.join(" ");
|
||||
.join(" "));
|
||||
|
||||
export function div(): HTMLDivElement | undefined {
|
||||
return self;
|
||||
|
@ -34,26 +52,26 @@
|
|||
style={`${styleName} ${extraStyles}`.trim() || undefined}
|
||||
title={tooltip}
|
||||
bind:this={self}
|
||||
on:auxclick
|
||||
on:blur
|
||||
on:click
|
||||
on:dblclick
|
||||
on:dragend
|
||||
on:dragleave
|
||||
on:dragover
|
||||
on:dragstart
|
||||
on:drop
|
||||
on:mouseup
|
||||
on:pointerdown
|
||||
on:pointerenter
|
||||
on:pointerleave
|
||||
on:scroll
|
||||
{...$$restProps}
|
||||
{...rest}
|
||||
>
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
<!-- Unused (each impacts performance, see <https://github.com/GraphiteEditor/Graphite/issues/1877>):
|
||||
onauxclick={bubble('auxclick')}
|
||||
onblur={bubble('blur')}
|
||||
onclick={bubble('click')}
|
||||
ondblclick={bubble('dblclick')}
|
||||
ondragend={bubble('dragend')}
|
||||
ondragleave={bubble('dragleave')}
|
||||
ondragover={bubble('dragover')}
|
||||
ondragstart={bubble('dragstart')}
|
||||
ondrop={bubble('drop')}
|
||||
onmouseup={bubble('mouseup')}
|
||||
onpointerdown={bubble('pointerdown')}
|
||||
onpointerenter={bubble('pointerenter')}
|
||||
onpointerleave={bubble('pointerleave')}
|
||||
onscroll={bubble('scroll')}
|
||||
on:contextmenu
|
||||
on:copy
|
||||
on:cut
|
||||
|
|
|
@ -15,8 +15,8 @@
|
|||
UpdateEyedropperSamplingState,
|
||||
UpdateMouseCursor,
|
||||
isWidgetSpanRow,
|
||||
} from "@graphite/messages";
|
||||
import type { DocumentState } from "@graphite/state-providers/document";
|
||||
} from "@graphite/messages.svelte";
|
||||
import type { DocumentState } from "@graphite/state-providers/document.svelte";
|
||||
import { textInputCleanup } from "@graphite/utility-functions/keyboard-entry";
|
||||
import { extractPixelData, rasterizeSVGCanvas } from "@graphite/utility-functions/rasterization";
|
||||
import { updateBoundsOfViewports } from "@graphite/utility-functions/viewports";
|
||||
|
@ -29,70 +29,70 @@
|
|||
import ScrollbarInput from "@graphite/components/widgets/inputs/ScrollbarInput.svelte";
|
||||
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
|
||||
|
||||
let rulerHorizontal: RulerInput | undefined;
|
||||
let rulerVertical: RulerInput | undefined;
|
||||
let viewport: HTMLDivElement | undefined;
|
||||
let rulerHorizontal: RulerInput | undefined = $state();
|
||||
let rulerVertical: RulerInput | undefined = $state();
|
||||
let viewport: HTMLDivElement | undefined = $state();
|
||||
|
||||
const editor = getContext<Editor>("editor");
|
||||
const document = getContext<DocumentState>("document");
|
||||
|
||||
// Interactive text editing
|
||||
let textInput: undefined | HTMLDivElement = undefined;
|
||||
let showTextInput: boolean;
|
||||
let textInputMatrix: number[];
|
||||
let textInput: undefined | HTMLDivElement = $state(undefined);
|
||||
let showTextInput: boolean = $state(false);
|
||||
let textInputMatrix: number[] = $state([]);
|
||||
|
||||
// Scrollbars
|
||||
let scrollbarPos: XY = { x: 0.5, y: 0.5 };
|
||||
let scrollbarSize: XY = { x: 0.5, y: 0.5 };
|
||||
let scrollbarPos: XY = $state({ x: 0.5, y: 0.5 });
|
||||
let scrollbarSize: XY = $state({ x: 0.5, y: 0.5 });
|
||||
let scrollbarMultiplier: XY = { x: 0, y: 0 };
|
||||
|
||||
// Rulers
|
||||
let rulerOrigin: XY = { x: 0, y: 0 };
|
||||
let rulerSpacing = 100;
|
||||
let rulerInterval = 100;
|
||||
let rulersVisible = true;
|
||||
let rulerOrigin: XY = $state({ x: 0, y: 0 });
|
||||
let rulerSpacing = $state(100);
|
||||
let rulerInterval = $state(100);
|
||||
let rulersVisible = $state(true);
|
||||
|
||||
// Rendered SVG viewport data
|
||||
let artworkSvg = "";
|
||||
let artworkSvg = $state("");
|
||||
|
||||
// Rasterized SVG viewport data, or none if it's not up-to-date
|
||||
let rasterizedCanvas: HTMLCanvasElement | undefined = undefined;
|
||||
let rasterizedContext: CanvasRenderingContext2D | undefined = undefined;
|
||||
|
||||
// Cursor icon to display while hovering over the canvas
|
||||
let canvasCursor = "default";
|
||||
let canvasCursor = $state("default");
|
||||
|
||||
// Cursor position for cursor floating menus like the Eyedropper tool zoom
|
||||
let cursorLeft = 0;
|
||||
let cursorTop = 0;
|
||||
let cursorEyedropper = false;
|
||||
let cursorEyedropperPreviewImageData: ImageData | undefined = undefined;
|
||||
let cursorEyedropperPreviewColorChoice = "";
|
||||
let cursorEyedropperPreviewColorPrimary = "";
|
||||
let cursorEyedropperPreviewColorSecondary = "";
|
||||
let cursorLeft = $state(0);
|
||||
let cursorTop = $state(0);
|
||||
let cursorEyedropper = $state(false);
|
||||
let cursorEyedropperPreviewImageData: ImageData | undefined = $state(undefined);
|
||||
let cursorEyedropperPreviewColorChoice = $state("");
|
||||
let cursorEyedropperPreviewColorPrimary = $state("");
|
||||
let cursorEyedropperPreviewColorSecondary = $state("");
|
||||
|
||||
// Canvas dimensions
|
||||
let canvasSvgWidth: number | undefined = undefined;
|
||||
let canvasSvgHeight: number | undefined = undefined;
|
||||
let canvasSvgWidth: number | undefined = $state(undefined);
|
||||
let canvasSvgHeight: number | undefined = $state(undefined);
|
||||
|
||||
let devicePixelRatio: number | undefined;
|
||||
let devicePixelRatio: number | undefined = $state();
|
||||
|
||||
// Dimension is rounded up to the nearest even number because resizing is centered, and dividing an odd number by 2 for centering causes antialiasing
|
||||
$: canvasWidthRoundedToEven = canvasSvgWidth && (canvasSvgWidth % 2 === 1 ? canvasSvgWidth + 1 : canvasSvgWidth);
|
||||
$: canvasHeightRoundedToEven = canvasSvgHeight && (canvasSvgHeight % 2 === 1 ? canvasSvgHeight + 1 : canvasSvgHeight);
|
||||
let canvasWidthRoundedToEven = $derived(canvasSvgWidth && (canvasSvgWidth % 2 === 1 ? canvasSvgWidth + 1 : canvasSvgWidth));
|
||||
let canvasHeightRoundedToEven = $derived(canvasSvgHeight && (canvasSvgHeight % 2 === 1 ? canvasSvgHeight + 1 : canvasSvgHeight));
|
||||
// Used to set the canvas element size on the page.
|
||||
// The value above in pixels, or if undefined, we fall back to 100% as a non-pixel-perfect backup that's hopefully short-lived
|
||||
$: canvasWidthCSS = canvasWidthRoundedToEven ? `${canvasWidthRoundedToEven}px` : "100%";
|
||||
$: canvasHeightCSS = canvasHeightRoundedToEven ? `${canvasHeightRoundedToEven}px` : "100%";
|
||||
let canvasWidthCSS = $derived(canvasWidthRoundedToEven ? `${canvasWidthRoundedToEven}px` : "100%");
|
||||
let canvasHeightCSS = $derived(canvasHeightRoundedToEven ? `${canvasHeightRoundedToEven}px` : "100%");
|
||||
|
||||
$: canvasWidthScaled = canvasSvgWidth && devicePixelRatio && Math.floor(canvasSvgWidth * devicePixelRatio);
|
||||
$: canvasHeightScaled = canvasSvgHeight && devicePixelRatio && Math.floor(canvasSvgHeight * devicePixelRatio);
|
||||
let canvasWidthScaled = $derived(canvasSvgWidth && devicePixelRatio && Math.floor(canvasSvgWidth * devicePixelRatio));
|
||||
let canvasHeightScaled = $derived(canvasSvgHeight && devicePixelRatio && Math.floor(canvasSvgHeight * devicePixelRatio));
|
||||
|
||||
// Used to set the canvas rendering dimensions.
|
||||
$: canvasWidthScaledRoundedToEven = canvasWidthScaled && (canvasWidthScaled % 2 === 1 ? canvasWidthScaled + 1 : canvasWidthScaled);
|
||||
$: canvasHeightScaledRoundedToEven = canvasHeightScaled && (canvasHeightScaled % 2 === 1 ? canvasHeightScaled + 1 : canvasHeightScaled);
|
||||
let canvasWidthScaledRoundedToEven = $derived(canvasWidthScaled && (canvasWidthScaled % 2 === 1 ? canvasWidthScaled + 1 : canvasWidthScaled));
|
||||
let canvasHeightScaledRoundedToEven = $derived(canvasHeightScaled && (canvasHeightScaled % 2 === 1 ? canvasHeightScaled + 1 : canvasHeightScaled));
|
||||
|
||||
$: toolShelfTotalToolsAndSeparators = ((layoutGroup) => {
|
||||
let toolShelfTotalToolsAndSeparators = $derived(((layoutGroup) => {
|
||||
if (!isWidgetSpanRow(layoutGroup)) return undefined;
|
||||
|
||||
let totalSeparators = 0;
|
||||
|
@ -124,7 +124,7 @@
|
|||
totalToolRowsFor2Columns,
|
||||
totalToolRowsFor3Columns,
|
||||
};
|
||||
})($document.toolShelfLayout.layout[0]);
|
||||
})(document.toolShelfLayout.layout[0]));
|
||||
|
||||
function dropFile(e: DragEvent) {
|
||||
const { dataTransfer } = e;
|
||||
|
@ -461,15 +461,15 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<LayoutCol class="document" on:dragover={(e) => e.preventDefault()} on:drop={dropFile}>
|
||||
<LayoutRow class="control-bar" classes={{ "for-graph": $document.graphViewOverlayOpen }} scrollableX={true}>
|
||||
{#if !$document.graphViewOverlayOpen}
|
||||
<WidgetLayout layout={$document.documentModeLayout} />
|
||||
<WidgetLayout layout={$document.toolOptionsLayout} />
|
||||
<LayoutCol class="document" ondragover={(e) => e.preventDefault()} ondrop={dropFile}>
|
||||
<LayoutRow class="control-bar" classes={{ "for-graph": document.graphViewOverlayOpen }} scrollableX={true}>
|
||||
{#if !document.graphViewOverlayOpen}
|
||||
<WidgetLayout layout={document.documentModeLayout} />
|
||||
<WidgetLayout layout={document.toolOptionsLayout} />
|
||||
<LayoutRow class="spacer" />
|
||||
<WidgetLayout layout={$document.documentBarLayout} />
|
||||
<WidgetLayout layout={document.documentBarLayout} />
|
||||
{:else}
|
||||
<WidgetLayout layout={$document.nodeGraphControlBarLayout} />
|
||||
<WidgetLayout layout={document.nodeGraphControlBarLayout} />
|
||||
{/if}
|
||||
</LayoutRow>
|
||||
<LayoutRow
|
||||
|
@ -482,15 +482,15 @@
|
|||
}}
|
||||
>
|
||||
<LayoutCol class="tool-shelf">
|
||||
{#if !$document.graphViewOverlayOpen}
|
||||
{#if !document.graphViewOverlayOpen}
|
||||
<LayoutCol class="tools" scrollableY={true}>
|
||||
<WidgetLayout layout={$document.toolShelfLayout} />
|
||||
<WidgetLayout layout={document.toolShelfLayout} />
|
||||
</LayoutCol>
|
||||
{:else}
|
||||
<LayoutRow class="spacer" />
|
||||
{/if}
|
||||
<LayoutCol class="tool-shelf-bottom-widgets">
|
||||
<WidgetLayout class={"working-colors-input-area"} layout={$document.workingColorsLayout} />
|
||||
<WidgetLayout class={"working-colors-input-area"} layout={document.workingColorsLayout} />
|
||||
</LayoutCol>
|
||||
</LayoutCol>
|
||||
<LayoutCol class="viewport-container">
|
||||
|
@ -517,13 +517,13 @@
|
|||
y={cursorTop}
|
||||
/>
|
||||
{/if}
|
||||
<div class="viewport" on:pointerdown={(e) => canvasPointerDown(e)} bind:this={viewport} data-viewport>
|
||||
<div class="viewport" onpointerdown={(e) => canvasPointerDown(e)} bind:this={viewport} data-viewport>
|
||||
<svg class="artboards" style:width={canvasWidthCSS} style:height={canvasHeightCSS}>
|
||||
{@html artworkSvg}
|
||||
</svg>
|
||||
<div class="text-input" style:width={canvasWidthCSS} style:height={canvasHeightCSS} style:pointer-events={showTextInput ? "auto" : ""}>
|
||||
{#if showTextInput}
|
||||
<div bind:this={textInput} style:transform="matrix({textInputMatrix})" on:scroll={preventTextEditingScroll} />
|
||||
<div bind:this={textInput} style:transform="matrix({textInputMatrix})" onscroll={preventTextEditingScroll}></div>
|
||||
{/if}
|
||||
</div>
|
||||
<canvas
|
||||
|
@ -536,7 +536,7 @@
|
|||
>
|
||||
</canvas>
|
||||
</div>
|
||||
<div class="graph-view" class:open={$document.graphViewOverlayOpen} style:--fade-artwork={`${$document.fadeArtwork}%`} data-graph>
|
||||
<div class="graph-view" class:open={document.graphViewOverlayOpen} style:--fade-artwork={`${document.fadeArtwork}%`} data-graph>
|
||||
<Graph />
|
||||
</div>
|
||||
</LayoutCol>
|
||||
|
@ -545,10 +545,10 @@
|
|||
direction="Vertical"
|
||||
thumbLength={scrollbarSize.y}
|
||||
thumbPosition={scrollbarPos.y}
|
||||
on:trackShift={({ detail }) => editor.handle.panCanvasByFraction(0, detail)}
|
||||
on:thumbPosition={({ detail }) => panCanvasY(detail)}
|
||||
on:thumbDragStart={() => editor.handle.panCanvasAbortPrepare(false)}
|
||||
on:thumbDragAbort={() => editor.handle.panCanvasAbort(false)}
|
||||
ontrackShift={(detail) => editor.handle.panCanvasByFraction(0, detail)}
|
||||
onthumbPosition={(detail) => panCanvasY(detail)}
|
||||
onthumbDragStart={() => editor.handle.panCanvasAbortPrepare(false)}
|
||||
onthumbDragAbort={() => editor.handle.panCanvasAbort(false)}
|
||||
/>
|
||||
</LayoutCol>
|
||||
</LayoutRow>
|
||||
|
@ -557,11 +557,11 @@
|
|||
direction="Horizontal"
|
||||
thumbLength={scrollbarSize.x}
|
||||
thumbPosition={scrollbarPos.x}
|
||||
on:trackShift={({ detail }) => editor.handle.panCanvasByFraction(detail, 0)}
|
||||
on:thumbPosition={({ detail }) => panCanvasX(detail)}
|
||||
on:thumbDragEnd={() => editor.handle.setGridAlignedEdges()}
|
||||
on:thumbDragStart={() => editor.handle.panCanvasAbortPrepare(true)}
|
||||
on:thumbDragAbort={() => editor.handle.panCanvasAbort(true)}
|
||||
ontrackShift={(detail) => editor.handle.panCanvasByFraction(detail, 0)}
|
||||
onthumbPosition={(detail) => panCanvasX(detail)}
|
||||
onthumbDragEnd={() => editor.handle.setGridAlignedEdges()}
|
||||
onthumbDragStart={() => editor.handle.panCanvasAbortPrepare(true)}
|
||||
onthumbDragAbort={() => editor.handle.panCanvasAbort(true)}
|
||||
/>
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
|
|
|
@ -11,11 +11,12 @@
|
|||
UpdateLayersPanelControlBarLeftLayout,
|
||||
UpdateLayersPanelControlBarRightLayout,
|
||||
UpdateLayersPanelBottomBarLayout,
|
||||
} from "@graphite/messages";
|
||||
import type { DataBuffer, LayerPanelEntry } from "@graphite/messages";
|
||||
} from "@graphite/messages.svelte";
|
||||
import type { DataBuffer, LayerPanelEntry } from "@graphite/messages.svelte";
|
||||
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
|
||||
import { platformIsMac } from "@graphite/utility-functions/platform";
|
||||
import { extractPixelData } from "@graphite/utility-functions/rasterization";
|
||||
import type { WidgetLayout as WidgetLayoutState } from "@graphite/messages.svelte";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
|
@ -43,41 +44,38 @@
|
|||
const editor = getContext<Editor>("editor");
|
||||
const nodeGraph = getContext<NodeGraphState>("nodeGraph");
|
||||
|
||||
let list: LayoutCol | undefined;
|
||||
let list: LayoutCol | undefined = $state();
|
||||
|
||||
// Layer data
|
||||
let layerCache = new Map<string, LayerPanelEntry>(); // TODO: replace with BigUint64Array as index
|
||||
let layers: LayerListingInfo[] = [];
|
||||
let layers: LayerListingInfo[] = $state([]);
|
||||
|
||||
// Interactive dragging
|
||||
let draggable = true;
|
||||
let draggingData: undefined | DraggingData = undefined;
|
||||
let fakeHighlightOfNotYetSelectedLayerBeingDragged: undefined | bigint = undefined;
|
||||
let dragInPanel = false;
|
||||
let draggable = $state(true);
|
||||
let draggingData: undefined | DraggingData = $state(undefined);
|
||||
let fakeHighlightOfNotYetSelectedLayerBeingDragged: undefined | bigint = $state(undefined);
|
||||
let dragInPanel = $state(false);
|
||||
|
||||
// Interactive clipping
|
||||
let layerToClipUponClick: LayerListingInfo | undefined = undefined;
|
||||
let layerToClipAltKeyPressed = false;
|
||||
let layerToClipUponClick: LayerListingInfo | undefined = $state(undefined);
|
||||
let layerToClipAltKeyPressed = $state(false);
|
||||
|
||||
// Layouts
|
||||
let layersPanelControlBarLeftLayout = defaultWidgetLayout();
|
||||
let layersPanelControlBarRightLayout = defaultWidgetLayout();
|
||||
let layersPanelBottomBarLayout = defaultWidgetLayout();
|
||||
let layersPanelControlBarLeftLayout = $state<WidgetLayoutState>(defaultWidgetLayout());
|
||||
let layersPanelControlBarRightLayout = $state<WidgetLayoutState>(defaultWidgetLayout());
|
||||
let layersPanelBottomBarLayout = $state<WidgetLayoutState>(defaultWidgetLayout());
|
||||
|
||||
onMount(() => {
|
||||
editor.subscriptions.subscribeJsMessage(UpdateLayersPanelControlBarLeftLayout, (updateLayersPanelControlBarLeftLayout) => {
|
||||
patchWidgetLayout(layersPanelControlBarLeftLayout, updateLayersPanelControlBarLeftLayout);
|
||||
layersPanelControlBarLeftLayout = layersPanelControlBarLeftLayout;
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateLayersPanelControlBarRightLayout, (updateLayersPanelControlBarRightLayout) => {
|
||||
patchWidgetLayout(layersPanelControlBarRightLayout, updateLayersPanelControlBarRightLayout);
|
||||
layersPanelControlBarRightLayout = layersPanelControlBarRightLayout;
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateLayersPanelBottomBarLayout, (updateLayersPanelBottomBarLayout) => {
|
||||
patchWidgetLayout(layersPanelBottomBarLayout, updateLayersPanelBottomBarLayout);
|
||||
layersPanelBottomBarLayout = layersPanelBottomBarLayout;
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerStructureJs, (updateDocumentLayerStructure) => {
|
||||
|
@ -182,7 +180,7 @@
|
|||
|
||||
draggable = false;
|
||||
listing.editingName = true;
|
||||
layers = layers;
|
||||
// layers = layers;
|
||||
|
||||
await tick();
|
||||
|
||||
|
@ -197,7 +195,7 @@
|
|||
|
||||
draggable = true;
|
||||
listing.editingName = false;
|
||||
layers = layers;
|
||||
// layers = layers;
|
||||
|
||||
const name = (e.target instanceof HTMLInputElement && e.target.value) || "";
|
||||
editor.handle.setLayerName(listing.entry.id, name);
|
||||
|
@ -207,7 +205,7 @@
|
|||
async function onEditLayerNameDeselect(listing: LayerListingInfo) {
|
||||
draggable = true;
|
||||
listing.editingName = false;
|
||||
layers = layers;
|
||||
// layers = layers;
|
||||
|
||||
// Set it back to the original name if the user didn't enter a new name
|
||||
if (document.activeElement instanceof HTMLInputElement) document.activeElement.value = listing.entry.alias;
|
||||
|
@ -472,7 +470,7 @@
|
|||
});
|
||||
};
|
||||
recurse(updateDocumentLayerStructure);
|
||||
layers = layers;
|
||||
// layers = layers;
|
||||
}
|
||||
|
||||
function updateLayerInTree(targetId: bigint, targetLayer: LayerPanelEntry) {
|
||||
|
@ -481,12 +479,12 @@
|
|||
const layer = layers.find((layer: LayerListingInfo) => layer.entry.id === targetId);
|
||||
if (layer) {
|
||||
layer.entry = targetLayer;
|
||||
layers = layers;
|
||||
// layers = layers;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<LayoutCol class="layers" on:dragleave={() => (dragInPanel = false)}>
|
||||
<LayoutCol class="layers" ondragleave={() => (dragInPanel = false)}>
|
||||
<LayoutRow class="control-bar" scrollableX={true}>
|
||||
<WidgetLayout layout={layersPanelControlBarLeftLayout} />
|
||||
<Separator />
|
||||
|
@ -498,10 +496,10 @@
|
|||
styles={{ cursor: layerToClipUponClick && layerToClipAltKeyPressed && layerToClipUponClick.entry.clippable ? "alias" : "auto" }}
|
||||
data-layer-panel
|
||||
bind:this={list}
|
||||
on:click={() => deselectAllLayers()}
|
||||
on:dragover={updateInsertLine}
|
||||
on:dragend={drop}
|
||||
on:drop={drop}
|
||||
onclick={() => deselectAllLayers()}
|
||||
ondragover={updateInsertLine}
|
||||
ondragend={drop}
|
||||
ondrop={drop}
|
||||
>
|
||||
{#each layers as listing, index}
|
||||
{@const selected = fakeHighlightOfNotYetSelectedLayerBeingDragged !== undefined ? fakeHighlightOfNotYetSelectedLayerBeingDragged === listing.entry.id : listing.entry.selected}
|
||||
|
@ -519,8 +517,8 @@
|
|||
data-index={index}
|
||||
tooltip={listing.entry.tooltip}
|
||||
{draggable}
|
||||
on:dragstart={(e) => draggable && dragStart(e, listing)}
|
||||
on:click={(e) => selectLayerWithModifiers(e, listing)}
|
||||
ondragstart={(e) => draggable && dragStart(e, listing)}
|
||||
onclick={(e) => selectLayerWithModifiers(e, listing)}
|
||||
>
|
||||
{#if listing.entry.childrenAllowed}
|
||||
<button
|
||||
|
@ -530,7 +528,7 @@
|
|||
title={listing.entry.expanded
|
||||
? "Collapse (Click) / Collapse All (Alt Click)"
|
||||
: `Expand (Click) / Expand All (Alt Click)${listing.entry.ancestorOfSelected ? "\n(A selected layer is contained within)" : ""}`}
|
||||
on:click={(e) => handleExpandArrowClickWithModifiers(e, listing.entry.id)}
|
||||
onclick={(e) => handleExpandArrowClickWithModifiers(e, listing.entry.id)}
|
||||
tabindex="0"
|
||||
></button>
|
||||
{:else}
|
||||
|
@ -547,24 +545,26 @@
|
|||
{#if listing.entry.name === "Artboard"}
|
||||
<IconLabel icon="Artboard" class={"layer-type-icon"} />
|
||||
{/if}
|
||||
<LayoutRow class="layer-name" on:dblclick={() => onEditLayerName(listing)}>
|
||||
<LayoutRow class="layer-name" ondblclick={() => onEditLayerName(listing)}>
|
||||
<input
|
||||
data-text-input
|
||||
type="text"
|
||||
value={listing.entry.alias}
|
||||
placeholder={listing.entry.name}
|
||||
disabled={!listing.editingName}
|
||||
on:blur={() => onEditLayerNameDeselect(listing)}
|
||||
on:keydown={(e) => e.key === "Escape" && onEditLayerNameDeselect(listing)}
|
||||
on:keydown={(e) => e.key === "Enter" && onEditLayerNameChange(listing, e)}
|
||||
on:change={(e) => onEditLayerNameChange(listing, e)}
|
||||
onblur={() => onEditLayerNameDeselect(listing)}
|
||||
onkeydown={(e) => {
|
||||
e.key === "Escape" && onEditLayerNameDeselect(listing);
|
||||
e.key === "Enter" && onEditLayerNameChange(listing, e);
|
||||
}}
|
||||
onchange={(e) => onEditLayerNameChange(listing, e)}
|
||||
/>
|
||||
</LayoutRow>
|
||||
{#if !listing.entry.unlocked || !listing.entry.parentsUnlocked}
|
||||
<IconButton
|
||||
class={"status-toggle"}
|
||||
classes={{ inherited: !listing.entry.parentsUnlocked }}
|
||||
action={(e) => (toggleLayerLock(listing.entry.id), e?.stopPropagation())}
|
||||
onclick={(e) => (toggleLayerLock(listing.entry.id), e?.stopPropagation())}
|
||||
size={24}
|
||||
icon={listing.entry.unlocked ? "PadlockUnlocked" : "PadlockLocked"}
|
||||
hoverIcon={listing.entry.unlocked ? "PadlockLocked" : "PadlockUnlocked"}
|
||||
|
@ -574,7 +574,7 @@
|
|||
<IconButton
|
||||
class={"status-toggle"}
|
||||
classes={{ inherited: !listing.entry.parentsVisible }}
|
||||
action={(e) => (toggleNodeVisibilityLayerPanel(listing.entry.id), e?.stopPropagation())}
|
||||
onclick={(e) => (toggleNodeVisibilityLayerPanel(listing.entry.id), e?.stopPropagation())}
|
||||
size={24}
|
||||
icon={listing.entry.visible ? "EyeVisible" : "EyeHidden"}
|
||||
hoverIcon={listing.entry.visible ? "EyeHide" : "EyeShow"}
|
||||
|
@ -584,7 +584,7 @@
|
|||
{/each}
|
||||
</LayoutCol>
|
||||
{#if draggingData && !draggingData.highlightFolder && dragInPanel}
|
||||
<div class="insert-mark" style:left={`${4 + draggingData.insertDepth * 16}px`} style:top={`${draggingData.markerHeight}px`} />
|
||||
<div class="insert-mark" style:left={`${4 + draggingData.insertDepth * 16}px`} style:top={`${draggingData.markerHeight}px`}></div>
|
||||
{/if}
|
||||
</LayoutRow>
|
||||
<LayoutRow class="bottom-bar" scrollableX={true}>
|
||||
|
|
|
@ -2,19 +2,21 @@
|
|||
import { getContext, onMount } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import { defaultWidgetLayout, patchWidgetLayout, UpdatePropertyPanelSectionsLayout } from "@graphite/messages";
|
||||
import { defaultWidgetLayout, patchWidgetLayout, UpdatePropertyPanelSectionsLayout } from "@graphite/messages.svelte";
|
||||
import type { WidgetLayout as WidgetLayoutState } from "@graphite/messages.svelte";
|
||||
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
|
||||
|
||||
|
||||
const editor = getContext<Editor>("editor");
|
||||
|
||||
let propertiesSectionsLayout = defaultWidgetLayout();
|
||||
let propertiesSectionsLayout = $state<WidgetLayoutState>(defaultWidgetLayout());
|
||||
|
||||
onMount(() => {
|
||||
editor.subscriptions.subscribeJsMessage(UpdatePropertyPanelSectionsLayout, (updatePropertyPanelSectionsLayout) => {
|
||||
patchWidgetLayout(propertiesSectionsLayout, updatePropertyPanelSectionsLayout);
|
||||
propertiesSectionsLayout = propertiesSectionsLayout;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
import { fade } from "svelte/transition";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { Node } from "@graphite/messages";
|
||||
import type { FrontendNode, FrontendGraphInput, FrontendGraphOutput } from "@graphite/messages";
|
||||
import type { Node } from "@graphite/messages.svelte";
|
||||
import { type FrontendNode, type FrontendGraphInput, FrontendGraphOutput, } from "@graphite/messages.svelte";
|
||||
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
|
||||
import type { IconName } from "@graphite/utility-functions/icons";
|
||||
|
||||
|
@ -26,21 +26,17 @@
|
|||
const editor = getContext<Editor>("editor");
|
||||
const nodeGraph = getContext<NodeGraphState>("nodeGraph");
|
||||
|
||||
let graph: HTMLDivElement | undefined;
|
||||
let graph: HTMLDivElement | undefined = $state();
|
||||
|
||||
// Key value is node id + input/output index
|
||||
// Imports/Export are stored at a key value of 0
|
||||
|
||||
$: gridSpacing = calculateGridSpacing($nodeGraph.transform.scale);
|
||||
$: dotRadius = 1 + Math.floor($nodeGraph.transform.scale - 0.5 + 0.001) / 2;
|
||||
let inputElement = $state<HTMLInputElement>();
|
||||
let hoveringImportIndex = $state<number>();
|
||||
let hoveringExportIndex = $state<number>()
|
||||
|
||||
let inputElement: HTMLInputElement;
|
||||
let hoveringImportIndex: number | undefined = undefined;
|
||||
let hoveringExportIndex: number | undefined = undefined;
|
||||
|
||||
let editingNameImportIndex: number | undefined = undefined;
|
||||
let editingNameExportIndex: number | undefined = undefined;
|
||||
let editingNameText = "";
|
||||
let editingNameImportIndex = $state<number>();
|
||||
let editingNameExportIndex = $state<number>();
|
||||
let editingNameText = $state<string>("");
|
||||
let nodeValues: FrontendNode[] = $state([]);
|
||||
|
||||
function exportsToEdgeTextInputWidth() {
|
||||
let exportTextDivs = document.querySelectorAll(`[data-export-text-edge]`);
|
||||
|
@ -204,7 +200,7 @@
|
|||
}
|
||||
|
||||
function primaryOutputConnectedToLayer(node: FrontendNode): boolean {
|
||||
let firstConnectedNode = Array.from($nodeGraph.nodes.values()).find((n) =>
|
||||
let firstConnectedNode = nodeValues.find((n) =>
|
||||
node.primaryOutput?.connectedTo.some((connector) => {
|
||||
if ((connector as Node).nodeId === undefined) return false;
|
||||
if (connector.index !== 0n) return false;
|
||||
|
@ -215,7 +211,7 @@
|
|||
}
|
||||
|
||||
function primaryInputConnectedToLayer(node: FrontendNode): boolean {
|
||||
const connectedNode = Array.from($nodeGraph.nodes.values()).find((n) => {
|
||||
const connectedNode = nodeValues.find((n) => {
|
||||
if ((node.primaryInput?.connectedTo as Node) === undefined) return false;
|
||||
return n.id === (node.primaryInput?.connectedTo as Node).nodeId;
|
||||
});
|
||||
|
@ -230,6 +226,16 @@
|
|||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
$effect.pre(() => {
|
||||
nodeValues = Array.from($nodeGraph.nodes.values());
|
||||
});
|
||||
|
||||
// Key value is node id + input/output index
|
||||
// Imports/Export are stored at a key value of 0
|
||||
|
||||
let gridSpacing = $derived(calculateGridSpacing($nodeGraph.transform.scale));
|
||||
let dotRadius = $derived(1 + Math.floor($nodeGraph.transform.scale - 0.5 + 0.001) / 2);
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -252,9 +258,9 @@
|
|||
}}
|
||||
>
|
||||
{#if typeof $nodeGraph.contextMenuInformation.contextMenuData === "string" && $nodeGraph.contextMenuInformation.contextMenuData === "CreateNode"}
|
||||
<NodeCatalog on:selectNodeType={(e) => createNode(e.detail)} />
|
||||
<NodeCatalog onselectNodeType={(e) => createNode(e)} />
|
||||
{:else if $nodeGraph.contextMenuInformation.contextMenuData && "compatibleType" in $nodeGraph.contextMenuInformation.contextMenuData}
|
||||
<NodeCatalog initialSearchTerm={$nodeGraph.contextMenuInformation.contextMenuData.compatibleType || ""} on:selectNodeType={(e) => createNode(e.detail)} />
|
||||
<NodeCatalog initialSearchTerm={$nodeGraph.contextMenuInformation.contextMenuData.compatibleType || ""} onselectNodeType={(e) => createNode(e)} />
|
||||
{:else}
|
||||
{@const contextMenuData = $nodeGraph.contextMenuInformation.contextMenuData}
|
||||
<LayoutRow class="toggle-layer-or-node">
|
||||
|
@ -282,7 +288,7 @@
|
|||
</LayoutRow>
|
||||
<Separator type="Section" direction="Vertical" />
|
||||
<LayoutRow class="merge-selected-nodes">
|
||||
<TextButton label="Merge Selected Nodes" action={() => editor.handle.mergeSelectedNodes()} />
|
||||
<TextButton label="Merge Selected Nodes" onclick={() => editor.handle.mergeSelectedNodes()} />
|
||||
</LayoutRow>
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
|
@ -334,102 +340,68 @@
|
|||
|
||||
<!-- Import and Export ports -->
|
||||
<div class="imports-and-exports" style:transform-origin={`0 0`} style:transform={`translate(${$nodeGraph.transform.x}px, ${$nodeGraph.transform.y}px) scale(${$nodeGraph.transform.scale})`}>
|
||||
{#each $nodeGraph.imports as { outputMetadata, position }, index}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
class="port"
|
||||
data-port="output"
|
||||
data-datatype={outputMetadata.dataType}
|
||||
style:--data-color={`var(--color-data-${outputMetadata.dataType.toLowerCase()})`}
|
||||
style:--data-color-dim={`var(--color-data-${outputMetadata.dataType.toLowerCase()}-dim)`}
|
||||
style:--offset-left={position.x / 24}
|
||||
style:--offset-top={position.y / 24}
|
||||
>
|
||||
<title>{`${dataTypeTooltip(outputMetadata)}\n\n${outputConnectedToText(outputMetadata)}`}</title>
|
||||
{#if outputMetadata.connectedTo !== undefined}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
</svg>
|
||||
{#each $nodeGraph.imports as { outputMetadata, position }, index}
|
||||
{@render port("port", "output", outputMetadata, position.x / 24, position.y / 24)}
|
||||
<div
|
||||
class="edit-import-export import"
|
||||
onpointerenter={() => (hoveringImportIndex = index)}
|
||||
onpointerleave={() => (hoveringImportIndex = undefined)}
|
||||
style:--offset-left={position.x / 24}
|
||||
style:--offset-top={position.y / 24}
|
||||
>
|
||||
{#if editingNameImportIndex == index}
|
||||
<input
|
||||
class="import-text-input"
|
||||
type="text"
|
||||
style:width={importsToEdgeTextInputWidth()}
|
||||
bind:this={inputElement}
|
||||
bind:value={editingNameText}
|
||||
onblur={setEditingImportName}
|
||||
onkeydown={(e) => e.key === "Enter" && setEditingImportName(e)}
|
||||
/>
|
||||
{:else}
|
||||
<p class="import-text" ondblclick={() => setEditingImportNameIndex(index, outputMetadata.name)}>{outputMetadata.name}</p>
|
||||
{/if}
|
||||
{#if hoveringImportIndex === index || editingNameImportIndex === index}
|
||||
<IconButton
|
||||
size={16}
|
||||
icon={"Remove"}
|
||||
class="remove-button-import"
|
||||
data-index={index}
|
||||
data-import-text-edge
|
||||
onclick={() => {
|
||||
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
|
||||
}}
|
||||
/>
|
||||
<div class="reorder-drag-grip" title="Reorder this import"></div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div
|
||||
class="edit-import-export import"
|
||||
on:pointerenter={() => (hoveringImportIndex = index)}
|
||||
on:pointerleave={() => (hoveringImportIndex = undefined)}
|
||||
style:--offset-left={position.x / 24}
|
||||
style:--offset-top={position.y / 24}
|
||||
>
|
||||
{#if editingNameImportIndex == index}
|
||||
<input
|
||||
class="import-text-input"
|
||||
type="text"
|
||||
style:width={importsToEdgeTextInputWidth()}
|
||||
bind:this={inputElement}
|
||||
bind:value={editingNameText}
|
||||
on:blur={setEditingImportName}
|
||||
on:keydown={(e) => e.key === "Enter" && setEditingImportName(e)}
|
||||
/>
|
||||
{:else}
|
||||
<p class="import-text" on:dblclick={() => setEditingImportNameIndex(index, outputMetadata.name)}>{outputMetadata.name}</p>
|
||||
{/if}
|
||||
{#if hoveringImportIndex === index || editingNameImportIndex === index}
|
||||
<IconButton
|
||||
size={16}
|
||||
icon={"Remove"}
|
||||
class="remove-button-import"
|
||||
data-index={index}
|
||||
data-import-text-edge
|
||||
action={() => {
|
||||
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
|
||||
}}
|
||||
/>
|
||||
<div class="reorder-drag-grip" title="Reorder this import"></div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{#if $nodeGraph.reorderImportIndex !== undefined}
|
||||
{@const position = {
|
||||
x: Number($nodeGraph.imports[0].position.x),
|
||||
y: Number($nodeGraph.imports[0].position.y) + Number($nodeGraph.reorderImportIndex) * 24,
|
||||
}}
|
||||
<div class="reorder-bar" style:--offset-left={(position.x - 48) / 24} style:--offset-top={(position.y - 4) / 24} />
|
||||
<div class="reorder-bar" style:--offset-left={(position.x - 48) / 24} style:--offset-top={(position.y - 4) / 24}></div>
|
||||
{/if}
|
||||
{#if $nodeGraph.addImport !== undefined}
|
||||
<div class="plus" style:--offset-left={$nodeGraph.addImport.x / 24} style:--offset-top={$nodeGraph.addImport.y / 24}>
|
||||
<IconButton
|
||||
size={24}
|
||||
icon="Add"
|
||||
action={() => {
|
||||
onclick={() => {
|
||||
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#each $nodeGraph.exports as { inputMetadata, position }, index}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
class="port"
|
||||
data-port="input"
|
||||
data-datatype={inputMetadata.dataType}
|
||||
style:--data-color={`var(--color-data-${inputMetadata.dataType.toLowerCase()})`}
|
||||
style:--data-color-dim={`var(--color-data-${inputMetadata.dataType.toLowerCase()}-dim)`}
|
||||
style:--offset-left={position.x / 24}
|
||||
style:--offset-top={position.y / 24}
|
||||
>
|
||||
<title>{`${dataTypeTooltip(inputMetadata)}\n\n${inputConnectedToText(inputMetadata)}`}</title>
|
||||
{#if inputMetadata.connectedTo !== undefined}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
</svg>
|
||||
{@render port("port", "input", inputMetadata, position.x / 24, position.y / 24)}
|
||||
<div
|
||||
class="edit-import-export export"
|
||||
on:pointerenter={() => (hoveringExportIndex = index)}
|
||||
on:pointerleave={() => (hoveringExportIndex = undefined)}
|
||||
onpointerenter={() => (hoveringExportIndex = index)}
|
||||
onpointerleave={() => (hoveringExportIndex = undefined)}
|
||||
style:--offset-left={position.x / 24}
|
||||
style:--offset-top={position.y / 24}
|
||||
>
|
||||
|
@ -441,7 +413,7 @@
|
|||
class="remove-button-export"
|
||||
data-index={index}
|
||||
data-export-text-edge
|
||||
action={() => {
|
||||
onclick={() => {
|
||||
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
|
||||
}}
|
||||
/>
|
||||
|
@ -452,11 +424,11 @@
|
|||
style:width={exportsToEdgeTextInputWidth()}
|
||||
bind:this={inputElement}
|
||||
bind:value={editingNameText}
|
||||
on:blur={setEditingExportName}
|
||||
on:keydown={(e) => e.key === "Enter" && setEditingExportName(e)}
|
||||
onblur={setEditingExportName}
|
||||
onkeydown={(e) => e.key === "Enter" && setEditingExportName(e)}
|
||||
/>
|
||||
{:else}
|
||||
<p class="export-text" on:dblclick={() => setEditingExportNameIndex(index, inputMetadata.name)}>{inputMetadata.name}</p>
|
||||
<p class="export-text" ondblclick={() => setEditingExportNameIndex(index, inputMetadata.name)}>{inputMetadata.name}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
@ -465,14 +437,14 @@
|
|||
x: Number($nodeGraph.exports[0].position.x),
|
||||
y: Number($nodeGraph.exports[0].position.y) + Number($nodeGraph.reorderExportIndex) * 24,
|
||||
}}
|
||||
<div class="reorder-bar" style:--offset-left={position.x / 24} style:--offset-top={(position.y - 4) / 24} />
|
||||
<div class="reorder-bar" style:--offset-left={position.x / 24} style:--offset-top={(position.y - 4) / 24}></div>
|
||||
{/if}
|
||||
{#if $nodeGraph.addExport !== undefined}
|
||||
<div class="plus" style:--offset-left={$nodeGraph.addExport.x / 24} style:--offset-top={$nodeGraph.addExport.y / 24}>
|
||||
<IconButton
|
||||
size={24}
|
||||
icon={"Add"}
|
||||
action={() => {
|
||||
onclick={() => {
|
||||
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
|
||||
}}
|
||||
/>
|
||||
|
@ -564,22 +536,7 @@
|
|||
<!-- Layer input port (from left) -->
|
||||
{#if node.exposedInputs.length > 0}
|
||||
<div class="input ports">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
class="port"
|
||||
data-port="input"
|
||||
data-datatype={stackDataInput.dataType}
|
||||
style:--data-color={`var(--color-data-${stackDataInput.dataType.toLowerCase()})`}
|
||||
style:--data-color-dim={`var(--color-data-${stackDataInput.dataType.toLowerCase()}-dim)`}
|
||||
>
|
||||
<title>{`${dataTypeTooltip(stackDataInput)}\n\n${validTypesText(stackDataInput)}\n\n${inputConnectedToText(stackDataInput)}`}</title>
|
||||
{#if stackDataInput.connectedTo !== undefined}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
</svg>
|
||||
{@render port("port", "input", stackDataInput)}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="details">
|
||||
|
@ -592,7 +549,7 @@
|
|||
data-visibility-button
|
||||
size={24}
|
||||
icon={node.visible ? "EyeVisible" : "EyeHidden"}
|
||||
action={() => {
|
||||
onclick={() => {
|
||||
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
|
||||
}}
|
||||
tooltip={node.visible ? "Visible" : "Hidden"}
|
||||
|
@ -682,81 +639,19 @@
|
|||
<!-- Input ports -->
|
||||
<div class="input ports">
|
||||
{#if node.primaryInput?.dataType}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
class="port primary-port"
|
||||
data-port="input"
|
||||
data-datatype={node.primaryInput?.dataType}
|
||||
style:--data-color={`var(--color-data-${node.primaryInput.dataType.toLowerCase()})`}
|
||||
style:--data-color-dim={`var(--color-data-${node.primaryInput.dataType.toLowerCase()}-dim)`}
|
||||
>
|
||||
<title>{`${dataTypeTooltip(node.primaryInput)}\n\n${validTypesText(node.primaryInput)}\n\n${inputConnectedToText(node.primaryInput)}`}</title>
|
||||
{#if node.primaryInput.connectedTo !== undefined}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
</svg>
|
||||
{@render port("port primary-port", "input", node.primaryInput)}
|
||||
{/if}
|
||||
{#each node.exposedInputs as secondary, index}
|
||||
{#if index < node.exposedInputs.length}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
class="port"
|
||||
data-port="input"
|
||||
data-datatype={secondary.dataType}
|
||||
style:--data-color={`var(--color-data-${secondary.dataType.toLowerCase()})`}
|
||||
style:--data-color-dim={`var(--color-data-${secondary.dataType.toLowerCase()}-dim)`}
|
||||
>
|
||||
<title>{`${dataTypeTooltip(secondary)}\n\n${validTypesText(secondary)}\n\n${inputConnectedToText(secondary)}`}</title>
|
||||
{#if secondary.connectedTo !== undefined}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
</svg>
|
||||
{@render port("port", "input", secondary)}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<!-- Output ports -->
|
||||
<div class="output ports">
|
||||
{#if node.primaryOutput}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
class="port primary-port"
|
||||
data-port="output"
|
||||
data-datatype={node.primaryOutput.dataType}
|
||||
style:--data-color={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()})`}
|
||||
style:--data-color-dim={`var(--color-data-${node.primaryOutput.dataType.toLowerCase()}-dim)`}
|
||||
>
|
||||
<title>{`${dataTypeTooltip(node.primaryOutput)}\n\n${outputConnectedToText(node.primaryOutput)}`}</title>
|
||||
{#if node.primaryOutput.connectedTo !== undefined}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
</svg>
|
||||
{/if}
|
||||
{#each node.exposedOutputs as secondary}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
class="port"
|
||||
data-port="output"
|
||||
data-datatype={secondary.dataType}
|
||||
style:--data-color={`var(--color-data-${secondary.dataType.toLowerCase()})`}
|
||||
style:--data-color-dim={`var(--color-data-${secondary.dataType.toLowerCase()}-dim)`}
|
||||
>
|
||||
<title>{`${dataTypeTooltip(secondary)}\n\n${outputConnectedToText(secondary)}`}</title>
|
||||
{#if secondary.connectedTo !== undefined}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
</svg>
|
||||
{@render port("port primary-port", "output", node.primaryOutput)}
|
||||
{#each node.exposedOutputs as secondary, outputIndex}
|
||||
{@render port("port", "output", secondary)}
|
||||
{/each}
|
||||
</div>
|
||||
<svg class="border-mask" width="0" height="0">
|
||||
|
@ -786,6 +681,34 @@
|
|||
></div>
|
||||
{/if}
|
||||
|
||||
{#snippet port(className: string, dataPort: string, node?: FrontendGraphInput | FrontendGraphOutput, offsetLeft?: number, offsetTop?: number)}
|
||||
{#if node}
|
||||
{@const color = node.dataType.toLowerCase()}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 8 8"
|
||||
class={className}
|
||||
data-port={dataPort}
|
||||
data-datatype={node.dataType}
|
||||
style:--data-color={`var(--color-data-${color})`}
|
||||
style:--data-color-dim={`var(--color-data-${color}-dim)`}
|
||||
style:--offset-left={offsetLeft}
|
||||
style:--offset-top={offsetTop}
|
||||
>
|
||||
{#if node instanceof FrontendGraphOutput}
|
||||
<title>{`${dataTypeTooltip(node)}\n\n${outputConnectedToText(node)}`}</title>
|
||||
{:else}
|
||||
<title>{`${dataTypeTooltip(node)}\n\n${validTypesText(node)}\n\n${inputConnectedToText(node)}`}</title>
|
||||
{/if}
|
||||
{#if node.connectedTo !== undefined}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
|
||||
{:else}
|
||||
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
|
||||
{/if}
|
||||
</svg>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
<style lang="scss" global>
|
||||
.graph {
|
||||
position: relative;
|
||||
|
|
|
@ -1,15 +1,19 @@
|
|||
<script lang="ts">
|
||||
import { isWidgetSpanColumn, isWidgetSpanRow, isWidgetSection, type WidgetLayout, isWidgetTable } from "@graphite/messages";
|
||||
import { isWidgetSpanColumn, isWidgetSpanRow, isWidgetSection, type WidgetLayout, isWidgetTable } from "@graphite/messages.svelte";
|
||||
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
import WidgetSection from "@graphite/components/widgets/WidgetSection.svelte";
|
||||
import WidgetSpan from "@graphite/components/widgets/WidgetSpan.svelte";
|
||||
import WidgetTable from "@graphite/components/widgets/WidgetTable.svelte";
|
||||
|
||||
export let layout: WidgetLayout;
|
||||
let className = "";
|
||||
export { className as class };
|
||||
export let classes: Record<string, boolean> = {};
|
||||
|
||||
interface Props {
|
||||
layout: WidgetLayout;
|
||||
class?: string;
|
||||
classes?: Record<string, boolean>;
|
||||
}
|
||||
|
||||
let { layout, class: className = "", classes = {} }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#each layout.layout as layoutGroup}
|
||||
|
|
|
@ -1,37 +1,52 @@
|
|||
<script lang="ts">
|
||||
import WidgetSection from './WidgetSection.svelte';
|
||||
|
||||
import { getContext } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import { isWidgetSpanRow, isWidgetSpanColumn, isWidgetSection, type WidgetSection as WidgetSectionFromJsMessages } from "@graphite/messages";
|
||||
import { isWidgetSpanRow, isWidgetSpanColumn, isWidgetSection, type WidgetSection as WidgetSectionFromJsMessages } from "@graphite/messages.svelte";
|
||||
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
import WidgetSpan from "@graphite/components/widgets/WidgetSpan.svelte";
|
||||
|
||||
export let widgetData: WidgetSectionFromJsMessages;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export let layoutTarget: any; // TODO: Give type
|
||||
|
||||
|
||||
let className = "";
|
||||
export { className as class };
|
||||
export let classes: Record<string, boolean> = {};
|
||||
|
||||
interface Props {
|
||||
widgetData: WidgetSectionFromJsMessages;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
layoutTarget: any; // TODO: Give type
|
||||
class?: string;
|
||||
classes?: Record<string, boolean>;
|
||||
}
|
||||
|
||||
let expanded = true;
|
||||
let {
|
||||
widgetData,
|
||||
layoutTarget,
|
||||
class: className = "",
|
||||
classes = {}
|
||||
}: Props = $props();
|
||||
|
||||
let expanded = $state(true);
|
||||
|
||||
const editor = getContext<Editor>("editor");
|
||||
</script>
|
||||
|
||||
<!-- TODO: Implement collapsable sections with properties system -->
|
||||
<LayoutCol class={`widget-section ${className}`.trim()} {classes}>
|
||||
<button class="header" class:expanded on:click|stopPropagation={() => (expanded = !expanded)} tabindex="0">
|
||||
<div class="expand-arrow" />
|
||||
<button class="header" class:expanded onclick={(event) => {
|
||||
event.stopPropagation();
|
||||
expanded = !expanded;
|
||||
}} tabindex="0">
|
||||
<div class="expand-arrow"></div>
|
||||
<TextLabel tooltip={widgetData.description} bold={true}>{widgetData.name}</TextLabel>
|
||||
<IconButton
|
||||
icon={widgetData.pinned ? "PinActive" : "PinInactive"}
|
||||
tooltip={widgetData.pinned ? "Unpin this node so it's no longer shown here when nothing is selected" : "Pin this node so it's shown here when nothing is selected"}
|
||||
size={24}
|
||||
action={(e) => {
|
||||
onclick={(e) => {
|
||||
editor.handle.setNodePinned(widgetData.id, !widgetData.pinned);
|
||||
e?.stopPropagation();
|
||||
}}
|
||||
|
@ -41,7 +56,7 @@
|
|||
icon={"Trash"}
|
||||
tooltip={"Delete this node from the layer chain"}
|
||||
size={24}
|
||||
action={(e) => {
|
||||
onclick={(e) => {
|
||||
editor.handle.deleteNode(widgetData.id);
|
||||
e?.stopPropagation();
|
||||
}}
|
||||
|
@ -52,7 +67,7 @@
|
|||
hoverIcon={widgetData.visible ? "EyeHide" : "EyeShow"}
|
||||
tooltip={widgetData.visible ? "Hide this node" : "Show this node"}
|
||||
size={24}
|
||||
action={(e) => {
|
||||
onclick={(e) => {
|
||||
editor.handle.toggleNodeVisibilityLayerPanel(widgetData.id);
|
||||
e?.stopPropagation();
|
||||
}}
|
||||
|
@ -67,7 +82,7 @@
|
|||
{:else if isWidgetSpanColumn(layoutGroup)}
|
||||
<TextLabel styles={{ color: "#d6536e" }}>Error: The WidgetSpan used here should be a row not a column</TextLabel>
|
||||
{:else if isWidgetSection(layoutGroup)}
|
||||
<svelte:self widgetData={layoutGroup} {layoutTarget} />
|
||||
<WidgetSection widgetData={layoutGroup} {layoutTarget} />
|
||||
{:else}
|
||||
<TextLabel styles={{ color: "#d6536e" }}>Error: The widget that belongs here has an invalid layout group type</TextLabel>
|
||||
{/if}
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
import { getContext } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { Widget, WidgetSpanColumn, WidgetSpanRow } from "@graphite/messages";
|
||||
import { narrowWidgetProps, isWidgetSpanColumn, isWidgetSpanRow } from "@graphite/messages";
|
||||
import type { Widget, WidgetSpanColumn, WidgetSpanRow } from "@graphite/messages.svelte";
|
||||
import { narrowWidgetProps, isWidgetSpanColumn, isWidgetSpanRow } from "@graphite/messages.svelte";
|
||||
import { debouncer } from "@graphite/utility-functions/debounce";
|
||||
|
||||
import NodeCatalog from "@graphite/components/floating-menus/NodeCatalog.svelte";
|
||||
|
@ -30,21 +30,21 @@
|
|||
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
|
||||
|
||||
const editor = getContext<Editor>("editor");
|
||||
|
||||
interface Props {
|
||||
widgetData: WidgetSpanRow | WidgetSpanColumn;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
layoutTarget: any;
|
||||
class?: string;
|
||||
classes?: Record<string, boolean>;
|
||||
}
|
||||
|
||||
export let widgetData: WidgetSpanRow | WidgetSpanColumn;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export let layoutTarget: any;
|
||||
|
||||
let className = "";
|
||||
export { className as class };
|
||||
export let classes: Record<string, boolean> = {};
|
||||
|
||||
$: extraClasses = Object.entries(classes)
|
||||
.flatMap(([className, stateName]) => (stateName ? [className] : []))
|
||||
.join(" ");
|
||||
|
||||
$: direction = watchDirection(widgetData);
|
||||
$: widgets = watchWidgets(widgetData);
|
||||
let {
|
||||
widgetData,
|
||||
layoutTarget,
|
||||
class: className = "",
|
||||
classes = {}
|
||||
}: Props = $props();
|
||||
|
||||
function watchDirection(widgetData: WidgetSpanRow | WidgetSpanColumn): "row" | "column" | undefined {
|
||||
if (isWidgetSpanRow(widgetData)) return "row";
|
||||
|
@ -77,6 +77,12 @@
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return Object.fromEntries(Object.entries(props).filter((entry) => !exclusions.includes(entry[0]))) as any;
|
||||
}
|
||||
let extraClasses = $derived(Object.entries(classes)
|
||||
.flatMap(([className, stateName]) => (stateName ? [className] : []))
|
||||
.join(" "));
|
||||
|
||||
let direction = $derived(watchDirection(widgetData));
|
||||
let widgets = $derived(watchWidgets(widgetData));
|
||||
</script>
|
||||
|
||||
<!-- TODO: Refactor this component to use `<svelte:component this={attributesObject} />` to avoid all the separate conditional components -->
|
||||
|
@ -85,32 +91,32 @@
|
|||
{#each widgets as component, index}
|
||||
{@const checkboxInput = narrowWidgetProps(component.props, "CheckboxInput")}
|
||||
{#if checkboxInput}
|
||||
<CheckboxInput {...exclude(checkboxInput)} on:checked={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
<CheckboxInput {...exclude(checkboxInput)} onchecked={(detail) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
{/if}
|
||||
{@const colorInput = narrowWidgetProps(component.props, "ColorInput")}
|
||||
{#if colorInput}
|
||||
<ColorInput {...exclude(colorInput)} on:value={({ detail }) => widgetValueUpdate(index, detail)} on:startHistoryTransaction={() => widgetValueCommit(index, colorInput.value)} />
|
||||
<ColorInput {...exclude(colorInput)} onvalue={(detail) => widgetValueUpdate(index, detail)} onstartHistoryTransaction={() => widgetValueCommit(index, colorInput.value)} />
|
||||
{/if}
|
||||
{@const curvesInput = narrowWidgetProps(component.props, "CurveInput")}
|
||||
{#if curvesInput}
|
||||
<CurveInput {...exclude(curvesInput)} on:value={({ detail }) => debouncer((value) => widgetValueCommitAndUpdate(index, value), { debounceTime: 120 }).debounceUpdateValue(detail)} />
|
||||
<CurveInput {...exclude(curvesInput)} onvalue={(detail) => debouncer((value) => widgetValueCommitAndUpdate(index, value), { debounceTime: 120 }).debounceUpdateValue(detail)} />
|
||||
{/if}
|
||||
{@const dropdownInput = narrowWidgetProps(component.props, "DropdownInput")}
|
||||
{#if dropdownInput}
|
||||
<DropdownInput
|
||||
{...exclude(dropdownInput)}
|
||||
on:hoverInEntry={({ detail }) => {
|
||||
onhoverInEntry={(detail) => {
|
||||
return widgetValueUpdate(index, detail);
|
||||
}}
|
||||
on:hoverOutEntry={({ detail }) => {
|
||||
onhoverOutEntry={(detail) => {
|
||||
return widgetValueUpdate(index, detail);
|
||||
}}
|
||||
on:selectedIndex={({ detail }) => widgetValueCommitAndUpdate(index, detail)}
|
||||
onselectedIndex={(detail) => widgetValueCommitAndUpdate(index, detail)}
|
||||
/>
|
||||
{/if}
|
||||
{@const fontInput = narrowWidgetProps(component.props, "FontInput")}
|
||||
{#if fontInput}
|
||||
<FontInput {...exclude(fontInput)} on:changeFont={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
<FontInput {...exclude(fontInput)} onchangeFont={(detail) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
{/if}
|
||||
{@const parameterExposeButton = narrowWidgetProps(component.props, "ParameterExposeButton")}
|
||||
{#if parameterExposeButton}
|
||||
|
@ -118,7 +124,7 @@
|
|||
{/if}
|
||||
{@const iconButton = narrowWidgetProps(component.props, "IconButton")}
|
||||
{#if iconButton}
|
||||
<IconButton {...exclude(iconButton)} action={() => widgetValueCommitAndUpdate(index, undefined)} />
|
||||
<IconButton {...exclude(iconButton)} onclick={() => widgetValueCommitAndUpdate(index, undefined)} />
|
||||
{/if}
|
||||
{@const iconLabel = narrowWidgetProps(component.props, "IconLabel")}
|
||||
{#if iconLabel}
|
||||
|
@ -130,21 +136,21 @@
|
|||
{/if}
|
||||
{@const nodeCatalog = narrowWidgetProps(component.props, "NodeCatalog")}
|
||||
{#if nodeCatalog}
|
||||
<NodeCatalog {...exclude(nodeCatalog)} on:selectNodeType={(e) => widgetValueCommitAndUpdate(index, e.detail)} />
|
||||
<NodeCatalog {...exclude(nodeCatalog)} onselectNodeType={(e) => widgetValueCommitAndUpdate(index, e)} />
|
||||
{/if}
|
||||
{@const numberInput = narrowWidgetProps(component.props, "NumberInput")}
|
||||
{#if numberInput}
|
||||
<NumberInput
|
||||
{...exclude(numberInput)}
|
||||
on:value={({ detail }) => debouncer((value) => widgetValueUpdate(index, value)).debounceUpdateValue(detail)}
|
||||
on:startHistoryTransaction={() => widgetValueCommit(index, numberInput.value)}
|
||||
onvalue={(detail) => debouncer((value) => widgetValueUpdate(index, value)).debounceUpdateValue(detail)}
|
||||
onstartHistoryTransaction={() => widgetValueCommit(index, numberInput.value)}
|
||||
incrementCallbackIncrease={() => widgetValueCommitAndUpdate(index, "Increment")}
|
||||
incrementCallbackDecrease={() => widgetValueCommitAndUpdate(index, "Decrement")}
|
||||
/>
|
||||
{/if}
|
||||
{@const referencePointInput = narrowWidgetProps(component.props, "ReferencePointInput")}
|
||||
{#if referencePointInput}
|
||||
<ReferencePointInput {...exclude(referencePointInput)} on:value={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
<ReferencePointInput {...exclude(referencePointInput)} onvalue={(detail) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
{/if}
|
||||
{@const popoverButton = narrowWidgetProps(component.props, "PopoverButton")}
|
||||
{#if popoverButton}
|
||||
|
@ -154,7 +160,7 @@
|
|||
{/if}
|
||||
{@const radioInput = narrowWidgetProps(component.props, "RadioInput")}
|
||||
{#if radioInput}
|
||||
<RadioInput {...exclude(radioInput)} on:selectedIndex={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
<RadioInput {...exclude(radioInput)} onselect={(detail) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
{/if}
|
||||
{@const separator = narrowWidgetProps(component.props, "Separator")}
|
||||
{#if separator}
|
||||
|
@ -166,19 +172,19 @@
|
|||
{/if}
|
||||
{@const textAreaInput = narrowWidgetProps(component.props, "TextAreaInput")}
|
||||
{#if textAreaInput}
|
||||
<TextAreaInput {...exclude(textAreaInput)} on:commitText={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
<TextAreaInput {...exclude(textAreaInput)} oncommitText={(detail) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
{/if}
|
||||
{@const textButton = narrowWidgetProps(component.props, "TextButton")}
|
||||
{#if textButton}
|
||||
<TextButton {...exclude(textButton)} action={() => widgetValueCommitAndUpdate(index, undefined)} />
|
||||
<TextButton {...exclude(textButton)} onclick={() => widgetValueCommitAndUpdate(index, undefined)} />
|
||||
{/if}
|
||||
{@const breadcrumbTrailButtons = narrowWidgetProps(component.props, "BreadcrumbTrailButtons")}
|
||||
{#if breadcrumbTrailButtons}
|
||||
<BreadcrumbTrailButtons {...exclude(breadcrumbTrailButtons)} action={(breadcrumbIndex) => widgetValueCommitAndUpdate(index, breadcrumbIndex)} />
|
||||
<BreadcrumbTrailButtons {...exclude(breadcrumbTrailButtons)} onclick={(breadcrumbIndex) => widgetValueCommitAndUpdate(index, breadcrumbIndex)} />
|
||||
{/if}
|
||||
{@const textInput = narrowWidgetProps(component.props, "TextInput")}
|
||||
{#if textInput}
|
||||
<TextInput {...exclude(textInput)} on:commitText={({ detail }) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
<TextInput {...exclude(textInput)} oncommitText={(detail) => widgetValueCommitAndUpdate(index, detail)} />
|
||||
{/if}
|
||||
{@const textLabel = narrowWidgetProps(component.props, "TextLabel")}
|
||||
{#if textLabel}
|
||||
|
|
|
@ -1,23 +1,30 @@
|
|||
<script lang="ts">
|
||||
import { type WidgetTable as WidgetTableFromJsMessages } from "@graphite/messages";
|
||||
import { type WidgetTable as WidgetTableFromJsMessages } from "@graphite/messages.svelte";
|
||||
|
||||
import WidgetSpan from "@graphite/components/widgets/WidgetSpan.svelte";
|
||||
|
||||
export let widgetData: WidgetTableFromJsMessages;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export let layoutTarget: any; // TODO: Give this a real type
|
||||
|
||||
interface Props {
|
||||
widgetData: WidgetTableFromJsMessages;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
layoutTarget: any; // TODO: Give this a real type
|
||||
}
|
||||
|
||||
let { widgetData, layoutTarget }: Props = $props();
|
||||
</script>
|
||||
|
||||
<table>
|
||||
{#each widgetData.tableWidgets as row}
|
||||
<tr>
|
||||
{#each row as cell}
|
||||
<td>
|
||||
<WidgetSpan widgetData={{ rowWidgets: [cell] }} {layoutTarget} />
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
<tbody>
|
||||
{#each widgetData.tableWidgets as row}
|
||||
<tr>
|
||||
{#each row as cell}
|
||||
<td>
|
||||
<WidgetSpan widgetData={{ rowWidgets: [cell] }} {layoutTarget} />
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
@ -2,16 +2,25 @@
|
|||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
|
||||
|
||||
export let labels: string[];
|
||||
export let disabled = false;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
// Callbacks
|
||||
export let action: (index: number) => void;
|
||||
interface Props {
|
||||
labels: string[];
|
||||
disabled?: boolean;
|
||||
tooltip?: string | undefined;
|
||||
// Callbacks
|
||||
onclick: (index: number) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
labels,
|
||||
disabled = false,
|
||||
tooltip = undefined,
|
||||
onclick
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<LayoutRow class="breadcrumb-trail-buttons" {tooltip}>
|
||||
{#each labels as label, index}
|
||||
<TextButton {label} emphasized={index === labels.length - 1} {disabled} action={() => !disabled && index !== labels.length - 1 && action(index)} />
|
||||
<TextButton {label} emphasized={index === labels.length - 1} {disabled} onclick={() => !disabled && index !== labels.length - 1 && onclick(index)} />
|
||||
{/each}
|
||||
</LayoutRow>
|
||||
|
||||
|
|
|
@ -1,24 +1,37 @@
|
|||
<script lang="ts">
|
||||
import { type IconName, type IconSize } from "@graphite/utility-functions/icons";
|
||||
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
|
||||
export let icon: IconName;
|
||||
export let hoverIcon: IconName | undefined = undefined;
|
||||
export let size: IconSize;
|
||||
export let disabled = false;
|
||||
export let active = false;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
// Callbacks
|
||||
export let action: (e?: MouseEvent) => void;
|
||||
type ButtonHTMLElementProps = SvelteHTMLElements["button"];
|
||||
|
||||
interface Props extends ButtonHTMLElementProps {
|
||||
class?: string;
|
||||
classes?: Record<string, boolean>;
|
||||
icon: IconName;
|
||||
hoverIcon?: IconName | undefined;
|
||||
size: IconSize;
|
||||
disabled?: boolean;
|
||||
active?: boolean;
|
||||
tooltip?: string | undefined;
|
||||
}
|
||||
|
||||
let className = "";
|
||||
export { className as class };
|
||||
export let classes: Record<string, boolean> = {};
|
||||
let {
|
||||
class: className = "",
|
||||
classes = {},
|
||||
icon,
|
||||
hoverIcon = undefined,
|
||||
size,
|
||||
disabled = false,
|
||||
active = false,
|
||||
tooltip = undefined,
|
||||
...rest
|
||||
}: Props = $props();
|
||||
|
||||
$: extraClasses = Object.entries(classes)
|
||||
let extraClasses = $derived(Object.entries(classes)
|
||||
.flatMap(([className, stateName]) => (stateName ? [className] : []))
|
||||
.join(" ");
|
||||
.join(" "));
|
||||
</script>
|
||||
|
||||
<button
|
||||
|
@ -26,11 +39,10 @@
|
|||
class:hover-icon={hoverIcon && !disabled}
|
||||
class:disabled
|
||||
class:active
|
||||
on:click={action}
|
||||
{disabled}
|
||||
title={tooltip}
|
||||
tabindex={active ? -1 : 0}
|
||||
{...$$restProps}
|
||||
{...rest}
|
||||
>
|
||||
<IconLabel {icon} />
|
||||
{#if hoverIcon && !disabled}
|
||||
|
@ -55,7 +67,7 @@
|
|||
}
|
||||
|
||||
// The `where` pseudo-class does not contribtue to specificity
|
||||
& + :where(.icon-button) {
|
||||
& + :where(:global(.icon-button)) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,23 +1,33 @@
|
|||
<script lang="ts">
|
||||
import { IMAGE_BASE64_STRINGS } from "@graphite/utility-functions/images";
|
||||
|
||||
let className = "";
|
||||
export { className as class };
|
||||
export let classes: Record<string, boolean> = {};
|
||||
interface Props {
|
||||
class?: string;
|
||||
classes?: Record<string, boolean>;
|
||||
image: string;
|
||||
width: string | undefined;
|
||||
height: string | undefined;
|
||||
tooltip?: string | undefined;
|
||||
// Callbacks
|
||||
action: (e?: MouseEvent) => void;
|
||||
}
|
||||
|
||||
export let image: string;
|
||||
export let width: string | undefined;
|
||||
export let height: string | undefined;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
// Callbacks
|
||||
export let action: (e?: MouseEvent) => void;
|
||||
let {
|
||||
class: className = "",
|
||||
classes = {},
|
||||
image,
|
||||
width,
|
||||
height,
|
||||
tooltip = undefined,
|
||||
action
|
||||
}: Props = $props();
|
||||
|
||||
$: extraClasses = Object.entries(classes)
|
||||
let extraClasses = $derived(Object.entries(classes)
|
||||
.flatMap(([className, stateName]) => (stateName ? [className] : []))
|
||||
.join(" ");
|
||||
.join(" "));
|
||||
</script>
|
||||
|
||||
<img src={IMAGE_BASE64_STRINGS[image]} style:width style:height class={`image-label ${className} ${extraClasses}`.trim()} title={tooltip} alt="" on:click={action} />
|
||||
<img src={IMAGE_BASE64_STRINGS[image]} style:width style:height class={`image-label ${className} ${extraClasses}`.trim()} title={tooltip} alt="" onclick={action} />
|
||||
|
||||
<style lang="scss" global>
|
||||
.image-label {
|
||||
|
|
|
@ -1,13 +1,22 @@
|
|||
<script lang="ts">
|
||||
import type { FrontendGraphDataType } from "@graphite/messages";
|
||||
import type { FrontendGraphDataType } from "@graphite/messages.svelte";
|
||||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
|
||||
export let exposed: boolean;
|
||||
export let dataType: FrontendGraphDataType;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
// Callbacks
|
||||
export let action: (e?: MouseEvent) => void;
|
||||
interface Props {
|
||||
exposed: boolean;
|
||||
dataType: FrontendGraphDataType;
|
||||
tooltip?: string | undefined;
|
||||
// Callbacks
|
||||
action: (e?: MouseEvent) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
exposed,
|
||||
dataType,
|
||||
tooltip = undefined,
|
||||
action
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<LayoutRow class="parameter-expose-button">
|
||||
|
@ -15,7 +24,7 @@
|
|||
class:exposed
|
||||
style:--data-type-color={`var(--color-data-${dataType.toLowerCase()})`}
|
||||
style:--data-type-color-dim={`var(--color-data-${dataType.toLowerCase()}-dim)`}
|
||||
on:click={action}
|
||||
onclick={action}
|
||||
title={tooltip}
|
||||
tabindex="-1"
|
||||
>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import type { MenuDirection } from "@graphite/messages";
|
||||
import type { MenuDirection } from "@graphite/messages.svelte";
|
||||
import { type IconName, type PopoverButtonStyle } from "@graphite/utility-functions/icons";
|
||||
|
||||
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
|
@ -7,17 +7,30 @@
|
|||
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
|
||||
export let style: PopoverButtonStyle = "DropdownArrow";
|
||||
export let menuDirection: MenuDirection = "Bottom";
|
||||
export let icon: IconName | undefined = undefined;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
export let disabled = false;
|
||||
export let popoverMinWidth = 1;
|
||||
interface Props {
|
||||
style?: PopoverButtonStyle;
|
||||
menuDirection?: MenuDirection;
|
||||
icon?: IconName | undefined;
|
||||
tooltip?: string | undefined;
|
||||
disabled?: boolean;
|
||||
popoverMinWidth?: number;
|
||||
// Callbacks
|
||||
action?: (() => void) | undefined;
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
// Callbacks
|
||||
export let action: (() => void) | undefined = undefined;
|
||||
let {
|
||||
style = "DropdownArrow",
|
||||
menuDirection = "Bottom",
|
||||
icon = undefined,
|
||||
tooltip = undefined,
|
||||
disabled = false,
|
||||
popoverMinWidth = 1,
|
||||
action = undefined,
|
||||
children
|
||||
}: Props = $props();
|
||||
|
||||
let open = false;
|
||||
let open = $state(false);
|
||||
|
||||
function onClick() {
|
||||
open = true;
|
||||
|
@ -26,13 +39,13 @@
|
|||
</script>
|
||||
|
||||
<LayoutRow class="popover-button" classes={{ "has-icon": icon !== undefined, "direction-top": menuDirection === "Top" }}>
|
||||
<IconButton class="dropdown-icon" classes={{ open }} {disabled} action={() => onClick()} icon={style || "DropdownArrow"} size={16} {tooltip} data-floating-menu-spawner />
|
||||
<IconButton class="dropdown-icon" classes={{ open }} {disabled} onclick={() => onClick()} icon={style || "DropdownArrow"} size={16} {tooltip} data-floating-menu-spawner />
|
||||
{#if icon !== undefined}
|
||||
<IconLabel class="descriptive-icon" classes={{ open }} {disabled} {icon} {tooltip} />
|
||||
{/if}
|
||||
|
||||
<FloatingMenu {open} on:open={({ detail }) => (open = detail)} minWidth={popoverMinWidth} type="Popover" direction={menuDirection || "Bottom"}>
|
||||
<slot />
|
||||
<FloatingMenu bind:open minWidth={popoverMinWidth} type="Popover" direction={menuDirection || "Bottom"}>
|
||||
{@render children?.()}
|
||||
</FloatingMenu>
|
||||
</LayoutRow>
|
||||
|
||||
|
|
|
@ -1,38 +1,51 @@
|
|||
<script lang="ts">
|
||||
import type { MenuListEntry } from "@graphite/messages";
|
||||
import type { MenuListEntry } from "@graphite/messages.svelte";
|
||||
import type { IconName } from "@graphite/utility-functions/icons";
|
||||
|
||||
import MenuList from "@graphite/components/floating-menus/MenuList.svelte";
|
||||
import ConditionalWrapper from "@graphite/components/layout/ConditionalWrapper.svelte";
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
import type { MouseEventHandler } from "svelte/elements";
|
||||
|
||||
let self: MenuList;
|
||||
|
||||
let self: MenuList | undefined = $state();
|
||||
let open: boolean = $state(false);
|
||||
// Note: IconButton should be used if only an icon, but no label, is desired.
|
||||
// However, if multiple TextButton widgets are used in a group with only some having no label, this component is able to accommodate that.
|
||||
export let label: string;
|
||||
export let icon: IconName | undefined = undefined;
|
||||
export let hoverIcon: IconName | undefined = undefined;
|
||||
export let emphasized = false;
|
||||
export let flush = false;
|
||||
export let minWidth = 0;
|
||||
export let disabled = false;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
export let menuListChildren: MenuListEntry[][] | undefined = undefined;
|
||||
interface Props {
|
||||
label: string;
|
||||
icon?: IconName | undefined;
|
||||
hoverIcon?: IconName | undefined;
|
||||
emphasized?: boolean;
|
||||
flush?: boolean;
|
||||
minWidth?: number;
|
||||
disabled?: boolean;
|
||||
tooltip?: string | undefined;
|
||||
menuListChildren?: MenuListEntry[][] | undefined;
|
||||
onclick: MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
// Callbacks
|
||||
// TODO: Replace this with an event binding (and on other components that do this)
|
||||
export let action: (() => void) | undefined;
|
||||
let {
|
||||
label,
|
||||
icon = undefined,
|
||||
hoverIcon = undefined,
|
||||
emphasized = false,
|
||||
flush = false,
|
||||
minWidth = 0,
|
||||
disabled = false,
|
||||
tooltip = undefined,
|
||||
menuListChildren = $bindable([]),
|
||||
onclick,
|
||||
}: Props = $props();
|
||||
|
||||
$: menuListChildrenExists = (menuListChildren?.length ?? 0) > 0;
|
||||
let menuListChildrenExists = $derived((menuListChildren?.length ?? 0) > 0);
|
||||
|
||||
// Handles either a button click or, if applicable, the opening of the menu list floating menu
|
||||
function onClick(e: MouseEvent) {
|
||||
function onClick(e: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement; }) {
|
||||
// If there's no menu to open, trigger the action
|
||||
if ((menuListChildren?.length ?? 0) === 0) {
|
||||
// Call the action
|
||||
if (action && !disabled) action();
|
||||
if (!disabled) onclick?.(e);
|
||||
|
||||
// Exit early so we don't continue on and try to open the menu
|
||||
return;
|
||||
|
@ -50,7 +63,7 @@
|
|||
<ConditionalWrapper condition={menuListChildrenExists} wrapperClass="text-button-container">
|
||||
<button
|
||||
class="text-button"
|
||||
class:open={self?.open}
|
||||
class:open={open}
|
||||
class:hover-icon={hoverIcon && !disabled}
|
||||
class:emphasized
|
||||
class:disabled
|
||||
|
@ -62,7 +75,7 @@
|
|||
data-text-button
|
||||
tabindex={disabled ? -1 : 0}
|
||||
data-floating-menu-spawner={menuListChildrenExists ? "" : "no-hover-transfer"}
|
||||
on:click={onClick}
|
||||
onclick={onClick}
|
||||
>
|
||||
{#if icon}
|
||||
<IconLabel {icon} />
|
||||
|
@ -76,9 +89,8 @@
|
|||
</button>
|
||||
{#if menuListChildrenExists}
|
||||
<MenuList
|
||||
on:open={({ detail }) => self && (self.open = detail)}
|
||||
open={self?.open || false}
|
||||
entries={menuListChildren || []}
|
||||
bind:open={open}
|
||||
entries={menuListChildren}
|
||||
direction="Bottom"
|
||||
minWidth={240}
|
||||
drawIcon={true}
|
||||
|
@ -109,6 +121,7 @@
|
|||
color: var(--button-text-color);
|
||||
--button-background-color: var(--color-5-dullgray);
|
||||
--button-text-color: var(--color-e-nearwhite);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&.open {
|
||||
|
@ -129,6 +142,8 @@
|
|||
&.disabled {
|
||||
--button-background-color: var(--color-4-dimgray);
|
||||
--button-text-color: var(--color-8-uppergray);
|
||||
cursor: not-allowed;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
&.emphasized {
|
||||
|
|
|
@ -1,25 +1,34 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import type { IconName } from "@graphite/utility-functions/icons";
|
||||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher<{ checked: boolean }>();
|
||||
|
||||
export let checked = false;
|
||||
export let disabled = false;
|
||||
export let icon: IconName = "Checkmark";
|
||||
export let tooltip: string | undefined = undefined;
|
||||
export let forLabel: bigint | undefined = undefined;
|
||||
interface Props {
|
||||
checked?: boolean;
|
||||
disabled?: boolean;
|
||||
icon?: IconName;
|
||||
tooltip?: string | undefined;
|
||||
forLabel?: bigint | undefined;
|
||||
onchecked?: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
checked = false,
|
||||
disabled = false,
|
||||
icon = "Checkmark",
|
||||
tooltip = undefined,
|
||||
forLabel = undefined,
|
||||
onchecked,
|
||||
}: Props = $props();
|
||||
|
||||
let inputElement: HTMLInputElement | undefined;
|
||||
let inputElement: HTMLInputElement | undefined = $state();
|
||||
|
||||
const backupId = String(Math.random()).substring(2);
|
||||
|
||||
$: id = forLabel !== undefined ? String(forLabel) : backupId;
|
||||
$: displayIcon = (!checked && icon === "Checkmark" ? "Empty12px" : icon) as IconName;
|
||||
let id = $derived(forLabel !== undefined ? String(forLabel) : backupId);
|
||||
let displayIcon = $derived((!checked && icon === "Checkmark" ? "Empty12px" : icon) as IconName);
|
||||
|
||||
export function isChecked() {
|
||||
return checked;
|
||||
|
@ -41,12 +50,12 @@
|
|||
type="checkbox"
|
||||
id={`checkbox-input-${id}`}
|
||||
bind:checked
|
||||
on:change={(_) => dispatch("checked", inputElement?.checked || false)}
|
||||
onchange={(_) => onchecked?.(inputElement?.checked ?? false)}
|
||||
{disabled}
|
||||
tabindex={disabled ? -1 : 0}
|
||||
bind:this={inputElement}
|
||||
/>
|
||||
<label class:disabled class:checked for={`checkbox-input-${id}`} on:keydown={(e) => e.key === "Enter" && toggleCheckboxFromLabel(e)} title={tooltip}>
|
||||
<label class:disabled class:checked for={`checkbox-input-${id}`} onkeydown={(e) => e.key === "Enter" && toggleCheckboxFromLabel(e)} title={tooltip}>
|
||||
<LayoutRow class="checkbox-box">
|
||||
<IconLabel icon={displayIcon} />
|
||||
</LayoutRow>
|
||||
|
|
|
@ -1,48 +1,54 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import type { FillChoice } from "@graphite/messages";
|
||||
import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages";
|
||||
import type { FillChoice } from "@graphite/messages.svelte";
|
||||
import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages.svelte";
|
||||
|
||||
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher<{ value: FillChoice; startHistoryTransaction: undefined }>();
|
||||
let open = $state(false);
|
||||
|
||||
let open = false;
|
||||
interface Props {
|
||||
value: FillChoice;
|
||||
disabled?: boolean;
|
||||
allowNone?: boolean;
|
||||
// export let allowTransparency = false; // TODO: Implement
|
||||
tooltip?: string | undefined;
|
||||
onvalue?: (value: FillChoice) => void;
|
||||
onstartHistoryTransaction?: () => void;
|
||||
}
|
||||
|
||||
export let value: FillChoice;
|
||||
export let disabled = false;
|
||||
export let allowNone = false;
|
||||
// export let allowTransparency = false; // TODO: Implement
|
||||
export let tooltip: string | undefined = undefined;
|
||||
let {
|
||||
value,
|
||||
disabled = false,
|
||||
allowNone = false,
|
||||
tooltip = undefined,
|
||||
onvalue,
|
||||
onstartHistoryTransaction,
|
||||
}: Props = $props();
|
||||
|
||||
let outlineFactor = $derived(contrastingOutlineFactor(value, ["--color-1-nearblack", "--color-3-darkgray"], 0.01));
|
||||
let outlined = $derived(outlineFactor > 0.0001);
|
||||
let chosenGradient = $derived(value instanceof Gradient ? value.toLinearGradientCSS() : `linear-gradient(${value.toHexOptionalAlpha()}, ${value.toHexOptionalAlpha()})`);
|
||||
let none = $derived(value instanceof Color ? value.none : false);
|
||||
let transparency = $derived(value instanceof Gradient ? value.stops.some((stop) => stop.color.alpha < 1) : value.alpha < 1);
|
||||
|
||||
$: outlineFactor = contrastingOutlineFactor(value, ["--color-1-nearblack", "--color-3-darkgray"], 0.01);
|
||||
$: outlined = outlineFactor > 0.0001;
|
||||
$: chosenGradient = value instanceof Gradient ? value.toLinearGradientCSS() : `linear-gradient(${value.toHexOptionalAlpha()}, ${value.toHexOptionalAlpha()})`;
|
||||
$: none = value instanceof Color ? value.none : false;
|
||||
$: transparency = value instanceof Gradient ? value.stops.some((stop) => stop.color.alpha < 1) : value.alpha < 1;
|
||||
</script>
|
||||
|
||||
<LayoutCol class="color-button" classes={{ open, disabled, none, transparency, outlined }} {tooltip}>
|
||||
<button {disabled} style:--chosen-gradient={chosenGradient} style:--outline-amount={outlineFactor} on:click={() => (open = true)} tabindex="0" data-floating-menu-spawner>
|
||||
<button {disabled} style:--chosen-gradient={chosenGradient} style:--outline-amount={outlineFactor} onclick={() => (open = true)} tabindex="0" data-floating-menu-spawner>
|
||||
{#if disabled && value instanceof Color && !value.none}
|
||||
<TextLabel>sRGB</TextLabel>
|
||||
{/if}
|
||||
</button>
|
||||
<ColorPicker
|
||||
{open}
|
||||
on:open={({ detail }) => (open = detail)}
|
||||
bind:open
|
||||
colorOrGradient={value}
|
||||
on:colorOrGradient={({ detail }) => {
|
||||
value = detail;
|
||||
dispatch("value", detail);
|
||||
}}
|
||||
on:startHistoryTransaction={() => {
|
||||
oncolorOrGradient={onvalue}
|
||||
onstartHistoryTransaction={() => {
|
||||
// This event is sent to the backend so it knows to start a transaction for the history system. See discussion for some explanation:
|
||||
// <https://github.com/GraphiteEditor/Graphite/pull/1584#discussion_r1477592483>
|
||||
dispatch("startHistoryTransaction");
|
||||
onstartHistoryTransaction?.()
|
||||
}}
|
||||
{allowNone}
|
||||
/>
|
||||
|
|
|
@ -1,26 +1,34 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import type { Curve, CurveManipulatorGroup } from "@graphite/messages";
|
||||
import type { Curve, CurveManipulatorGroup } from "@graphite/messages.svelte";
|
||||
import { clamp } from "@graphite/utility-functions/math";
|
||||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
|
||||
interface Props {
|
||||
classes?: Record<string, boolean>;
|
||||
style?: string;
|
||||
styles?: Record<string, string | number | undefined>;
|
||||
value: Curve;
|
||||
}>();
|
||||
disabled?: boolean;
|
||||
tooltip?: string | undefined;
|
||||
children?: import('svelte').Snippet;
|
||||
onvalue?: (curve: Curve) => void;
|
||||
}
|
||||
|
||||
export let classes: Record<string, boolean> = {};
|
||||
let styleName = "";
|
||||
export { styleName as style };
|
||||
export let styles: Record<string, string | number | undefined> = {};
|
||||
export let value: Curve;
|
||||
export let disabled = false;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
let {
|
||||
classes = {},
|
||||
style: styleName = "",
|
||||
styles = {},
|
||||
value,
|
||||
disabled = false,
|
||||
tooltip = undefined,
|
||||
children,
|
||||
onvalue,
|
||||
}: Props = $props();
|
||||
|
||||
const GRID_SIZE = 4;
|
||||
|
||||
let groups: CurveManipulatorGroup[] = [
|
||||
let groups: CurveManipulatorGroup[] = $state([
|
||||
{
|
||||
anchor: [0, 0],
|
||||
handles: [
|
||||
|
@ -42,20 +50,14 @@
|
|||
[2, 2],
|
||||
],
|
||||
},
|
||||
];
|
||||
let selectedNodeIndex: number | undefined = undefined;
|
||||
]);
|
||||
let selectedNodeIndex: number | undefined = $state(undefined);
|
||||
let draggedNodeIndex: number | undefined = undefined;
|
||||
let dAttribute = recalculateSvgPath();
|
||||
let dAttribute = $state(recalculateSvgPath());
|
||||
|
||||
$: {
|
||||
groups = [groups[0]].concat(value.manipulatorGroups).concat([groups[groups.length - 1]]);
|
||||
groups[0].handles[1] = value.firstHandle;
|
||||
groups[groups.length - 1].handles[0] = value.lastHandle;
|
||||
dAttribute = recalculateSvgPath();
|
||||
}
|
||||
|
||||
function updateCurve() {
|
||||
dispatch("value", {
|
||||
onvalue?.({
|
||||
manipulatorGroups: groups.slice(1, groups.length - 1),
|
||||
firstHandle: groups[0].handles[1],
|
||||
lastHandle: groups[groups.length - 1].handles[0],
|
||||
|
@ -83,7 +85,6 @@
|
|||
selectedNodeIndex = undefined;
|
||||
|
||||
groups.splice(i, 1);
|
||||
groups = groups;
|
||||
|
||||
dAttribute = recalculateSvgPath();
|
||||
|
||||
|
@ -186,10 +187,16 @@
|
|||
dAttribute = recalculateSvgPath();
|
||||
updateCurve();
|
||||
}
|
||||
$effect(() => {
|
||||
groups = [groups[0]].concat(value.manipulatorGroups).concat([groups[groups.length - 1]]);
|
||||
groups[0].handles[1] = value.firstHandle;
|
||||
groups[groups.length - 1].handles[0] = value.lastHandle;
|
||||
dAttribute = recalculateSvgPath();
|
||||
});
|
||||
</script>
|
||||
|
||||
<LayoutRow class={"curve-input"} classes={{ disabled, ...classes }} style={styleName} {styles} {tooltip}>
|
||||
<svg viewBox="0 0 1 1" on:pointermove={handlePointerMove} on:pointerup={handlePointerUp}>
|
||||
<svg viewBox="0 0 1 1" onpointermove={handlePointerMove} onpointerup={handlePointerUp}>
|
||||
{#each { length: GRID_SIZE - 1 } as _, i}
|
||||
<path class="grid" d={`M 0 ${(i + 1) / GRID_SIZE} L 1 ${(i + 1) / GRID_SIZE}`} />
|
||||
<path class="grid" d={`M ${(i + 1) / GRID_SIZE} 0 L ${(i + 1) / GRID_SIZE} 1`} />
|
||||
|
@ -199,14 +206,14 @@
|
|||
{@const group = groups[selectedNodeIndex]}
|
||||
{#each [0, 1] as i}
|
||||
<path d={`M ${group.anchor[0]} ${1 - group.anchor[1]} L ${group.handles[i][0]} ${1 - group.handles[i][1]}`} class="handle-line" />
|
||||
<circle cx={group.handles[i][0]} cy={1 - group.handles[i][1]} class="manipulator handle" r="0.02" on:pointerdown={(e) => handleManipulatorPointerDown(e, -i - 1)} />
|
||||
<circle cx={group.handles[i][0]} cy={1 - group.handles[i][1]} class="manipulator handle" r="0.02" onpointerdown={(e) => handleManipulatorPointerDown(e, -i - 1)} />
|
||||
{/each}
|
||||
{/if}
|
||||
{#each groups as group, i}
|
||||
<circle cx={group.anchor[0]} cy={1 - group.anchor[1]} class="manipulator" r="0.02" on:pointerdown={(e) => handleManipulatorPointerDown(e, i)} />
|
||||
<circle cx={group.anchor[0]} cy={1 - group.anchor[1]} class="manipulator" r="0.02" onpointerdown={(e) => handleManipulatorPointerDown(e, i)} />
|
||||
{/each}
|
||||
</svg>
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</LayoutRow>
|
||||
|
||||
<style lang="scss" global>
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import type { MenuListEntry } from "@graphite/messages";
|
||||
import type { MenuListEntry } from "@graphite/messages.svelte";
|
||||
|
||||
import MenuList from "@graphite/components/floating-menus/MenuList.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
|
@ -10,29 +8,41 @@
|
|||
|
||||
const DASH_ENTRY = { value: "", label: "-" };
|
||||
|
||||
const dispatch = createEventDispatcher<{ selectedIndex: number; hoverInEntry: number; hoverOutEntry: number }>();
|
||||
let menuList: MenuList | undefined = $state();
|
||||
let self: LayoutRow | undefined = $state();
|
||||
|
||||
let menuList: MenuList | undefined;
|
||||
let self: LayoutRow | undefined;
|
||||
interface Props {
|
||||
entries: MenuListEntry[][];
|
||||
selectedIndex?: number | undefined; // When not provided, a dash is displayed
|
||||
drawIcon?: boolean;
|
||||
interactive?: boolean;
|
||||
disabled?: boolean;
|
||||
tooltip?: string | undefined;
|
||||
minWidth?: number;
|
||||
maxWidth?: number;
|
||||
onhoverOutEntry?: (index: number) => void;
|
||||
onhoverInEntry?: (index: number) => void;
|
||||
onselectedIndex?: (index: number) => void;
|
||||
}
|
||||
|
||||
export let entries: MenuListEntry[][];
|
||||
export let selectedIndex: number | undefined = undefined; // When not provided, a dash is displayed
|
||||
export let drawIcon = false;
|
||||
export let interactive = true;
|
||||
export let disabled = false;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
export let minWidth = 0;
|
||||
export let maxWidth = 0;
|
||||
let {
|
||||
entries,
|
||||
selectedIndex = undefined,
|
||||
drawIcon = false,
|
||||
interactive = true,
|
||||
disabled = false,
|
||||
tooltip = undefined,
|
||||
minWidth = $bindable(0),
|
||||
maxWidth = 0,
|
||||
onhoverInEntry,
|
||||
onhoverOutEntry,
|
||||
onselectedIndex,
|
||||
}: Props = $props();
|
||||
|
||||
let activeEntry = makeActiveEntry();
|
||||
let activeEntry = $state(makeActiveEntry());
|
||||
let activeEntrySkipWatcher = false;
|
||||
let initialSelectedIndex: number | undefined = undefined;
|
||||
let open = false;
|
||||
|
||||
$: watchSelectedIndex(selectedIndex);
|
||||
$: watchEntries(entries);
|
||||
$: watchActiveEntry(activeEntry);
|
||||
$: watchOpen(open);
|
||||
let open = $state(false);
|
||||
|
||||
function watchOpen(open: boolean) {
|
||||
initialSelectedIndex = open ? selectedIndex : undefined;
|
||||
|
@ -50,23 +60,27 @@
|
|||
activeEntry = makeActiveEntry();
|
||||
}
|
||||
|
||||
function getEntryIndex(entry: MenuListEntry): number {
|
||||
return entries.flat().findIndex(item => item.value === entry.value);
|
||||
}
|
||||
|
||||
// Called when the `activeEntry` two-way binding on this component's MenuList component is changed, or by the `selectedIndex()` watcher above (but we want to skip that case)
|
||||
function watchActiveEntry(activeEntry: MenuListEntry) {
|
||||
if (activeEntrySkipWatcher) {
|
||||
activeEntrySkipWatcher = false;
|
||||
} else if (activeEntry !== DASH_ENTRY) {
|
||||
// We need to set to the initial value first to track a right history step, as if we hover in initial selection.
|
||||
if (initialSelectedIndex !== undefined) dispatch("hoverInEntry", initialSelectedIndex);
|
||||
dispatch("selectedIndex", entries.flat().indexOf(activeEntry));
|
||||
if (initialSelectedIndex !== undefined) onhoverInEntry?.(initialSelectedIndex);
|
||||
onselectedIndex?.(getEntryIndex(activeEntry));
|
||||
}
|
||||
}
|
||||
|
||||
function dispatchHoverInEntry(hoveredEntry: MenuListEntry) {
|
||||
dispatch("hoverInEntry", entries.flat().indexOf(hoveredEntry));
|
||||
onhoverInEntry?.(getEntryIndex(hoveredEntry));
|
||||
}
|
||||
|
||||
function dispatchHoverOutEntry() {
|
||||
if (initialSelectedIndex !== undefined) dispatch("hoverOutEntry", initialSelectedIndex);
|
||||
if (initialSelectedIndex !== undefined) onhoverOutEntry?.(initialSelectedIndex);
|
||||
}
|
||||
|
||||
function makeActiveEntry(): MenuListEntry {
|
||||
|
@ -82,6 +96,15 @@
|
|||
const blurTarget = (e.target as HTMLDivElement | undefined)?.closest("[data-dropdown-input]") || undefined;
|
||||
if (blurTarget !== self?.div?.()) open = false;
|
||||
}
|
||||
$effect(() => {
|
||||
watchSelectedIndex(selectedIndex);
|
||||
});
|
||||
$effect(() => {
|
||||
watchActiveEntry(activeEntry);
|
||||
});
|
||||
$effect(() => {
|
||||
watchOpen(open);
|
||||
});
|
||||
</script>
|
||||
|
||||
<LayoutRow
|
||||
|
@ -94,8 +117,8 @@
|
|||
class="dropdown-box"
|
||||
classes={{ disabled, open }}
|
||||
{tooltip}
|
||||
on:click={() => !disabled && (open = true)}
|
||||
on:blur={unFocusDropdownBox}
|
||||
onclick={() => !disabled && (open = true)}
|
||||
onblur={unFocusDropdownBox}
|
||||
tabindex={disabled ? -1 : 0}
|
||||
data-floating-menu-spawner
|
||||
>
|
||||
|
@ -106,13 +129,12 @@
|
|||
<IconLabel class="dropdown-arrow" icon="DropdownArrow" />
|
||||
</LayoutRow>
|
||||
<MenuList
|
||||
on:naturalWidth={({ detail }) => (minWidth = detail)}
|
||||
onnaturalWidth={(detail) => (minWidth = detail)}
|
||||
{activeEntry}
|
||||
on:activeEntry={({ detail }) => (activeEntry = detail)}
|
||||
on:hoverInEntry={({ detail }) => dispatchHoverInEntry(detail)}
|
||||
on:hoverOutEntry={() => dispatchHoverOutEntry()}
|
||||
{open}
|
||||
on:open={({ detail }) => (open = detail)}
|
||||
onactiveEntry={(detail) => (activeEntry = detail)}
|
||||
onhoverInEntry={dispatchHoverInEntry}
|
||||
onhoverOutEntry={dispatchHoverOutEntry}
|
||||
bind:open
|
||||
{entries}
|
||||
{drawIcon}
|
||||
{interactive}
|
||||
|
|
|
@ -1,41 +1,65 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import { platformIsMac } from "@graphite/utility-functions/platform";
|
||||
|
||||
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
type InputHTMLElementProps = SvelteHTMLElements["input"];
|
||||
type TextAreaHTMLElementProps = SvelteHTMLElements["textarea"];
|
||||
|
||||
type CommonProps = {
|
||||
class?: string;
|
||||
classes?: Record<string, boolean>;
|
||||
style?: string;
|
||||
styles?: Record<string, string | number | undefined>;
|
||||
value: string;
|
||||
textFocused: undefined;
|
||||
textChanged: undefined;
|
||||
textChangeCanceled: undefined;
|
||||
}>();
|
||||
label?: string | undefined;
|
||||
spellcheck?: boolean;
|
||||
disabled?: boolean;
|
||||
textarea?: boolean;
|
||||
tooltip?: string | undefined;
|
||||
placeholder?: string | undefined;
|
||||
hideContextMenu?: boolean;
|
||||
children?: import('svelte').Snippet;
|
||||
onfocus?: (event: FocusEvent & { currentTarget: HTMLInputElement | HTMLTextAreaElement }) => void;
|
||||
onblur?: (event: FocusEvent & { currentTarget: HTMLInputElement | HTMLTextAreaElement }) => void;
|
||||
onchange?: (event: Event & { currentTarget: HTMLInputElement | HTMLTextAreaElement }) => void;
|
||||
onkeydown?: (event: KeyboardEvent & { currentTarget: HTMLInputElement | HTMLTextAreaElement }) => void;
|
||||
onpointerdown?: (event: PointerEvent & { currentTarget: HTMLInputElement | HTMLTextAreaElement | HTMLLabelElement }) => void;
|
||||
ontextChangeCanceled?: () => void;
|
||||
}
|
||||
|
||||
let className = "";
|
||||
export { className as class };
|
||||
export let classes: Record<string, boolean> = {};
|
||||
let styleName = "";
|
||||
export { styleName as style };
|
||||
export let styles: Record<string, string | number | undefined> = {};
|
||||
export let value: string;
|
||||
export let label: string | undefined = undefined;
|
||||
export let spellcheck = false;
|
||||
export let disabled = false;
|
||||
export let textarea = false;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
export let placeholder: string | undefined = undefined;
|
||||
export let hideContextMenu = false;
|
||||
type Props = CommonProps & (
|
||||
CommonProps['textarea'] extends true
|
||||
? TextAreaHTMLElementProps // If 'textarea' is explicitly true
|
||||
: InputHTMLElementProps // Otherwise (false, undefined, or missing)
|
||||
);
|
||||
|
||||
let inputOrTextarea: HTMLInputElement | HTMLTextAreaElement | undefined;
|
||||
let {
|
||||
class: className = "",
|
||||
classes = {},
|
||||
style: styleName = "",
|
||||
styles = {},
|
||||
value = $bindable(),
|
||||
label = undefined,
|
||||
spellcheck = false,
|
||||
disabled = false,
|
||||
textarea = false,
|
||||
tooltip = undefined,
|
||||
placeholder = undefined,
|
||||
hideContextMenu = false,
|
||||
onfocus,
|
||||
onchange,
|
||||
children,
|
||||
onpointerdown,
|
||||
ontextChangeCanceled,
|
||||
}: Props = $props();
|
||||
|
||||
let inputOrTextarea: HTMLInputElement | HTMLTextAreaElement | undefined = $state();
|
||||
let id = String(Math.random()).substring(2);
|
||||
let macKeyboardLayout = platformIsMac();
|
||||
|
||||
$: inputValue = value;
|
||||
|
||||
$: dispatch("value", inputValue);
|
||||
|
||||
// Select (highlight) all the text. For technical reasons, it is necessary to pass the current text.
|
||||
export function selectAllText(currentText: string) {
|
||||
if (!inputOrTextarea) return;
|
||||
|
@ -54,7 +78,7 @@
|
|||
}
|
||||
|
||||
export function getValue(): string {
|
||||
return inputOrTextarea?.value || "";
|
||||
return inputOrTextarea?.value ?? "";
|
||||
}
|
||||
|
||||
export function setInputElementValue(value: string) {
|
||||
|
@ -68,10 +92,15 @@
|
|||
}
|
||||
|
||||
function cancel() {
|
||||
dispatch("textChangeCanceled");
|
||||
ontextChangeCanceled?.();
|
||||
|
||||
if (inputOrTextarea) preventEscapeClosingParentFloatingMenu(inputOrTextarea);
|
||||
}
|
||||
|
||||
function onkeydown(e: KeyboardEvent) {
|
||||
e.key === "Enter" && onchange?.(e);
|
||||
e.key === "Escape" && cancel();
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- This is a base component, extended by others like NumberInput and TextInput. It should not be used directly. -->
|
||||
|
@ -85,14 +114,13 @@
|
|||
{disabled}
|
||||
{placeholder}
|
||||
bind:this={inputOrTextarea}
|
||||
bind:value={inputValue}
|
||||
on:focus={() => dispatch("textFocused")}
|
||||
on:blur={() => dispatch("textChanged")}
|
||||
on:change={() => dispatch("textChanged")}
|
||||
on:keydown={(e) => e.key === "Enter" && dispatch("textChanged")}
|
||||
on:keydown={(e) => e.key === "Escape" && cancel()}
|
||||
on:pointerdown
|
||||
on:contextmenu={(e) => hideContextMenu && e.preventDefault()}
|
||||
bind:value={value}
|
||||
onfocus={onfocus}
|
||||
onblur={onchange}
|
||||
onchange={onchange}
|
||||
onkeydown={onkeydown}
|
||||
onpointerdown={onpointerdown}
|
||||
oncontextmenu={(e) => hideContextMenu && e.preventDefault()}
|
||||
data-input-element
|
||||
/>
|
||||
{:else}
|
||||
|
@ -104,20 +132,19 @@
|
|||
{spellcheck}
|
||||
{disabled}
|
||||
bind:this={inputOrTextarea}
|
||||
bind:value={inputValue}
|
||||
on:focus={() => dispatch("textFocused")}
|
||||
on:blur={() => dispatch("textChanged")}
|
||||
on:change={() => dispatch("textChanged")}
|
||||
on:keydown={(e) => (macKeyboardLayout ? e.metaKey : e.ctrlKey) && e.key === "Enter" && dispatch("textChanged")}
|
||||
on:keydown={(e) => e.key === "Escape" && cancel()}
|
||||
on:pointerdown
|
||||
on:contextmenu={(e) => hideContextMenu && e.preventDefault()}
|
||||
/>
|
||||
bind:value={value}
|
||||
onfocus={onfocus}
|
||||
onblur={onchange}
|
||||
onchange={onchange}
|
||||
onkeydown={onkeydown}
|
||||
onpointerdown={onpointerdown}
|
||||
oncontextmenu={(e) => hideContextMenu && e.preventDefault()}
|
||||
></textarea>
|
||||
{/if}
|
||||
{#if label}
|
||||
<label for={`field-input-${id}`} on:pointerdown>{label}</label>
|
||||
<label for={`field-input-${id}`} onpointerdown={onpointerdown}>{label}</label>
|
||||
{/if}
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</LayoutRow>
|
||||
|
||||
<style lang="scss" global>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher, getContext, onMount, tick } from "svelte";
|
||||
import { getContext, onMount, tick } from "svelte";
|
||||
|
||||
import type { MenuListEntry } from "@graphite/messages";
|
||||
import type { MenuListEntry } from "@graphite/messages.svelte";
|
||||
import type { FontsState } from "@graphite/state-providers/fonts";
|
||||
|
||||
import MenuList from "@graphite/components/floating-menus/MenuList.svelte";
|
||||
|
@ -11,26 +11,35 @@
|
|||
|
||||
const fonts = getContext<FontsState>("fonts");
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
let menuList: MenuList | undefined = $state();
|
||||
|
||||
interface Props {
|
||||
fontFamily: string;
|
||||
fontStyle: string;
|
||||
changeFont: { fontFamily: string; fontStyle: string; fontFileUrl: string | undefined };
|
||||
}>();
|
||||
isStyle?: boolean;
|
||||
disabled?: boolean;
|
||||
tooltip?: string | undefined;
|
||||
onchangeFont?: (arg1: { fontFamily: string; fontStyle: string; fontFileUrl: string | undefined }) => void;
|
||||
onfontFamily?: (fontFamily: string) => void;
|
||||
onfontStyle?: (fontStyle: string) => void;
|
||||
}
|
||||
|
||||
let menuList: MenuList | undefined;
|
||||
let {
|
||||
fontFamily,
|
||||
fontStyle,
|
||||
isStyle = false,
|
||||
disabled = false,
|
||||
tooltip = undefined,
|
||||
onchangeFont,
|
||||
onfontFamily,
|
||||
onfontStyle,
|
||||
}: Props = $props();
|
||||
|
||||
export let fontFamily: string;
|
||||
export let fontStyle: string;
|
||||
export let isStyle = false;
|
||||
export let disabled = false;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
let open = $state(false);
|
||||
let entries: MenuListEntry[] = $state([]);
|
||||
let activeEntry: MenuListEntry | undefined = $state(undefined);
|
||||
let minWidth = $state(isStyle ? 0 : 300);
|
||||
|
||||
let open = false;
|
||||
let entries: MenuListEntry[] = [];
|
||||
let activeEntry: MenuListEntry | undefined = undefined;
|
||||
let minWidth = isStyle ? 0 : 300;
|
||||
|
||||
$: watchFont(fontFamily, fontStyle);
|
||||
|
||||
async function watchFont(..._: string[]) {
|
||||
// We set this function's result to a local variable to avoid reading from `entries` which causes Svelte to trigger an update that results in an infinite loop
|
||||
|
@ -64,19 +73,19 @@
|
|||
let style;
|
||||
|
||||
if (isStyle) {
|
||||
dispatch("fontStyle", newName);
|
||||
onfontStyle?.(newName);
|
||||
|
||||
family = fontFamily;
|
||||
style = newName;
|
||||
} else {
|
||||
dispatch("fontFamily", newName);
|
||||
onfontFamily?.(newName);
|
||||
|
||||
family = newName;
|
||||
style = "Regular (400)";
|
||||
}
|
||||
|
||||
const fontFileUrl = await fonts.getFontFileUrl(family, style);
|
||||
dispatch("changeFont", { fontFamily: family, fontStyle: style, fontFileUrl });
|
||||
onchangeFont?.({ fontFamily: family, fontStyle: style, fontFileUrl });
|
||||
}
|
||||
|
||||
async function getEntries(): Promise<MenuListEntry[]> {
|
||||
|
@ -100,6 +109,9 @@
|
|||
|
||||
activeEntry = getActiveEntry(entries);
|
||||
});
|
||||
$effect(() => {
|
||||
watchFont(fontFamily, fontStyle);
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- TODO: Combine this widget into the DropdownInput widget -->
|
||||
|
@ -110,18 +122,17 @@
|
|||
styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }}
|
||||
{tooltip}
|
||||
tabindex={disabled ? -1 : 0}
|
||||
on:click={toggleOpen}
|
||||
onclick={toggleOpen}
|
||||
data-floating-menu-spawner
|
||||
>
|
||||
<TextLabel class="dropdown-label">{activeEntry?.value || ""}</TextLabel>
|
||||
<IconLabel class="dropdown-arrow" icon="DropdownArrow" />
|
||||
</LayoutRow>
|
||||
<MenuList
|
||||
on:naturalWidth={({ detail }) => isStyle && (minWidth = detail)}
|
||||
{activeEntry}
|
||||
on:activeEntry={({ detail }) => (activeEntry = detail)}
|
||||
{open}
|
||||
on:open={({ detail }) => (open = detail)}
|
||||
onnaturalWidth={(detail) => isStyle && (minWidth = detail)}
|
||||
onactiveEntry={(detail) => (activeEntry = detail)}
|
||||
bind:open
|
||||
entries={[entries]}
|
||||
minWidth={isStyle ? 0 : minWidth}
|
||||
virtualScrollingEntryHeight={isStyle ? 0 : 20}
|
||||
|
|
|
@ -1,82 +1,105 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount, onDestroy } from "svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import { PRESS_REPEAT_DELAY_MS, PRESS_REPEAT_INTERVAL_MS } from "@graphite/io-managers/input";
|
||||
import { type NumberInputMode, type NumberInputIncrementBehavior } from "@graphite/messages";
|
||||
import { type NumberInputMode, type NumberInputIncrementBehavior } from "@graphite/messages.svelte";
|
||||
import { evaluateMathExpression } from "@graphite-frontend/wasm/pkg/graphite_wasm.js";
|
||||
|
||||
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import FieldInput from "@graphite/components/widgets/inputs/FieldInput.svelte";
|
||||
import type { MouseEventHandler } from 'svelte/elements';
|
||||
|
||||
const BUTTONS_LEFT = 0b0000_0001;
|
||||
const BUTTONS_RIGHT = 0b0000_0010;
|
||||
const BUTTON_LEFT = 0;
|
||||
const BUTTON_RIGHT = 2;
|
||||
|
||||
interface Props {
|
||||
// Label
|
||||
label?: string | undefined;
|
||||
tooltip?: string | undefined;
|
||||
// Disabled
|
||||
disabled?: boolean;
|
||||
// Value
|
||||
// When `value` is not provided (i.e. it's `undefined`), a dash is displayed.
|
||||
value?: number | undefined; // NOTE: Do not update this directly, do so by calling `updateValue()` instead.
|
||||
min?: number | undefined;
|
||||
max?: number | undefined;
|
||||
isInteger?: boolean;
|
||||
// Number presentation
|
||||
displayDecimalPlaces?: number;
|
||||
unit?: string;
|
||||
unitIsHiddenWhenEditing?: boolean;
|
||||
// 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?: NumberInputMode;
|
||||
// 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?: number;
|
||||
// `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?: NumberInputIncrementBehavior;
|
||||
// `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?: number;
|
||||
rangeMax?: number;
|
||||
// Styling
|
||||
minWidth?: number;
|
||||
maxWidth?: number;
|
||||
// Callbacks
|
||||
incrementCallbackIncrease?: (() => void) | undefined;
|
||||
incrementCallbackDecrease?: (() => void) | undefined;
|
||||
onvalue?: (value: number | undefined) => void;
|
||||
onstartHistoryTransaction?: () => void;
|
||||
oncontextmenu?: MouseEventHandler<HTMLInputElement>
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher<{ value: number | undefined; startHistoryTransaction: undefined }>();
|
||||
let {
|
||||
label = undefined,
|
||||
tooltip = undefined,
|
||||
disabled = false,
|
||||
value = undefined,
|
||||
min = undefined,
|
||||
max = undefined,
|
||||
isInteger = false,
|
||||
displayDecimalPlaces = 2,
|
||||
unit = "",
|
||||
unitIsHiddenWhenEditing = true,
|
||||
mode = "Increment",
|
||||
step = 1,
|
||||
incrementBehavior = "Add",
|
||||
rangeMin = 0,
|
||||
rangeMax = 1,
|
||||
minWidth = 0,
|
||||
maxWidth = 0,
|
||||
incrementCallbackIncrease = undefined,
|
||||
incrementCallbackDecrease = undefined,
|
||||
onvalue,
|
||||
onstartHistoryTransaction,
|
||||
oncontextmenu,
|
||||
}: Props = $props();
|
||||
|
||||
// Label
|
||||
export let label: string | undefined = undefined;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
|
||||
// Disabled
|
||||
export let disabled = false;
|
||||
|
||||
// Value
|
||||
// When `value` is not provided (i.e. it's `undefined`), a dash is displayed.
|
||||
export let value: number | undefined = undefined; // NOTE: Do not update this directly, do so by calling `updateValue()` instead.
|
||||
export let min: number | undefined = undefined;
|
||||
export let max: number | undefined = undefined;
|
||||
export let isInteger = false;
|
||||
|
||||
// Number presentation
|
||||
export let displayDecimalPlaces = 2;
|
||||
export let unit = "";
|
||||
export let unitIsHiddenWhenEditing = 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.
|
||||
export let mode: NumberInputMode = "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`.
|
||||
export let step = 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.
|
||||
export let incrementBehavior: NumberInputIncrementBehavior = "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.
|
||||
export let rangeMin = 0;
|
||||
export let rangeMax = 1;
|
||||
|
||||
// Styling
|
||||
export let minWidth = 0;
|
||||
export let maxWidth = 0;
|
||||
|
||||
// Callbacks
|
||||
export let incrementCallbackIncrease: (() => void) | undefined = undefined;
|
||||
export let incrementCallbackDecrease: (() => void) | undefined = undefined;
|
||||
|
||||
let self: FieldInput | undefined;
|
||||
let inputRangeElement: HTMLInputElement | undefined;
|
||||
let text = displayText(value, unit);
|
||||
let self: FieldInput | undefined = $state();
|
||||
let inputRangeElement: HTMLInputElement | undefined = $state();
|
||||
let text = $state(displayText(value, unit));
|
||||
let editing = false;
|
||||
let isDragging = false;
|
||||
let pressingArrow = false;
|
||||
let repeatTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
||||
// Stays in sync with a binding to the actual input range slider element.
|
||||
let rangeSliderValue = value !== undefined ? value : 0;
|
||||
let rangeSliderValue = $state(value !== undefined ? 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 "Deciding" state, when it has the previous location before the user's mousedown.
|
||||
let rangeSliderValueAsRendered = value !== undefined ? value : 0;
|
||||
let rangeSliderValueAsRendered = $state(value !== undefined ? value : 0);
|
||||
// Keeps track of the state of the slider drag as the user transitions through steps of the input process.
|
||||
// - "Ready": no interaction is happening.
|
||||
// - "Deciding": 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.
|
||||
// - "Aborted": the user has right clicked or pressed Escape to abort the drag, but hasn't yet released all mouse buttons.
|
||||
let rangeSliderClickDragState: "Ready" | "Deciding" | "Dragging" | "Aborted" = "Ready";
|
||||
let rangeSliderClickDragState: "Ready" | "Deciding" | "Dragging" | "Aborted" = $state("Ready");
|
||||
// Stores the initial value upon beginning to drag so it can be restored upon aborting. Set to `undefined` when not dragging.
|
||||
let initialValueBeforeDragging: number | undefined = undefined;
|
||||
// Stores the total value change during the process of dragging the slider. Set to 0 when not dragging.
|
||||
|
@ -84,14 +107,7 @@
|
|||
// Track whether the Ctrl key is currently held down.
|
||||
let ctrlKeyDown = false;
|
||||
|
||||
$: watchValue(value, unit);
|
||||
|
||||
$: sliderStepValue = isInteger ? (step === undefined ? 1 : step) : "any";
|
||||
$: styles = {
|
||||
...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}),
|
||||
...(maxWidth > 0 ? { "max-width": `${maxWidth}px` } : {}),
|
||||
...(mode === "Range" ? { "--progress-factor": Math.min(Math.max((rangeSliderValueAsRendered - rangeMin) / (rangeMax - rangeMin), 0), 1) } : {}),
|
||||
};
|
||||
|
||||
// Keep track of the Ctrl key being held down.
|
||||
const trackCtrl = (e: KeyboardEvent | MouseEvent) => (ctrlKeyDown = e.ctrlKey);
|
||||
|
@ -99,12 +115,16 @@
|
|||
addEventListener("keydown", trackCtrl);
|
||||
addEventListener("keyup", trackCtrl);
|
||||
addEventListener("mousemove", trackCtrl);
|
||||
|
||||
return () => {
|
||||
removeEventListener("keydown", trackCtrl);
|
||||
removeEventListener("keyup", trackCtrl);
|
||||
removeEventListener("mousemove", trackCtrl);
|
||||
}
|
||||
});
|
||||
onDestroy(() => {
|
||||
removeEventListener("keydown", trackCtrl);
|
||||
removeEventListener("keyup", trackCtrl);
|
||||
removeEventListener("mousemove", trackCtrl);
|
||||
});
|
||||
// onDestroy(() => {
|
||||
|
||||
// });
|
||||
|
||||
// ===============================
|
||||
// TRACKING AND UPDATING THE VALUE
|
||||
|
@ -152,7 +172,7 @@
|
|||
|
||||
text = displayText(newValueValidated, unit);
|
||||
|
||||
if (newValue !== undefined) dispatch("value", newValueValidated);
|
||||
if (newValue !== undefined) onvalue?.(newValueValidated);
|
||||
|
||||
// For any caller that needs to know what the value was changed to, we return it here
|
||||
return newValueValidated;
|
||||
|
@ -212,7 +232,7 @@
|
|||
|
||||
if (newValue !== undefined) {
|
||||
const oldValue = value !== undefined && isInteger ? Math.round(value) : value;
|
||||
if (newValue !== oldValue) dispatch("startHistoryTransaction");
|
||||
if (newValue !== oldValue) onstartHistoryTransaction?.();
|
||||
}
|
||||
updateValue(newValue);
|
||||
|
||||
|
@ -541,7 +561,7 @@
|
|||
function startDragging() {
|
||||
// This event is sent to the backend so it knows to start a transaction for the history system. See discussion for some explanation:
|
||||
// <https://github.com/GraphiteEditor/Graphite/pull/1584#discussion_r1477592483>
|
||||
dispatch("startHistoryTransaction");
|
||||
onstartHistoryTransaction?.()
|
||||
}
|
||||
|
||||
// We want to let the user abort while dragging the slider by right clicking or pressing Escape.
|
||||
|
@ -632,6 +652,15 @@
|
|||
removeEventListener("pointermove", sliderAbortFromDragging);
|
||||
removeEventListener("keydown", sliderAbortFromDragging);
|
||||
}
|
||||
$effect(() => {
|
||||
watchValue(value, unit);
|
||||
});
|
||||
let sliderStepValue = $derived(isInteger ? (step === undefined ? 1 : step) : "any");
|
||||
let styles = $derived({
|
||||
...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}),
|
||||
...(maxWidth > 0 ? { "max-width": `${maxWidth}px` } : {}),
|
||||
...(mode === "Range" ? { "--progress-factor": Math.min(Math.max((rangeSliderValueAsRendered - rangeMin) / (rangeMax - rangeMin), 0), 1) } : {}),
|
||||
});
|
||||
</script>
|
||||
|
||||
<FieldInput
|
||||
|
@ -640,12 +669,11 @@
|
|||
increment: mode === "Increment",
|
||||
range: mode === "Range",
|
||||
}}
|
||||
value={text}
|
||||
on:value={({ detail }) => (text = detail)}
|
||||
on:textFocused={onTextFocused}
|
||||
on:textChanged={onTextChanged}
|
||||
on:textChangeCanceled={onTextChangeCanceled}
|
||||
on:pointerdown={onDragPointerDown}
|
||||
bind:value={text}
|
||||
onfocus={onTextFocused}
|
||||
onchange={onTextChanged}
|
||||
ontextChangeCanceled={onTextChangeCanceled}
|
||||
onpointerdown={onDragPointerDown}
|
||||
{label}
|
||||
{disabled}
|
||||
{tooltip}
|
||||
|
@ -658,18 +686,18 @@
|
|||
{#if mode === "Increment" && incrementBehavior !== "None"}
|
||||
<button
|
||||
class="arrow left"
|
||||
on:pointerdown={(e) => onIncrementPointerDown(e, "Decrease")}
|
||||
on:mousedown={incrementPressAbort}
|
||||
on:pointerup={onIncrementPointerUp}
|
||||
on:pointerleave={onIncrementPointerUp}
|
||||
onpointerdown={(e) => onIncrementPointerDown(e, "Decrease")}
|
||||
onmousedown={incrementPressAbort}
|
||||
onpointerup={onIncrementPointerUp}
|
||||
onpointerleave={onIncrementPointerUp}
|
||||
tabindex="-1"
|
||||
></button>
|
||||
<button
|
||||
class="arrow right"
|
||||
on:pointerdown={(e) => onIncrementPointerDown(e, "Increase")}
|
||||
on:mousedown={incrementPressAbort}
|
||||
on:pointerup={onIncrementPointerUp}
|
||||
on:pointerleave={onIncrementPointerUp}
|
||||
onpointerdown={(e) => onIncrementPointerDown(e, "Increase")}
|
||||
onmousedown={incrementPressAbort}
|
||||
onpointerup={onIncrementPointerUp}
|
||||
onpointerleave={onIncrementPointerUp}
|
||||
tabindex="-1"
|
||||
></button>
|
||||
{/if}
|
||||
|
@ -680,20 +708,23 @@
|
|||
class="slider"
|
||||
class:hidden={rangeSliderClickDragState === "Deciding"}
|
||||
{disabled}
|
||||
min={rangeMin}
|
||||
min={rangeMin}
|
||||
max={rangeMax}
|
||||
step={sliderStepValue}
|
||||
bind:value={rangeSliderValue}
|
||||
on:input={onSliderInput}
|
||||
on:pointerup={onSliderPointerUp}
|
||||
on:contextmenu|preventDefault
|
||||
on:wheel={(e) => /* Stops slider eating the scroll event in Firefox */ e.target instanceof HTMLInputElement && e.target.blur()}
|
||||
oninput={onSliderInput}
|
||||
onpointerup={onSliderPointerUp}
|
||||
oncontextmenu={(event) => {
|
||||
event.preventDefault();
|
||||
oncontextmenu?.(event);
|
||||
}}
|
||||
onwheel={(e) => /* Stops slider eating the scroll event in Firefox */ e.target instanceof HTMLInputElement && e.target.blur()}
|
||||
bind:this={inputRangeElement}
|
||||
/>
|
||||
{#if rangeSliderClickDragState === "Deciding"}
|
||||
<div class="fake-slider-thumb" />
|
||||
<div class="fake-slider-thumb"></div>
|
||||
{/if}
|
||||
<div class="slider-progress" />
|
||||
<div class="slider-progress"></div>
|
||||
{/if}
|
||||
{/if}
|
||||
</FieldInput>
|
||||
|
|
|
@ -1,24 +1,31 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import { type RadioEntries, type RadioEntryData } from "@graphite/messages";
|
||||
import { type RadioEntries, type RadioEntryData } from "@graphite/messages.svelte";
|
||||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher<{ selectedIndex: number }>();
|
||||
interface Props {
|
||||
entries: RadioEntries;
|
||||
selectedIndex?: number | undefined;
|
||||
disabled?: boolean;
|
||||
minWidth?: number;
|
||||
onselect?: (position: number) => void;
|
||||
}
|
||||
|
||||
export let entries: RadioEntries;
|
||||
export let selectedIndex: number | undefined = undefined;
|
||||
export let disabled = false;
|
||||
export let minWidth = 0;
|
||||
let {
|
||||
entries,
|
||||
selectedIndex = undefined,
|
||||
disabled = false,
|
||||
minWidth = 0,
|
||||
onselect
|
||||
}: Props = $props();
|
||||
|
||||
$: mixed = selectedIndex === undefined && !disabled;
|
||||
let mixed = $derived(selectedIndex === undefined && !disabled);
|
||||
|
||||
function handleEntryClick(radioEntryData: RadioEntryData) {
|
||||
const index = entries.indexOf(radioEntryData);
|
||||
dispatch("selectedIndex", index);
|
||||
onselect?.(index);
|
||||
|
||||
radioEntryData.action?.();
|
||||
}
|
||||
|
@ -26,7 +33,7 @@
|
|||
|
||||
<LayoutRow class="radio-input" classes={{ disabled, mixed }} styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }}>
|
||||
{#each entries as entry, index}
|
||||
<button class:active={!mixed ? index === selectedIndex : undefined} on:click={() => handleEntryClick(entry)} title={entry.tooltip} tabindex={index === selectedIndex ? -1 : 0} {disabled}>
|
||||
<button class:active={!mixed ? index === selectedIndex : undefined} onclick={() => handleEntryClick(entry)} title={entry.tooltip} tabindex={index === selectedIndex ? -1 : 0} {disabled}>
|
||||
{#if entry.icon}
|
||||
<IconLabel icon={entry.icon} />
|
||||
{/if}
|
||||
|
|
|
@ -1,28 +1,34 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type { ReferencePoint } from "@graphite/messages.svelte";
|
||||
|
||||
import type { ReferencePoint } from "@graphite/messages";
|
||||
interface Props {
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
onvalue?: (point: ReferencePoint) => void;
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher<{ value: ReferencePoint }>();
|
||||
let { value, disabled = false, onvalue }: Props = $props();
|
||||
|
||||
export let value: string;
|
||||
export let disabled = false;
|
||||
|
||||
function setValue(newValue: ReferencePoint) {
|
||||
dispatch("value", newValue);
|
||||
function setValue(event: MouseEvent) {
|
||||
const element = event.target as HTMLDivElement;
|
||||
const button = element.parentElement;
|
||||
if (button instanceof HTMLButtonElement) {
|
||||
let position = button.dataset.position! as ReferencePoint;
|
||||
onvalue?.(position);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="reference-point-input" class:disabled>
|
||||
<button on:click={() => setValue("TopLeft")} class="row-1 col-1" class:active={value === "TopLeft"} tabindex="-1" {disabled}><div /></button>
|
||||
<button on:click={() => setValue("TopCenter")} class="row-1 col-2" class:active={value === "TopCenter"} tabindex="-1" {disabled}><div /></button>
|
||||
<button on:click={() => setValue("TopRight")} class="row-1 col-3" class:active={value === "TopRight"} tabindex="-1" {disabled}><div /></button>
|
||||
<button on:click={() => setValue("CenterLeft")} class="row-2 col-1" class:active={value === "CenterLeft"} tabindex="-1" {disabled}><div /></button>
|
||||
<button on:click={() => setValue("Center")} class="row-2 col-2" class:active={value === "Center"} tabindex="-1" {disabled}><div /></button>
|
||||
<button on:click={() => setValue("CenterRight")} class="row-2 col-3" class:active={value === "CenterRight"} tabindex="-1" {disabled}><div /></button>
|
||||
<button on:click={() => setValue("BottomLeft")} class="row-3 col-1" class:active={value === "BottomLeft"} tabindex="-1" {disabled}><div /></button>
|
||||
<button on:click={() => setValue("BottomCenter")} class="row-3 col-2" class:active={value === "BottomCenter"} tabindex="-1" {disabled}><div /></button>
|
||||
<button on:click={() => setValue("BottomRight")} class="row-3 col-3" class:active={value === "BottomRight"} tabindex="-1" {disabled}><div /></button>
|
||||
<div class="reference-point-input" class:disabled onclick={setValue}>
|
||||
<button data-position="TopLeft" class="row-1 col-1" class:active={value === "TopLeft"} tabindex="-1" {disabled}><div></div></button>
|
||||
<button data-position="TopCenter" class="row-1 col-2" class:active={value === "TopCenter"} tabindex="-1" {disabled}><div></div></button>
|
||||
<button data-position="TopRight" class="row-1 col-3" class:active={value === "TopRight"} tabindex="-1" {disabled}><div></div></button>
|
||||
<button data-position="CenterLeft" class="row-2 col-1" class:active={value === "CenterLeft"} tabindex="-1" {disabled}><div></div></button>
|
||||
<button data-position="Center" class="row-2 col-2" class:active={value === "Center"} tabindex="-1" {disabled}><div></div></button>
|
||||
<button data-position="CenterRight" class="row-2 col-3" class:active={value === "CenterRight"} tabindex="-1" {disabled}><div></div></button>
|
||||
<button data-position="BottomLeft" class="row-3 col-1" class:active={value === "BottomLeft"} tabindex="-1" {disabled}><div></div></button>
|
||||
<button data-position="BottomCenter" class="row-3 col-2" class:active={value === "BottomCenter"} tabindex="-1" {disabled}><div></div></button>
|
||||
<button data-position="BottomRight" class="row-3 col-3" class:active={value === "BottomRight"} tabindex="-1" {disabled}><div></div></button>
|
||||
</div>
|
||||
|
||||
<style lang="scss" global>
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
<script lang="ts" context="module">
|
||||
export type RulerDirection = "Horizontal" | "Vertical";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
|
@ -10,21 +6,30 @@
|
|||
const MINOR_MARK_THICKNESS = 6;
|
||||
const MICRO_MARK_THICKNESS = 3;
|
||||
|
||||
export let direction: RulerDirection = "Vertical";
|
||||
export let origin: number;
|
||||
export let numberInterval: number;
|
||||
export let majorMarkSpacing: number;
|
||||
export let minorDivisions = 5;
|
||||
export let microDivisions = 2;
|
||||
interface Props {
|
||||
direction?: Graphite.Axis;
|
||||
origin: number;
|
||||
numberInterval: number;
|
||||
majorMarkSpacing: number;
|
||||
minorDivisions?: number;
|
||||
microDivisions?: number;
|
||||
}
|
||||
|
||||
let rulerInput: HTMLDivElement | undefined;
|
||||
let rulerLength = 0;
|
||||
let svgBounds = { width: "0px", height: "0px" };
|
||||
let {
|
||||
direction = "Vertical",
|
||||
origin,
|
||||
numberInterval,
|
||||
majorMarkSpacing,
|
||||
minorDivisions = 5,
|
||||
microDivisions = 2
|
||||
}: Props = $props();
|
||||
|
||||
$: svgPath = computeSvgPath(direction, origin, majorMarkSpacing, minorDivisions, microDivisions, rulerLength);
|
||||
$: svgTexts = computeSvgTexts(direction, origin, majorMarkSpacing, numberInterval, rulerLength);
|
||||
let rulerInput: HTMLDivElement | undefined = $state();
|
||||
let rulerLength = $state(0);
|
||||
let svgBounds = $state({ width: "0px", height: "0px" });
|
||||
|
||||
function computeSvgPath(direction: RulerDirection, origin: number, majorMarkSpacing: number, minorDivisions: number, microDivisions: number, rulerLength: number): string {
|
||||
|
||||
function computeSvgPath(direction: Graphite.Axis, origin: number, majorMarkSpacing: number, minorDivisions: number, microDivisions: number, rulerLength: number): string {
|
||||
const isVertical = direction === "Vertical";
|
||||
const lineDirection = isVertical ? "H" : "V";
|
||||
|
||||
|
@ -51,7 +56,7 @@
|
|||
return dPathAttribute;
|
||||
}
|
||||
|
||||
function computeSvgTexts(direction: RulerDirection, origin: number, majorMarkSpacing: number, numberInterval: number, rulerLength: number): { transform: string; text: string }[] {
|
||||
function computeSvgTexts(direction: Graphite.Axis, origin: number, majorMarkSpacing: number, numberInterval: number, rulerLength: number): { transform: string; text: string }[] {
|
||||
const isVertical = direction === "Vertical";
|
||||
|
||||
const offsetStart = mod(origin, majorMarkSpacing);
|
||||
|
@ -102,6 +107,8 @@
|
|||
}
|
||||
|
||||
onMount(resize);
|
||||
let svgPath = $derived(computeSvgPath(direction, origin, majorMarkSpacing, minorDivisions, microDivisions, rulerLength));
|
||||
let svgTexts = $derived(computeSvgTexts(direction, origin, majorMarkSpacing, numberInterval, rulerLength));
|
||||
</script>
|
||||
|
||||
<div class={`ruler-input ${direction.toLowerCase()}`} bind:this={rulerInput}>
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
<script lang="ts" context="module">
|
||||
export type ScrollbarDirection = "Horizontal" | "Vertical";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import { PRESS_REPEAT_DELAY_MS, PRESS_REPEAT_INTERVAL_MS, PRESS_REPEAT_INTERVAL_RAPID_MS } from "@graphite/io-managers/input";
|
||||
|
||||
const ARROW_CLICK_DISTANCE = 0.05;
|
||||
|
@ -19,26 +13,38 @@
|
|||
|
||||
const clamp01 = (value: number): number => Math.min(Math.max(value, 0), 1);
|
||||
|
||||
const dispatch = createEventDispatcher<{ trackShift: number; thumbPosition: number; thumbDragStart: undefined; thumbDragEnd: undefined; thumbDragAbort: undefined }>();
|
||||
interface Props {
|
||||
direction?: Graphite.Axis;
|
||||
thumbPosition?: number;
|
||||
thumbLength?: number;
|
||||
ontrackShift?: (trackShift: number) => void;
|
||||
onthumbPosition?: (trackShift: number) => void;
|
||||
onthumbDragEnd?: () => void;
|
||||
onthumbDragStart?: () => void;
|
||||
onthumbDragAbort?: () => void;
|
||||
}
|
||||
|
||||
export let direction: ScrollbarDirection = "Vertical";
|
||||
export let thumbPosition = 0.5;
|
||||
export let thumbLength = 0.5;
|
||||
let {
|
||||
direction = "Vertical",
|
||||
thumbPosition = 0.5,
|
||||
thumbLength = 0.5,
|
||||
ontrackShift,
|
||||
onthumbPosition,
|
||||
onthumbDragEnd,
|
||||
onthumbDragStart,
|
||||
onthumbDragAbort,
|
||||
}: Props = $props();
|
||||
|
||||
let scrollTrack: HTMLDivElement | undefined;
|
||||
let dragging = false;
|
||||
let scrollTrack: HTMLDivElement | undefined = $state();
|
||||
let dragging = $state(false);
|
||||
let pressingTrack = false;
|
||||
let pressingArrow = false;
|
||||
let repeatTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
|
||||
let pointerPositionLastFrame = 0;
|
||||
let thumbTop: string | undefined = undefined;
|
||||
let thumbBottom: string | undefined = undefined;
|
||||
let thumbLeft: string | undefined = undefined;
|
||||
let thumbRight: string | undefined = undefined;
|
||||
|
||||
$: start = thumbToTrack(thumbLength, thumbPosition) - thumbLength / 2;
|
||||
$: end = 1 - thumbToTrack(thumbLength, thumbPosition) - thumbLength / 2;
|
||||
$: [thumbTop, thumbBottom, thumbLeft, thumbRight] = direction === "Vertical" ? [`${start * 100}%`, `${end * 100}%`, "0%", "0%"] : ["0%", "0%", `${start * 100}%`, `${end * 100}%`];
|
||||
let start = $derived(thumbToTrack(thumbLength, thumbPosition) - thumbLength / 2);
|
||||
let end = $derived(1 - thumbToTrack(thumbLength, thumbPosition) - thumbLength / 2);
|
||||
let [thumbTop, thumbBottom, thumbLeft, thumbRight] = $derived(direction === "Vertical" ? [`${start * 100}%`, `${end * 100}%`, "0%", "0%"] : ["0%", "0%", `${start * 100}%`, `${end * 100}%`]);
|
||||
|
||||
function trackLength(): number | undefined {
|
||||
if (scrollTrack === undefined) return undefined;
|
||||
|
@ -54,7 +60,7 @@
|
|||
if (dragging) return;
|
||||
|
||||
dragging = true;
|
||||
dispatch("thumbDragStart");
|
||||
onthumbDragStart?.();
|
||||
pointerPositionLastFrame = pointerPosition(e);
|
||||
|
||||
addEvents();
|
||||
|
@ -65,14 +71,14 @@
|
|||
if (!pressingArrow) return;
|
||||
|
||||
const distance = afterInitialDelay ? ARROW_REPEAT_DISTANCE : ARROW_CLICK_DISTANCE;
|
||||
dispatch("trackShift", -direction * distance);
|
||||
ontrackShift?.( -direction * distance);
|
||||
|
||||
if (afterInitialDelay) repeatTimeout = setTimeout(sendMove, PRESS_REPEAT_INTERVAL_RAPID_MS);
|
||||
afterInitialDelay = true;
|
||||
};
|
||||
|
||||
pressingArrow = true;
|
||||
dispatch("thumbDragStart");
|
||||
onthumbDragStart?.();
|
||||
let afterInitialDelay = false;
|
||||
sendMove();
|
||||
repeatTimeout = setTimeout(sendMove, PRESS_REPEAT_DELAY_MS);
|
||||
|
@ -108,13 +114,13 @@
|
|||
}
|
||||
|
||||
const move = newPointer - oldPointer < 0 ? 1 : -1;
|
||||
dispatch("trackShift", move);
|
||||
ontrackShift?.(move);
|
||||
|
||||
if (afterInitialDelay) repeatTimeout = setTimeout(sendMove, PRESS_REPEAT_INTERVAL_MS);
|
||||
afterInitialDelay = true;
|
||||
};
|
||||
|
||||
dispatch("thumbDragStart");
|
||||
onthumbDragStart?.();
|
||||
pressingTrack = true;
|
||||
let afterInitialDelay = false;
|
||||
sendMove();
|
||||
|
@ -128,17 +134,17 @@
|
|||
pressingTrack = false;
|
||||
pressingArrow = false;
|
||||
clearTimeout(repeatTimeout);
|
||||
dispatch("thumbDragAbort");
|
||||
onthumbDragAbort?.();
|
||||
}
|
||||
|
||||
if (dragging) {
|
||||
dragging = false;
|
||||
dispatch("thumbDragAbort");
|
||||
onthumbDragAbort?.();
|
||||
}
|
||||
}
|
||||
|
||||
function onPointerUp() {
|
||||
if (dragging) dispatch("thumbDragEnd");
|
||||
if (dragging) onthumbDragEnd?.();
|
||||
|
||||
dragging = false;
|
||||
pressingTrack = false;
|
||||
|
@ -172,7 +178,7 @@
|
|||
const dragDelta = positionPositionThisFrame - pointerPositionLastFrame;
|
||||
const movement = dragDelta / (length * (1 - thumbLength));
|
||||
const newThumbPosition = clamp01(thumbPosition + movement);
|
||||
dispatch("thumbPosition", newThumbPosition);
|
||||
onthumbPosition?.(newThumbPosition)
|
||||
|
||||
pointerPositionLastFrame = positionPositionThisFrame;
|
||||
|
||||
|
@ -207,11 +213,11 @@
|
|||
</script>
|
||||
|
||||
<div class={`scrollbar-input ${direction.toLowerCase()}`}>
|
||||
<button class="arrow decrease" on:pointerdown={() => pressArrow(-1)} tabindex="-1" data-scrollbar-arrow></button>
|
||||
<div class="scroll-track" on:pointerdown={pressTrack} bind:this={scrollTrack}>
|
||||
<div class="scroll-thumb" on:pointerdown={dragThumb} class:dragging style:top={thumbTop} style:bottom={thumbBottom} style:left={thumbLeft} style:right={thumbRight} />
|
||||
<button class="arrow decrease" onpointerdown={() => pressArrow(-1)} tabindex="-1" data-scrollbar-arrow></button>
|
||||
<div class="scroll-track" onpointerdown={pressTrack} bind:this={scrollTrack}>
|
||||
<div class="scroll-thumb" onpointerdown={dragThumb} class:dragging style:top={thumbTop} style:bottom={thumbBottom} style:left={thumbLeft} style:right={thumbRight}></div>
|
||||
</div>
|
||||
<button class="arrow increase" on:pointerdown={() => pressArrow(1)} tabindex="-1" data-scrollbar-arrow></button>
|
||||
<button class="arrow increase" onpointerdown={() => pressArrow(1)} tabindex="-1" data-scrollbar-arrow></button>
|
||||
</div>
|
||||
|
||||
<style lang="scss" global>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher, onDestroy } from "svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import { Color, type Gradient } from "@graphite/messages";
|
||||
import { Color, type Gradient } from "@graphite/messages.svelte";
|
||||
|
||||
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
|
@ -10,22 +10,33 @@
|
|||
const BUTTON_LEFT = 0;
|
||||
const BUTTON_RIGHT = 2;
|
||||
|
||||
const dispatch = createEventDispatcher<{ activeMarkerIndexChange: number | undefined; gradient: Gradient; dragging: boolean }>();
|
||||
interface Props {
|
||||
activeMarkerIndex: number | undefined;
|
||||
drag: boolean;
|
||||
gradient: Gradient;
|
||||
ongradient?: (gradient: Gradient) => void;
|
||||
onactiveMarkerIndexChange?: (index: number | undefined) => void;
|
||||
}
|
||||
|
||||
export let gradient: Gradient;
|
||||
export let activeMarkerIndex = 0 as number | undefined;
|
||||
let {
|
||||
activeMarkerIndex = 0,
|
||||
drag = $bindable(),
|
||||
gradient,
|
||||
ongradient,
|
||||
onactiveMarkerIndexChange,
|
||||
}: Props = $props();
|
||||
// export let disabled = false;
|
||||
// export let tooltip: string | undefined = undefined;
|
||||
|
||||
let markerTrack: LayoutRow | undefined = undefined;
|
||||
let markerTrack: LayoutRow | undefined = $state(undefined);
|
||||
let positionRestore: number | undefined = undefined;
|
||||
let deletionRestore: boolean | undefined = undefined;
|
||||
|
||||
function markerPointerDown(e: PointerEvent, index: number) {
|
||||
// Left-click to select and begin potentially dragging
|
||||
if (e.button === BUTTON_LEFT) {
|
||||
activeMarkerIndex = index;
|
||||
dispatch("activeMarkerIndexChange", index);
|
||||
// activeMarkerIndex = index;
|
||||
onactiveMarkerIndexChange?.(index);
|
||||
addEvents();
|
||||
return;
|
||||
}
|
||||
|
@ -69,11 +80,9 @@
|
|||
if (index === -1) index = gradient.stops.length;
|
||||
|
||||
gradient.stops.splice(index, 0, { position, color });
|
||||
activeMarkerIndex = index;
|
||||
deletionRestore = true;
|
||||
|
||||
dispatch("activeMarkerIndexChange", index);
|
||||
dispatch("gradient", gradient);
|
||||
onactiveMarkerIndexChange?.(index);
|
||||
ongradient?.(gradient)
|
||||
|
||||
addEvents();
|
||||
}
|
||||
|
@ -91,15 +100,10 @@
|
|||
if (gradient.stops.length <= 2) return;
|
||||
|
||||
gradient.stops.splice(index, 1);
|
||||
if (gradient.stops.length === 0) {
|
||||
activeMarkerIndex = undefined;
|
||||
} else {
|
||||
activeMarkerIndex = Math.max(0, Math.min(gradient.stops.length - 1, index));
|
||||
}
|
||||
let newMarkerIndex = gradient.stops.length === 0 ? undefined : Math.max(0, Math.min(gradient.stops.length - 1, index));
|
||||
deletionRestore = undefined;
|
||||
|
||||
dispatch("activeMarkerIndexChange", activeMarkerIndex);
|
||||
dispatch("gradient", gradient);
|
||||
onactiveMarkerIndexChange?.(newMarkerIndex);
|
||||
ongradient?.(gradient);
|
||||
}
|
||||
|
||||
function moveMarker(e: PointerEvent, index: number) {
|
||||
|
@ -113,7 +117,7 @@
|
|||
if (deletionRestore === undefined) {
|
||||
deletionRestore = false;
|
||||
|
||||
dispatch("dragging", true);
|
||||
drag = true;
|
||||
}
|
||||
|
||||
setPosition(index, position);
|
||||
|
@ -124,10 +128,9 @@
|
|||
active.position = position;
|
||||
gradient.stops.sort((a, b) => a.position - b.position);
|
||||
if (gradient.stops.indexOf(active) !== activeMarkerIndex) {
|
||||
activeMarkerIndex = gradient.stops.indexOf(active);
|
||||
dispatch("activeMarkerIndexChange", gradient.stops.indexOf(active));
|
||||
onactiveMarkerIndexChange?.(gradient.stops.indexOf(active));
|
||||
}
|
||||
dispatch("gradient", gradient);
|
||||
ongradient?.(gradient);
|
||||
}
|
||||
|
||||
function abortDrag() {
|
||||
|
@ -148,7 +151,7 @@
|
|||
positionRestore = undefined;
|
||||
deletionRestore = undefined;
|
||||
|
||||
dispatch("dragging", false);
|
||||
drag = false;
|
||||
}
|
||||
|
||||
function onPointerMove(e: PointerEvent) {
|
||||
|
@ -187,11 +190,14 @@
|
|||
document.removeEventListener("keydown", onKeyDown);
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", deleteStop);
|
||||
onDestroy(() => {
|
||||
removeEvents();
|
||||
document.removeEventListener("keydown", deleteStop);
|
||||
});
|
||||
onMount(() => {
|
||||
document.addEventListener("keydown", deleteStop);
|
||||
|
||||
return () => {
|
||||
removeEvents();
|
||||
document.removeEventListener("keydown", deleteStop);
|
||||
}
|
||||
})
|
||||
|
||||
// # Backend -> Frontend
|
||||
// Populate(gradient, { position, color }[], active) // The only way indexes get changed. Frontend drops marker if it's being dragged.
|
||||
|
@ -218,7 +224,7 @@
|
|||
"--gradient-stops": gradient.toLinearGradientCSS(),
|
||||
}}
|
||||
>
|
||||
<LayoutRow class="gradient-strip" on:pointerdown={insertStop}></LayoutRow>
|
||||
<LayoutRow class="gradient-strip" onpointerdown={insertStop}></LayoutRow>
|
||||
<LayoutRow class="marker-track" bind:this={markerTrack}>
|
||||
{#each gradient.stops as marker, index}
|
||||
<svg
|
||||
|
@ -226,7 +232,7 @@
|
|||
class:active={index === activeMarkerIndex}
|
||||
style:--marker-position={marker.position}
|
||||
style:--marker-color={marker.color.toRgbCSS()}
|
||||
on:pointerdown={(e) => markerPointerDown(e, index)}
|
||||
onpointerdown={(e) => markerPointerDown(e, index)}
|
||||
data-gradient-marker
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 12 12"
|
||||
|
|
|
@ -1,16 +1,23 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import FieldInput from "@graphite/components/widgets/inputs/FieldInput.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher<{ commitText: string }>();
|
||||
interface Props {
|
||||
value: string;
|
||||
label?: string | undefined;
|
||||
tooltip?: string | undefined;
|
||||
disabled?: boolean;
|
||||
oncommitText?: (arg1: string) => void;
|
||||
}
|
||||
|
||||
export let value: string;
|
||||
export let label: string | undefined = undefined;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
export let disabled = false;
|
||||
let {
|
||||
value = $bindable(),
|
||||
label = undefined,
|
||||
tooltip = undefined,
|
||||
disabled = false,
|
||||
oncommitText
|
||||
}: Props = $props();
|
||||
|
||||
let self: FieldInput | undefined;
|
||||
let self: FieldInput | undefined = $state();
|
||||
let editing = false;
|
||||
|
||||
function onTextFocused() {
|
||||
|
@ -26,7 +33,7 @@
|
|||
onTextChangeCanceled();
|
||||
|
||||
// TODO: Find a less hacky way to do this
|
||||
if (self) dispatch("commitText", self.getValue());
|
||||
if (self) oncommitText?.(self.getValue());
|
||||
|
||||
// Required if value is not changed by the parent component upon update:value event
|
||||
self?.setInputElementValue(self.getValue());
|
||||
|
@ -46,11 +53,10 @@
|
|||
<FieldInput
|
||||
class="text-area-input"
|
||||
classes={{ "has-label": Boolean(label) }}
|
||||
{value}
|
||||
on:value
|
||||
on:textFocused={onTextFocused}
|
||||
on:textChanged={onTextChanged}
|
||||
on:textChangeCanceled={onTextChangeCanceled}
|
||||
bind:value
|
||||
onfocus={onTextFocused}
|
||||
onchange={onTextChanged}
|
||||
ontextChangeCanceled={onTextChangeCanceled}
|
||||
textarea={true}
|
||||
spellcheck={true}
|
||||
{label}
|
||||
|
|
|
@ -1,27 +1,37 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import FieldInput from "@graphite/components/widgets/inputs/FieldInput.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher<{ commitText: string }>();
|
||||
interface Props {
|
||||
// Label
|
||||
label?: string | undefined;
|
||||
tooltip?: string | undefined;
|
||||
placeholder?: string | undefined;
|
||||
// Disabled
|
||||
disabled?: boolean;
|
||||
// Value
|
||||
value: string;
|
||||
// Styling
|
||||
centered?: boolean;
|
||||
minWidth?: number;
|
||||
class?: string;
|
||||
classes?: Record<string, boolean>;
|
||||
oncommitText?: (arg1: string) => void;
|
||||
}
|
||||
|
||||
// Label
|
||||
export let label: string | undefined = undefined;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
export let placeholder: string | undefined = undefined;
|
||||
// Disabled
|
||||
export let disabled = false;
|
||||
// Value
|
||||
export let value: string;
|
||||
// Styling
|
||||
export let centered = false;
|
||||
export let minWidth = 0;
|
||||
let {
|
||||
label = undefined,
|
||||
tooltip = undefined,
|
||||
placeholder = undefined,
|
||||
disabled = false,
|
||||
value = $bindable(),
|
||||
centered = false,
|
||||
minWidth = 0,
|
||||
class: className = "",
|
||||
classes = {},
|
||||
oncommitText,
|
||||
}: Props = $props();
|
||||
|
||||
let className = "";
|
||||
export { className as class };
|
||||
export let classes: Record<string, boolean> = {};
|
||||
|
||||
let self: FieldInput | undefined;
|
||||
let self: FieldInput | undefined = $state();
|
||||
let editing = false;
|
||||
|
||||
function onTextFocused() {
|
||||
|
@ -39,7 +49,7 @@
|
|||
onTextChangeCanceled();
|
||||
|
||||
// TODO: Find a less hacky way to do this
|
||||
if (self) dispatch("commitText", self.getValue());
|
||||
if (self) oncommitText?.(self.getValue());
|
||||
|
||||
// Required if value is not changed by the parent component upon update:value event
|
||||
self?.setInputElementValue(self.getValue());
|
||||
|
@ -64,11 +74,10 @@
|
|||
class={`text-input ${className}`.trim()}
|
||||
classes={{ centered, ...classes }}
|
||||
styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }}
|
||||
{value}
|
||||
on:value
|
||||
on:textFocused={onTextFocused}
|
||||
on:textChanged={onTextChanged}
|
||||
on:textChangeCanceled={onTextChangeCanceled}
|
||||
bind:value
|
||||
onfocus={onTextFocused}
|
||||
onchange={onTextChanged}
|
||||
ontextChangeCanceled={onTextChangeCanceled}
|
||||
spellcheck={true}
|
||||
{label}
|
||||
{disabled}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { getContext } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import { Color } from "@graphite/messages";
|
||||
import { Color } from "@graphite/messages.svelte";
|
||||
|
||||
import ColorPicker from "@graphite/components/floating-menus/ColorPicker.svelte";
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
|
@ -10,11 +10,15 @@
|
|||
|
||||
const editor = getContext<Editor>("editor");
|
||||
|
||||
export let primary: Color;
|
||||
export let secondary: Color;
|
||||
interface Props {
|
||||
primary: Color;
|
||||
secondary: Color;
|
||||
}
|
||||
|
||||
let primaryOpen = false;
|
||||
let secondaryOpen = false;
|
||||
let { primary, secondary }: Props = $props();
|
||||
|
||||
let primaryOpen = $state(false);
|
||||
let secondaryOpen = $state(false);
|
||||
|
||||
function clickPrimarySwatch() {
|
||||
primaryOpen = true;
|
||||
|
@ -37,22 +41,20 @@
|
|||
|
||||
<LayoutCol class="working-colors-button">
|
||||
<LayoutRow class="primary swatch">
|
||||
<button on:click={clickPrimarySwatch} class:open={primaryOpen} style:--swatch-color={primary.toRgbaCSS()} data-floating-menu-spawner="no-hover-transfer" tabindex="0"></button>
|
||||
<button onclick={clickPrimarySwatch} class:open={primaryOpen} style:--swatch-color={primary.toRgbaCSS()} data-floating-menu-spawner="no-hover-transfer" tabindex="0"></button>
|
||||
<ColorPicker
|
||||
open={primaryOpen}
|
||||
on:open={({ detail }) => (primaryOpen = detail)}
|
||||
bind:open={primaryOpen}
|
||||
colorOrGradient={primary}
|
||||
on:colorOrGradient={({ detail }) => detail instanceof Color && primaryColorChanged(detail)}
|
||||
oncolorOrGradient={(detail) => detail instanceof Color && primaryColorChanged(detail)}
|
||||
direction="Right"
|
||||
/>
|
||||
</LayoutRow>
|
||||
<LayoutRow class="secondary swatch">
|
||||
<button on:click={clickSecondarySwatch} class:open={secondaryOpen} style:--swatch-color={secondary.toRgbaCSS()} data-floating-menu-spawner="no-hover-transfer" tabindex="0"></button>
|
||||
<button onclick={clickSecondarySwatch} class:open={secondaryOpen} style:--swatch-color={secondary.toRgbaCSS()} data-floating-menu-spawner="no-hover-transfer" tabindex="0"></button>
|
||||
<ColorPicker
|
||||
open={secondaryOpen}
|
||||
on:open={({ detail }) => (secondaryOpen = detail)}
|
||||
bind:open={secondaryOpen}
|
||||
colorOrGradient={secondary}
|
||||
on:colorOrGradient={({ detail }) => detail instanceof Color && secondaryColorChanged(detail)}
|
||||
oncolorOrGradient={(detail) => detail instanceof Color && secondaryColorChanged(detail)}
|
||||
direction="Right"
|
||||
/>
|
||||
</LayoutRow>
|
||||
|
|
|
@ -3,15 +3,25 @@
|
|||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
|
||||
let className = "";
|
||||
export { className as class };
|
||||
export let classes: Record<string, boolean> = {};
|
||||
export let icon: IconName;
|
||||
export let iconSizeOverride: number | undefined = undefined;
|
||||
export let disabled = false;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
interface Props {
|
||||
class?: string;
|
||||
classes?: Record<string, boolean>;
|
||||
icon: IconName;
|
||||
iconSizeOverride?: number | undefined;
|
||||
disabled?: boolean;
|
||||
tooltip?: string | undefined;
|
||||
}
|
||||
|
||||
$: iconSizeClass = ((icon: IconName) => {
|
||||
let {
|
||||
class: className = "",
|
||||
classes = {},
|
||||
icon,
|
||||
iconSizeOverride = undefined,
|
||||
disabled = false,
|
||||
tooltip = undefined
|
||||
}: Props = $props();
|
||||
|
||||
let iconSizeClass = $derived(((icon: IconName) => {
|
||||
const iconData = ICONS[icon];
|
||||
if (!iconData) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
@ -20,10 +30,10 @@
|
|||
}
|
||||
if (iconData.size === undefined) return "";
|
||||
return `size-${iconSizeOverride || iconData.size}`;
|
||||
})(icon);
|
||||
$: extraClasses = Object.entries(classes)
|
||||
})(icon));
|
||||
let extraClasses = $derived(Object.entries(classes)
|
||||
.flatMap(([className, stateName]) => (stateName ? [className] : []))
|
||||
.join(" ");
|
||||
.join(" "));
|
||||
</script>
|
||||
|
||||
<LayoutRow class={`icon-label ${iconSizeClass} ${className} ${extraClasses}`.trim()} classes={{ disabled }} {tooltip}>
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
<script lang="ts">
|
||||
import { type SeparatorDirection, type SeparatorType } from "@graphite/messages";
|
||||
import { type SeparatorDirection, type SeparatorType } from "@graphite/messages.svelte";
|
||||
|
||||
export let direction: SeparatorDirection = "Horizontal";
|
||||
export let type: SeparatorType = "Unrelated";
|
||||
interface Props {
|
||||
direction?: SeparatorDirection;
|
||||
type?: SeparatorType;
|
||||
}
|
||||
|
||||
let { direction = "Horizontal", type = "Unrelated" }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class={`separator ${direction.toLowerCase()} ${type.toLowerCase()}`}>
|
||||
{#if type === "Section"}
|
||||
<div />
|
||||
<div></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,26 +1,48 @@
|
|||
<script lang="ts">
|
||||
let className = "";
|
||||
export { className as class };
|
||||
export let classes: Record<string, boolean> = {};
|
||||
let styleName = "";
|
||||
export { styleName as style };
|
||||
export let styles: Record<string, string | number | undefined> = {};
|
||||
export let disabled = false;
|
||||
export let bold = false;
|
||||
export let italic = false;
|
||||
export let centerAlign = false;
|
||||
export let tableAlign = false;
|
||||
export let minWidth = 0;
|
||||
export let multiline = false;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
export let checkboxId: bigint | undefined = undefined;
|
||||
import type { SvelteHTMLElements } from 'svelte/elements';
|
||||
|
||||
type LabelHTMLElementProps = SvelteHTMLElements["label"];
|
||||
|
||||
interface Props extends LabelHTMLElementProps {
|
||||
class?: string;
|
||||
classes?: Record<string, boolean>;
|
||||
style?: string;
|
||||
styles?: Record<string, string | number | undefined>;
|
||||
disabled?: boolean;
|
||||
bold?: boolean;
|
||||
italic?: boolean;
|
||||
centerAlign?: boolean;
|
||||
tableAlign?: boolean;
|
||||
minWidth?: number;
|
||||
multiline?: boolean;
|
||||
tooltip?: string | undefined;
|
||||
checkboxId?: bigint | undefined;
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
$: extraClasses = Object.entries(classes)
|
||||
let {
|
||||
class: className = "",
|
||||
classes = {},
|
||||
style: styleName = "",
|
||||
styles = {},
|
||||
disabled = false,
|
||||
bold = false,
|
||||
italic = false,
|
||||
centerAlign = false,
|
||||
tableAlign = false,
|
||||
minWidth = 0,
|
||||
multiline = false,
|
||||
tooltip = undefined,
|
||||
checkboxId = undefined,
|
||||
children
|
||||
}: Props = $props();
|
||||
|
||||
let extraClasses = $derived(Object.entries(classes)
|
||||
.flatMap(([className, stateName]) => (stateName ? [className] : []))
|
||||
.join(" ");
|
||||
$: extraStyles = Object.entries(styles)
|
||||
.join(" "));
|
||||
let extraStyles = $derived(Object.entries(styles)
|
||||
.flatMap((styleAndValue) => (styleAndValue[1] !== undefined ? [`${styleAndValue[0]}: ${styleAndValue[1]};`] : []))
|
||||
.join(" ");
|
||||
.join(" "));
|
||||
</script>
|
||||
|
||||
<label
|
||||
|
@ -36,7 +58,7 @@
|
|||
title={tooltip}
|
||||
for={checkboxId !== undefined ? `checkbox-input-${checkboxId}` : undefined}
|
||||
>
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</label>
|
||||
|
||||
<style lang="scss" global>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
|
||||
import { type KeyRaw, type LayoutKeysGroup, type Key, type MouseMotion } from "@graphite/messages";
|
||||
import { type KeyRaw, type LayoutKeysGroup, type Key, type MouseMotion } from "@graphite/messages.svelte";
|
||||
import type { FullscreenState } from "@graphite/state-providers/fullscreen";
|
||||
import type { IconName } from "@graphite/utility-functions/icons";
|
||||
import { platformIsMac } from "@graphite/utility-functions/platform";
|
||||
|
@ -34,14 +34,21 @@
|
|||
|
||||
const fullscreen = getContext<FullscreenState>("fullscreen");
|
||||
|
||||
export let keysWithLabelsGroups: LayoutKeysGroup[] = [];
|
||||
export let mouseMotion: MouseMotion | undefined = undefined;
|
||||
export let requiresLock = false;
|
||||
export let textOnly = false;
|
||||
interface Props {
|
||||
keysWithLabelsGroups?: LayoutKeysGroup[];
|
||||
mouseMotion?: MouseMotion | undefined;
|
||||
requiresLock?: boolean;
|
||||
textOnly?: boolean;
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
$: keyboardLockInfoMessage = watchKeyboardLockInfoMessage(fullscreen.keyboardLockApiSupported);
|
||||
|
||||
$: displayKeyboardLockNotice = requiresLock && !$fullscreen.keyboardLocked;
|
||||
let {
|
||||
keysWithLabelsGroups = [],
|
||||
mouseMotion = undefined,
|
||||
requiresLock = false,
|
||||
textOnly = false,
|
||||
children
|
||||
}: Props = $props();
|
||||
|
||||
function watchKeyboardLockInfoMessage(keyboardLockApiSupported: boolean): string {
|
||||
const RESERVED = "This hotkey is reserved by the browser. ";
|
||||
|
@ -114,6 +121,8 @@
|
|||
return undefined;
|
||||
}
|
||||
}
|
||||
let keyboardLockInfoMessage = $derived(watchKeyboardLockInfoMessage(fullscreen.keyboardLockApiSupported));
|
||||
let displayKeyboardLockNotice = $derived(requiresLock && !$fullscreen.keyboardLocked);
|
||||
</script>
|
||||
|
||||
{#if displayKeyboardLockNotice}
|
||||
|
@ -139,9 +148,9 @@
|
|||
<IconLabel icon={mouseHintIcon(mouseMotion)} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if $$slots.default}
|
||||
{#if children}
|
||||
<div class="hint-text">
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
</LayoutRow>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { getContext, onMount } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import { type HintData, type HintInfo, type LayoutKeysGroup, UpdateInputHints } from "@graphite/messages";
|
||||
import { type HintData, type HintInfo, type LayoutKeysGroup, UpdateInputHints } from "@graphite/messages.svelte";
|
||||
import { platformIsMac } from "@graphite/utility-functions/platform";
|
||||
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
|
@ -11,7 +11,7 @@
|
|||
|
||||
const editor = getContext<Editor>("editor");
|
||||
|
||||
let hintData: HintData = [];
|
||||
let hintData: HintData = $state([]);
|
||||
|
||||
function inputKeysForPlatform(hint: HintInfo): LayoutKeysGroup[] {
|
||||
if (platformIsMac() && hint.keyGroupsMac) return hint.keyGroupsMac;
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
<script lang="ts" context="module">
|
||||
export type Platform = "Windows" | "Mac" | "Linux" | "Web";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import { type KeyRaw, type LayoutKeysGroup, type MenuBarEntry, type MenuListEntry, UpdateMenuBarLayout } from "@graphite/messages";
|
||||
import { type KeyRaw, type LayoutKeysGroup, type MenuBarEntry, type MenuListEntry, UpdateMenuBarLayout } from "@graphite/messages.svelte";
|
||||
import type { PortfolioState } from "@graphite/state-providers/portfolio";
|
||||
import { platformIsMac } from "@graphite/utility-functions/platform";
|
||||
|
||||
|
@ -17,8 +13,12 @@
|
|||
import WindowButtonsWindows from "@graphite/components/window/title-bar/WindowButtonsWindows.svelte";
|
||||
import WindowTitle from "@graphite/components/window/title-bar/WindowTitle.svelte";
|
||||
|
||||
export let platform: Platform;
|
||||
export let maximized: boolean;
|
||||
interface Props {
|
||||
platform: Graphite.Platform;
|
||||
maximized: boolean;
|
||||
}
|
||||
|
||||
let { platform, maximized }: Props = $props();
|
||||
|
||||
const editor = getContext<Editor>("editor");
|
||||
const portfolio = getContext<PortfolioState>("portfolio");
|
||||
|
@ -33,11 +33,11 @@
|
|||
[ACCEL_KEY, "Shift", "KeyT"],
|
||||
];
|
||||
|
||||
let entries: MenuListEntry[] = [];
|
||||
let entries: MenuListEntry[] = $state([]);
|
||||
|
||||
$: docIndex = $portfolio.activeDocumentIndex;
|
||||
$: displayName = $portfolio.documents[docIndex]?.displayName || "";
|
||||
$: windowTitle = `${displayName}${displayName && " - "}Graphite`;
|
||||
let docIndex = $derived($portfolio.activeDocumentIndex);
|
||||
let displayName = $derived($portfolio.documents[docIndex]?.displayName || "");
|
||||
let windowTitle = $derived(`${displayName}${displayName && " - "}Graphite`);
|
||||
|
||||
onMount(() => {
|
||||
const arraysEqual = (a: KeyRaw[], b: KeyRaw[]): boolean => a.length === b.length && a.every((aValue, i) => aValue === b[i]);
|
||||
|
@ -76,7 +76,7 @@
|
|||
<WindowButtonsMac {maximized} />
|
||||
{:else}
|
||||
{#each entries as entry}
|
||||
<TextButton label={entry.label} icon={entry.icon} menuListChildren={entry.children} action={entry.action} flush={true} />
|
||||
<TextButton label={entry.label} icon={entry.icon} menuListChildren={entry.children} onclick={entry.action!} flush={true} />
|
||||
{/each}
|
||||
{/if}
|
||||
</LayoutRow>
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
<script lang="ts">
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
|
||||
export let maximized = false;
|
||||
interface Props {
|
||||
maximized?: boolean;
|
||||
}
|
||||
|
||||
let { maximized = false }: Props = $props();
|
||||
</script>
|
||||
|
||||
<LayoutRow class="window-buttons-mac">
|
||||
<div class="close" title="Close" />
|
||||
<div class="minimize" title={maximized ? "Minimize" : "Maximize"} />
|
||||
<div class="zoom" title="Zoom" />
|
||||
<div class="close" title="Close"></div>
|
||||
<div class="minimize" title={maximized ? "Minimize" : "Maximize"}></div>
|
||||
<div class="zoom" title="Zoom"></div>
|
||||
</LayoutRow>
|
||||
|
||||
<style lang="scss" global>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
const fullscreen = getContext<FullscreenState>("fullscreen");
|
||||
|
||||
$: requestFullscreenHotkeys = fullscreen.keyboardLockApiSupported && !$fullscreen.keyboardLocked;
|
||||
let requestFullscreenHotkeys = $derived(fullscreen.keyboardLockApiSupported && !$fullscreen.keyboardLocked);
|
||||
|
||||
async function handleClick() {
|
||||
if ($fullscreen.windowFullscreen) fullscreen.exitFullscreen();
|
||||
|
@ -17,7 +17,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<LayoutRow class="window-buttons-web" on:click={() => handleClick()} tooltip={($fullscreen.windowFullscreen ? "Exit" : "Enter") + " Fullscreen (F11)"}>
|
||||
<LayoutRow class="window-buttons-web" onclick={() => handleClick()} tooltip={($fullscreen.windowFullscreen ? "Exit" : "Enter") + " Fullscreen (F11)"}>
|
||||
{#if requestFullscreenHotkeys}
|
||||
<TextLabel italic={true}>Go fullscreen to access all hotkeys</TextLabel>
|
||||
{/if}
|
||||
|
|
|
@ -2,7 +2,11 @@
|
|||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||
|
||||
export let maximized = false;
|
||||
interface Props {
|
||||
maximized?: boolean;
|
||||
}
|
||||
|
||||
let { maximized = false }: Props = $props();
|
||||
</script>
|
||||
|
||||
<LayoutRow class="window-button windows minimize" tooltip="Minimize">
|
||||
|
|
|
@ -2,7 +2,11 @@
|
|||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
||||
export let text: string;
|
||||
interface Props {
|
||||
text: string;
|
||||
}
|
||||
|
||||
let { text }: Props = $props();
|
||||
</script>
|
||||
|
||||
<LayoutRow class="window-title">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<script lang="ts" context="module">
|
||||
<script lang="ts" module>
|
||||
import Document from "@graphite/components/panels/Document.svelte";
|
||||
import Layers from "@graphite/components/panels/Layers.svelte";
|
||||
import Properties from "@graphite/components/panels/Properties.svelte";
|
||||
|
@ -17,7 +17,7 @@
|
|||
import { getContext, tick } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import { type LayoutKeysGroup, type Key } from "@graphite/messages";
|
||||
import { type LayoutKeysGroup, type Key } from "@graphite/messages.svelte";
|
||||
import { platformIsMac, isEventSupported } from "@graphite/utility-functions/platform";
|
||||
|
||||
import { extractPixelData } from "@graphite/utility-functions/rasterization";
|
||||
|
@ -34,15 +34,27 @@
|
|||
|
||||
const editor = getContext<Editor>("editor");
|
||||
|
||||
export let tabMinWidths = false;
|
||||
export let tabCloseButtons = false;
|
||||
export let tabLabels: { name: string; tooltip?: string }[];
|
||||
export let tabActiveIndex: number;
|
||||
export let panelType: PanelType | undefined = undefined;
|
||||
export let clickAction: ((index: number) => void) | undefined = undefined;
|
||||
export let closeAction: ((index: number) => void) | undefined = undefined;
|
||||
interface Props {
|
||||
tabMinWidths?: boolean;
|
||||
tabCloseButtons?: boolean;
|
||||
tabLabels: { name: string; tooltip?: string }[];
|
||||
tabActiveIndex: number;
|
||||
panelType?: PanelType | undefined;
|
||||
clickAction?: ((index: number) => void) | undefined;
|
||||
closeAction?: ((index: number) => void) | undefined;
|
||||
}
|
||||
|
||||
let tabElements: (LayoutRow | undefined)[] = [];
|
||||
let {
|
||||
tabMinWidths = false,
|
||||
tabCloseButtons = false,
|
||||
tabLabels,
|
||||
tabActiveIndex,
|
||||
panelType = undefined,
|
||||
clickAction = undefined,
|
||||
closeAction = undefined
|
||||
}: Props = $props();
|
||||
|
||||
let tabElements: (LayoutRow | undefined)[] = $state([]);
|
||||
|
||||
function platformModifiers(reservedKey: boolean): LayoutKeysGroup {
|
||||
// TODO: Remove this by properly feeding these keys from a layout provided by the backend
|
||||
|
@ -90,7 +102,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<LayoutCol class="panel" on:pointerdown={() => panelType && editor.handle.setActivePanel(panelType)}>
|
||||
<LayoutCol class="panel" onpointerdown={() => panelType && editor.handle.setActivePanel(panelType)}>
|
||||
<LayoutRow class="tab-bar" classes={{ "min-widths": tabMinWidths }}>
|
||||
<LayoutRow class="tab-group" scrollableX={true}>
|
||||
{#each tabLabels as tabLabel, tabIndex}
|
||||
|
@ -98,18 +110,18 @@
|
|||
class="tab"
|
||||
classes={{ active: tabIndex === tabActiveIndex }}
|
||||
tooltip={tabLabel.tooltip || undefined}
|
||||
on:click={(e) => {
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
clickAction?.(tabIndex);
|
||||
}}
|
||||
on:auxclick={(e) => {
|
||||
onauxclick={(e) => {
|
||||
// Middle mouse button click
|
||||
if (e.button === BUTTON_MIDDLE) {
|
||||
e.stopPropagation();
|
||||
closeAction?.(tabIndex);
|
||||
}
|
||||
}}
|
||||
on:mouseup={(e) => {
|
||||
onmouseup={(e) => {
|
||||
// Middle mouse button click fallback for Safari:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event#browser_compatibility
|
||||
// The downside of using mouseup is that the mousedown didn't have to originate in the same element.
|
||||
|
@ -124,7 +136,7 @@
|
|||
<TextLabel>{tabLabel.name}</TextLabel>
|
||||
{#if tabCloseButtons}
|
||||
<IconButton
|
||||
action={(e) => {
|
||||
onclick={(e) => {
|
||||
e?.stopPropagation();
|
||||
closeAction?.(tabIndex);
|
||||
}}
|
||||
|
@ -142,41 +154,44 @@
|
|||
</LayoutRow>
|
||||
<LayoutCol class="panel-body">
|
||||
{#if panelType}
|
||||
<svelte:component this={PANEL_COMPONENTS[panelType]} />
|
||||
{@const SvelteComponent = PANEL_COMPONENTS[panelType]}
|
||||
<SvelteComponent />
|
||||
{:else}
|
||||
<LayoutCol class="empty-panel" on:dragover={(e) => e.preventDefault()} on:drop={dropFile}>
|
||||
<LayoutCol class="empty-panel" ondragover={(e) => e.preventDefault()} ondrop={dropFile}>
|
||||
<LayoutCol class="content">
|
||||
<LayoutRow class="logotype">
|
||||
<IconLabel icon="GraphiteLogotypeSolid" />
|
||||
</LayoutRow>
|
||||
<LayoutRow class="actions">
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<TextButton label="New Document" icon="File" flush={true} action={() => editor.handle.newDocumentDialog()} />
|
||||
</td>
|
||||
<td>
|
||||
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(true), { key: "KeyN", label: "N" }]]} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<TextButton label="Open Document" icon="Folder" flush={true} action={() => editor.handle.openDocument()} />
|
||||
</td>
|
||||
<td>
|
||||
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(false), { key: "KeyO", label: "O" }]]} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<TextButton label="Open Demo Artwork" icon="Image" flush={true} action={() => editor.handle.demoArtworkDialog()} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<TextButton label="Support the Development Fund" icon="Heart" flush={true} action={() => editor.handle.visitUrl("https://graphite.rs/donate/")} />
|
||||
</td>
|
||||
</tr>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<TextButton label="New Document" icon="File" flush={true} onclick={() => editor.handle.newDocumentDialog()} />
|
||||
</td>
|
||||
<td>
|
||||
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(true), { key: "KeyN", label: "N" }]]} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<TextButton label="Open Document" icon="Folder" flush={true} onclick={() => editor.handle.openDocument()} />
|
||||
</td>
|
||||
<td>
|
||||
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(false), { key: "KeyO", label: "O" }]]} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<TextButton label="Open Demo Artwork" icon="Image" flush={true} onclick={() => editor.handle.demoArtworkDialog()} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<TextButton label="Support the Development Fund" icon="Heart" flush={true} onclick={() => editor.handle.visitUrl("https://graphite.rs/donate/")} />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</LayoutRow>
|
||||
</LayoutCol>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { getContext } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { FrontendDocumentDetails } from "@graphite/messages";
|
||||
import type { FrontendDocumentDetails } from "@graphite/messages.svelte";
|
||||
import type { DialogState } from "@graphite/state-providers/dialog";
|
||||
import type { PortfolioState } from "@graphite/state-providers/portfolio";
|
||||
|
||||
|
@ -22,21 +22,12 @@
|
|||
/* └─ */ layers: 55,
|
||||
};
|
||||
|
||||
let panelSizes = PANEL_SIZES;
|
||||
let documentPanel: Panel | undefined;
|
||||
let panelSizes = $state(PANEL_SIZES);
|
||||
let documentPanel: Panel | undefined = $state();
|
||||
let gutterResizeRestore: [number, number] | undefined = undefined;
|
||||
let pointerCaptureId: number | undefined = undefined;
|
||||
|
||||
$: documentPanel?.scrollTabIntoView($portfolio.activeDocumentIndex);
|
||||
|
||||
$: documentTabLabels = $portfolio.documents.map((doc: FrontendDocumentDetails) => {
|
||||
const name = doc.displayName;
|
||||
|
||||
if (!editor.handle.inDevelopmentMode()) return { name };
|
||||
|
||||
const tooltip = `Document ID: ${doc.id}`;
|
||||
return { name, tooltip };
|
||||
});
|
||||
|
||||
const editor = getContext<Editor>("editor");
|
||||
const portfolio = getContext<PortfolioState>("portfolio");
|
||||
|
@ -130,6 +121,17 @@
|
|||
|
||||
addListeners();
|
||||
}
|
||||
$effect(() => {
|
||||
documentPanel?.scrollTabIntoView($portfolio.activeDocumentIndex);
|
||||
});
|
||||
let documentTabLabels = $derived($portfolio.documents.map((doc: FrontendDocumentDetails) => {
|
||||
const name = doc.displayName;
|
||||
|
||||
if (!editor.handle.inDevelopmentMode()) return { name };
|
||||
|
||||
const tooltip = `Document ID: ${doc.id}`;
|
||||
return { name, tooltip };
|
||||
}));
|
||||
</script>
|
||||
|
||||
<LayoutRow class="workspace" data-workspace>
|
||||
|
@ -148,18 +150,18 @@
|
|||
/>
|
||||
</LayoutRow>
|
||||
{#if $portfolio.spreadsheetOpen}
|
||||
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical on:pointerdown={(e) => resizePanel(e)} />
|
||||
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical onpointerdown={(e) => resizePanel(e)} />
|
||||
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["spreadsheet"] }} data-subdivision-name="spreadsheet">
|
||||
<Panel panelType="Spreadsheet" tabLabels={[{ name: "Spreadsheet" }]} tabActiveIndex={0} />
|
||||
</LayoutRow>
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
<LayoutCol class="workspace-grid-resize-gutter" data-gutter-horizontal on:pointerdown={(e) => resizePanel(e)} />
|
||||
<LayoutCol class="workspace-grid-resize-gutter" data-gutter-horizontal onpointerdown={(e) => resizePanel(e)} />
|
||||
<LayoutCol class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["details"] }} data-subdivision-name="details">
|
||||
<LayoutRow class="workspace-grid-subdivision" styles={{ "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 on:pointerdown={(e) => resizePanel(e)} />
|
||||
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical onpointerdown={(e) => resizePanel(e)} />
|
||||
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["layers"] }} data-subdivision-name="layers">
|
||||
<Panel panelType="Layers" tabLabels={[{ name: "Layers" }]} tabActiveIndex={0} />
|
||||
</LayoutRow>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// import { panicProxy } from "@graphite/utility-functions/panic-proxy";
|
||||
import { type JsMessageType } from "@graphite/messages";
|
||||
import { type JsMessageType } from "@graphite/messages.svelte";
|
||||
import { createSubscriptionRouter, type SubscriptionRouter } from "@graphite/subscription-router";
|
||||
import init, { setRandomSeed, wasmMemory, EditorHandle } from "@graphite-frontend/wasm/pkg/graphite_wasm.js";
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { type Editor } from "@graphite/editor";
|
||||
import { TriggerTextCopy } from "@graphite/messages";
|
||||
import { TriggerTextCopy } from "@graphite/messages.svelte";
|
||||
|
||||
export function createClipboardManager(editor: Editor) {
|
||||
// Subscribe to process backend event
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { type Editor } from "@graphite/editor";
|
||||
import { TriggerVisitLink } from "@graphite/messages";
|
||||
import { TriggerVisitLink } from "@graphite/messages.svelte";
|
||||
|
||||
export function createHyperlinkManager(editor: Editor) {
|
||||
// Subscribe to process backend event
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { get } from "svelte/store";
|
||||
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import { TriggerPaste } from "@graphite/messages";
|
||||
import { TriggerPaste } from "@graphite/messages.svelte";
|
||||
import { type DialogState } from "@graphite/state-providers/dialog";
|
||||
import { type DocumentState } from "@graphite/state-providers/document";
|
||||
import { type DocumentState } from "@graphite/state-providers/document.svelte";
|
||||
import { documentContextState } from "@graphite/state-providers/document.svelte";
|
||||
import { type FullscreenState } from "@graphite/state-providers/fullscreen";
|
||||
import { type PortfolioState } from "@graphite/state-providers/portfolio";
|
||||
import { makeKeyboardModifiersBitfield, textInputCleanup, getLocalizedScanCode } from "@graphite/utility-functions/keyboard-entry";
|
||||
|
@ -155,7 +156,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
|||
// TODO: This would allow it to properly decide to act on removing hover focus from something that was hovered in the canvas before moving over the GUI.
|
||||
// TODO: Further explanation: https://github.com/GraphiteEditor/Graphite/pull/623#discussion_r866436197
|
||||
const inFloatingMenu = e.target instanceof Element && e.target.closest("[data-floating-menu-content]");
|
||||
const inGraphOverlay = get(document).graphViewOverlayOpen;
|
||||
const inGraphOverlay = documentContextState.graphViewOverlayOpen;
|
||||
if (!viewportPointerInteractionOngoing && (inFloatingMenu || inGraphOverlay)) return;
|
||||
|
||||
const modifiers = makeKeyboardModifiersBitfield(e);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { type Editor } from "@graphite/editor";
|
||||
import { TriggerAboutGraphiteLocalizedCommitDate } from "@graphite/messages";
|
||||
import { TriggerAboutGraphiteLocalizedCommitDate } from "@graphite/messages.svelte";
|
||||
|
||||
export function createLocalizationManager(editor: Editor) {
|
||||
// Subscribe to process backend event
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { type Editor } from "@graphite/editor";
|
||||
import { DisplayDialogPanic } from "@graphite/messages";
|
||||
import { DisplayDialogPanic } from "@graphite/messages.svelte";
|
||||
import { type DialogState } from "@graphite/state-providers/dialog";
|
||||
import { browserVersion, operatingSystem } from "@graphite/utility-functions/platform";
|
||||
import { stripIndents } from "@graphite/utility-functions/strip-indents";
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
TriggerLoadFirstAutoSaveDocument,
|
||||
TriggerLoadRestAutoSaveDocuments,
|
||||
TriggerSaveActiveDocument,
|
||||
} from "@graphite/messages";
|
||||
} from "@graphite/messages.svelte";
|
||||
import { type PortfolioState } from "@graphite/state-providers/portfolio";
|
||||
|
||||
const graphiteStore = createStore("graphite", "store");
|
||||
|
|
|
@ -12,8 +12,10 @@ import "@fontsource/source-sans-pro/700.css";
|
|||
// The library replaces the Reflect API on the window to support more features.
|
||||
import "reflect-metadata";
|
||||
|
||||
import { mount } from "svelte";
|
||||
|
||||
import App from "@graphite/App.svelte";
|
||||
|
||||
document.body.setAttribute("data-app-container", "");
|
||||
|
||||
export default new App({ target: document.body });
|
||||
export default mount(App, { target: document.body });
|
||||
|
|
|
@ -257,7 +257,7 @@ export class FrontendGraphOutput {
|
|||
|
||||
readonly description!: string;
|
||||
|
||||
readonly resolvedType!: string;
|
||||
readonly resolvedType!: string | undefined;
|
||||
|
||||
@CreateInputConnectorArray
|
||||
connectedTo!: Node[];
|
||||
|
@ -1380,11 +1380,17 @@ export function narrowWidgetProps<K extends WidgetPropsNames>(props: WidgetProps
|
|||
|
||||
export class Widget {
|
||||
constructor(props: WidgetPropsSet, widgetId: bigint) {
|
||||
this.props = props;
|
||||
this.widgetId = widgetId;
|
||||
this.props = $state(props);
|
||||
this.widgetId = $state(widgetId);
|
||||
}
|
||||
|
||||
@Type(() => WidgetProps, { discriminator: { property: "kind", subTypes: [...widgetSubTypes] }, keepDiscriminatorProperty: true })
|
||||
@Transform(({ value }) => {
|
||||
if (value.kind === "PopoverButton") {
|
||||
value.popoverLayout = value.popoverLayout.map(createLayoutGroup);
|
||||
}
|
||||
return value;
|
||||
})
|
||||
props!: WidgetPropsSet;
|
||||
|
||||
widgetId!: bigint;
|
||||
|
@ -1396,10 +1402,6 @@ function hoistWidgetHolder(widgetHolder: any): Widget {
|
|||
const props = widgetHolder.widget[kind];
|
||||
props.kind = kind;
|
||||
|
||||
if (kind === "PopoverButton") {
|
||||
props.popoverLayout = props.popoverLayout.map(createLayoutGroup);
|
||||
}
|
||||
|
||||
const { widgetId } = widgetHolder;
|
||||
|
||||
return plainToClass(Widget, { props, widgetId });
|
||||
|
@ -1436,13 +1438,18 @@ export function defaultWidgetLayout(): WidgetLayout {
|
|||
};
|
||||
}
|
||||
|
||||
function updateWidget(widget: Widget, newValues: Widget) {
|
||||
widget.props = newValues.props;
|
||||
widget.widgetId = newValues.widgetId;
|
||||
}
|
||||
|
||||
// Updates a widget layout based on a list of updates, giving the new layout by mutating the `layout` argument
|
||||
export function patchWidgetLayout(layout: /* &mut */ WidgetLayout, updates: WidgetDiffUpdate) {
|
||||
layout.layoutTarget = updates.layoutTarget;
|
||||
|
||||
updates.diff.forEach((update) => {
|
||||
// Find the object where the diff applies to
|
||||
const diffObject = update.widgetPath.reduce((targetLayout, index) => {
|
||||
let diffObject = update.widgetPath.reduce((targetLayout, index) => {
|
||||
if ("columnWidgets" in targetLayout) return targetLayout.columnWidgets[index];
|
||||
if ("rowWidgets" in targetLayout) return targetLayout.rowWidgets[index];
|
||||
if ("tableWidgets" in targetLayout) return targetLayout.tableWidgets[index];
|
||||
|
@ -1461,18 +1468,28 @@ export function patchWidgetLayout(layout: /* &mut */ WidgetLayout, updates: Widg
|
|||
return targetLayout[index];
|
||||
}, layout.layout as UIItem);
|
||||
|
||||
// If this is a list with a length, then set the length to 0 to clear the list
|
||||
if ("length" in diffObject) {
|
||||
diffObject.length = 0;
|
||||
}
|
||||
// Remove all of the keys from the old object
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
Object.keys(diffObject).forEach((key) => delete (diffObject as any)[key]);
|
||||
// Clear array length using Reflect to trigger reactivity
|
||||
if (diffObject instanceof Widget) {
|
||||
// For Widget instances, use direct property assignment
|
||||
// The setters will handle the reactivity
|
||||
updateWidget(diffObject, update.newValue as Widget);
|
||||
} else {
|
||||
if (Reflect.has(diffObject, "length")) {
|
||||
Reflect.set(diffObject, "length", 0);
|
||||
}
|
||||
|
||||
// Assign keys to the new object
|
||||
// `Object.assign` works but `diffObject = update.newValue;` doesn't.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
|
||||
Object.assign(diffObject, update.newValue);
|
||||
// Remove all enumerable properties using Reflect.deleteProperty to ensure proxy notifications
|
||||
Object.keys(diffObject).forEach((key) => {
|
||||
Reflect.deleteProperty(diffObject, key);
|
||||
});
|
||||
|
||||
// Assign new properties
|
||||
if (update.newValue && typeof update.newValue === "object") {
|
||||
Object.entries(update.newValue).forEach(([key, value]) => {
|
||||
Reflect.set(diffObject, key, value);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { writable } from "svelte/store";
|
||||
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import { defaultWidgetLayout, DisplayDialog, DisplayDialogDismiss, UpdateDialogButtons, UpdateDialogColumn1, UpdateDialogColumn2, patchWidgetLayout } from "@graphite/messages";
|
||||
import { defaultWidgetLayout, DisplayDialog, DisplayDialogDismiss, UpdateDialogButtons, UpdateDialogColumn1, UpdateDialogColumn2, patchWidgetLayout } from "@graphite/messages.svelte";
|
||||
import { type IconName } from "@graphite/utility-functions/icons";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { tick } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import { type Editor } from "@graphite/editor";
|
||||
|
||||
|
@ -15,85 +14,60 @@ import {
|
|||
UpdateGraphViewOverlay,
|
||||
TriggerDelayedZoomCanvasToFitAll,
|
||||
UpdateGraphFadeArtwork,
|
||||
} from "@graphite/messages";
|
||||
} from "@graphite/messages.svelte";
|
||||
|
||||
export const documentContextState = $state({
|
||||
documentModeLayout: defaultWidgetLayout(),
|
||||
toolOptionsLayout: defaultWidgetLayout(),
|
||||
documentBarLayout: defaultWidgetLayout(),
|
||||
toolShelfLayout: defaultWidgetLayout(),
|
||||
workingColorsLayout: defaultWidgetLayout(),
|
||||
nodeGraphControlBarLayout: defaultWidgetLayout(),
|
||||
// Graph view overlay
|
||||
graphViewOverlayOpen: false,
|
||||
fadeArtwork: 100,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export function createDocumentState(editor: Editor) {
|
||||
const state = writable({
|
||||
// Layouts
|
||||
documentModeLayout: defaultWidgetLayout(),
|
||||
toolOptionsLayout: defaultWidgetLayout(),
|
||||
documentBarLayout: defaultWidgetLayout(),
|
||||
toolShelfLayout: defaultWidgetLayout(),
|
||||
workingColorsLayout: defaultWidgetLayout(),
|
||||
nodeGraphControlBarLayout: defaultWidgetLayout(),
|
||||
// Graph view overlay
|
||||
graphViewOverlayOpen: false,
|
||||
fadeArtwork: 100,
|
||||
});
|
||||
const { subscribe, update } = state;
|
||||
|
||||
// Update layouts
|
||||
// Set up subscriptions (these run once when the function is called)
|
||||
editor.subscriptions.subscribeJsMessage(UpdateGraphFadeArtwork, (updateGraphFadeArtwork) => {
|
||||
update((state) => {
|
||||
state.fadeArtwork = updateGraphFadeArtwork.percentage;
|
||||
return state;
|
||||
});
|
||||
documentContextState.fadeArtwork = updateGraphFadeArtwork.percentage;
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentModeLayout, async (updateDocumentModeLayout) => {
|
||||
await tick();
|
||||
|
||||
update((state) => {
|
||||
patchWidgetLayout(state.documentModeLayout, updateDocumentModeLayout);
|
||||
return state;
|
||||
});
|
||||
patchWidgetLayout(documentContextState.documentModeLayout, updateDocumentModeLayout);
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateToolOptionsLayout, async (updateToolOptionsLayout) => {
|
||||
await tick();
|
||||
|
||||
update((state) => {
|
||||
patchWidgetLayout(state.toolOptionsLayout, updateToolOptionsLayout);
|
||||
return state;
|
||||
});
|
||||
patchWidgetLayout(documentContextState.toolOptionsLayout, updateToolOptionsLayout);
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateDocumentBarLayout, async (updateDocumentBarLayout) => {
|
||||
await tick();
|
||||
|
||||
update((state) => {
|
||||
patchWidgetLayout(state.documentBarLayout, updateDocumentBarLayout);
|
||||
return state;
|
||||
});
|
||||
patchWidgetLayout(documentContextState.documentBarLayout, updateDocumentBarLayout);
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateToolShelfLayout, async (updateToolShelfLayout) => {
|
||||
await tick();
|
||||
|
||||
update((state) => {
|
||||
patchWidgetLayout(state.toolShelfLayout, updateToolShelfLayout);
|
||||
return state;
|
||||
});
|
||||
patchWidgetLayout(documentContextState.toolShelfLayout, updateToolShelfLayout);
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateWorkingColorsLayout, async (updateWorkingColorsLayout) => {
|
||||
await tick();
|
||||
|
||||
update((state) => {
|
||||
patchWidgetLayout(state.workingColorsLayout, updateWorkingColorsLayout);
|
||||
return state;
|
||||
});
|
||||
patchWidgetLayout(documentContextState.workingColorsLayout, updateWorkingColorsLayout);
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(UpdateNodeGraphControlBarLayout, (updateNodeGraphControlBarLayout) => {
|
||||
update((state) => {
|
||||
patchWidgetLayout(state.nodeGraphControlBarLayout, updateNodeGraphControlBarLayout);
|
||||
return state;
|
||||
});
|
||||
patchWidgetLayout(documentContextState.nodeGraphControlBarLayout, updateNodeGraphControlBarLayout);
|
||||
});
|
||||
|
||||
// Show or hide the graph view overlay
|
||||
editor.subscriptions.subscribeJsMessage(UpdateGraphViewOverlay, (updateGraphViewOverlay) => {
|
||||
update((state) => {
|
||||
state.graphViewOverlayOpen = updateGraphViewOverlay.open;
|
||||
return state;
|
||||
});
|
||||
documentContextState.graphViewOverlayOpen = updateGraphViewOverlay.open;
|
||||
});
|
||||
|
||||
editor.subscriptions.subscribeJsMessage(TriggerDelayedZoomCanvasToFitAll, () => {
|
||||
// TODO: This is horribly hacky
|
||||
[0, 1, 10, 50, 100, 200, 300, 400, 500].forEach((delay) => {
|
||||
|
@ -101,8 +75,8 @@ export function createDocumentState(editor: Editor) {
|
|||
});
|
||||
});
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
};
|
||||
// Return the reactive state object directly
|
||||
return documentContextState;
|
||||
}
|
||||
|
||||
export type DocumentState = ReturnType<typeof createDocumentState>;
|
|
@ -1,7 +1,7 @@
|
|||
import { writable } from "svelte/store";
|
||||
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import { TriggerFontLoad } from "@graphite/messages";
|
||||
import { TriggerFontLoad } from "@graphite/messages.svelte";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export function createFontsState(editor: Editor) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { writable } from "svelte/store";
|
||||
|
||||
import { type Editor } from "@graphite/editor";
|
||||
import type { FrontendGraphOutput, FrontendGraphInput } from "@graphite/messages";
|
||||
import type { FrontendGraphOutput, FrontendGraphInput } from "@graphite/messages.svelte";
|
||||
import {
|
||||
type Box,
|
||||
type FrontendClickTargets,
|
||||
|
@ -26,7 +26,7 @@ import {
|
|||
UpdateNodeGraphTransform,
|
||||
UpdateNodeThumbnail,
|
||||
UpdateWirePathInProgress,
|
||||
} from "@graphite/messages";
|
||||
} from "@graphite/messages.svelte";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export function createNodeGraphState(editor: Editor) {
|
||||
|
@ -128,6 +128,7 @@ export function createNodeGraphState(editor: Editor) {
|
|||
});
|
||||
editor.subscriptions.subscribeJsMessage(UpdateVisibleNodes, (updateVisibleNodes) => {
|
||||
update((state) => {
|
||||
console.log("🚀 ~ update ~ state:", state);
|
||||
state.visibleNodes = new Set<bigint>(updateVisibleNodes.nodes);
|
||||
return state;
|
||||
});
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
defaultWidgetLayout,
|
||||
patchWidgetLayout,
|
||||
UpdateSpreadsheetLayout,
|
||||
} from "@graphite/messages";
|
||||
} from "@graphite/messages.svelte";
|
||||
import { downloadFileText, downloadFileBlob, upload } from "@graphite/utility-functions/files";
|
||||
import { extractPixelData, rasterizeSVG } from "@graphite/utility-functions/rasterization";
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { plainToInstance } from "class-transformer";
|
||||
|
||||
import { type JsMessageType, messageMakers, type JsMessage } from "@graphite/messages";
|
||||
import { type JsMessageType, messageMakers, type JsMessage } from "@graphite/messages.svelte";
|
||||
import { type EditorHandle } from "@graphite-frontend/wasm/pkg/graphite_wasm.js";
|
||||
|
||||
type JsMessageCallback<T extends JsMessage> = (messageData: T) => void;
|
||||
|
|
15
frontend/src/vite-env.d.ts
vendored
15
frontend/src/vite-env.d.ts
vendored
|
@ -1,2 +1,17 @@
|
|||
/// <reference types="node" />
|
||||
/// <reference types="svelte" />
|
||||
/// <reference types="svelte/store" />
|
||||
/// <reference types="svelte/motion" />
|
||||
/// <reference types="svelte/transition" />
|
||||
/// <reference types="svelte/animate" />
|
||||
/// <reference types="svelte/easing" />
|
||||
/// <reference types="svelte/elements" />
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
/*
|
||||
Why vite-env.d.ts instead of compilerOptions.types inside jsconfig.json or tsconfig.json?
|
||||
|
||||
Setting compilerOptions.types shuts out all other types not explicitly listed in the configuration.
|
||||
Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace,
|
||||
while also adding svelte and vite/client type information.
|
||||
*/
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
"allowSyntheticDefaultImports": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"sourceMap": true,
|
||||
"types": ["node", "svelte", "svelte/store", "svelte/motion", "svelte/transition", "svelte/animate", "svelte/easing"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"/*": ["./*"],
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
import { spawnSync } from "child_process";
|
||||
|
||||
import fs from "fs";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { minimatch } from "minimatch";
|
||||
import path from "path";
|
||||
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
|
@ -33,6 +34,16 @@ const ALLOWED_LICENSES = [
|
|||
"NCSA",
|
||||
];
|
||||
|
||||
const runesGlobs = ["**/*.svelte"];
|
||||
|
||||
function forceRunes(filePath: string): boolean {
|
||||
const relativePath = filePath.slice(filePath.indexOf("src"));
|
||||
// Test the file path against each glob pattern
|
||||
return runesGlobs.some((min) => {
|
||||
return minimatch(relativePath, min);
|
||||
});
|
||||
}
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
|
@ -45,6 +56,15 @@ export default defineConfig({
|
|||
|
||||
defaultHandler?.(warning);
|
||||
},
|
||||
dynamicCompileOptions({ filename, compileOptions }) {
|
||||
if (forceRunes(filename) && !compileOptions.runes) {
|
||||
console.log(`🚀 ~ runes ~`, filename, true);
|
||||
|
||||
return { runes: true };
|
||||
} else {
|
||||
console.log(`🚀 ~ runes ~`, filename, false);
|
||||
}
|
||||
},
|
||||
}),
|
||||
viteMultipleAssets(["../demo-artwork"]),
|
||||
],
|
||||
|
@ -137,7 +157,7 @@ function formatThirdPartyLicenses(jsLicenses: Dependency[]): string {
|
|||
const pkg = license.packages[foundPackagesIndex];
|
||||
|
||||
license.packages = license.packages.filter((pkg) => pkg.name !== "path-bool");
|
||||
const noticeText = fs.readFileSync(path.resolve(__dirname, "../libraries/path-bool/NOTICE"), "utf8");
|
||||
const noticeText = readFileSync(path.resolve(__dirname, "../libraries/path-bool/NOTICE"), "utf8");
|
||||
|
||||
licenses.push({
|
||||
licenseName: license.licenseName,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue