phase 2: update bindings to svelte 5 for files in runesGlob

This commit is contained in:
Smit 2025-06-19 03:04:55 +05:30
parent 7872610d14
commit c07f4b32af
42 changed files with 1299 additions and 898 deletions

View file

@ -2,6 +2,7 @@ declare global {
namespace Graphite { namespace Graphite {
type Platform = "Windows" | "Mac" | "Linux" | "Web"; type Platform = "Windows" | "Mac" | "Linux" | "Web";
type MenuType = "Popover" | "Dropdown" | "Dialog" | "Cursor"; type MenuType = "Popover" | "Dropdown" | "Dialog" | "Cursor";
type Axis = "Horizontal" | "Vertical";
// interface Error {} // interface Error {}
} }

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { onDestroy, createEventDispatcher, getContext } from "svelte"; import { onDestroy, getContext } from "svelte";
import type { Editor } from "@graphite/editor"; import type { Editor } from "@graphite/editor";
import type { HSV, RGB, FillChoice } from "@graphite/messages"; import type { HSV, RGB, FillChoice } from "@graphite/messages";
@ -33,43 +33,48 @@
}; };
const editor = getContext<Editor>("editor"); 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 }>(); let {
colorOrGradient,
export let colorOrGradient: FillChoice; allowNone = false,
export let allowNone = false; direction = "Bottom",
// export let allowTransparency = false; // TODO: Implement open = $bindable(),
export let direction: MenuDirection = "Bottom"; oncolorOrGradient,
// TODO: See if this should be made to follow the pattern of DropdownInput.svelte so this could be removed onstartHistoryTransaction,
export let open: boolean; }: Props = $props();
const hsvaOrNone = colorOrGradient instanceof Color ? colorOrGradient.toHSVA() : colorOrGradient.firstColor()?.toHSVA(); 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 let activeIndex = $state<number | undefined>(0);
$: 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;
// New color components // New color components
let hue = hsva.h; let hue = $state(hsva.h);
let saturation = hsva.s; let saturation = $state(hsva.s);
let value = hsva.v; let value = $state(hsva.v);
let alpha = hsva.a; let alpha = $state(hsva.a);
let isNone = hsvaOrNone === undefined; let isNone = $state(hsvaOrNone === undefined);
// Old color components // Old color components
let oldHue = hsva.h; let oldHue = $state(hsva.h);
let oldSaturation = hsva.s; let oldSaturation = $state(hsva.s);
let oldValue = hsva.v; let oldValue = $state(hsva.v);
let oldAlpha = hsva.a; let oldAlpha = $state(hsva.a);
let oldIsNone = hsvaOrNone === undefined; let oldIsNone = $state(hsvaOrNone === undefined);
// Transient state // Transient state
let draggingPickerTrack: HTMLDivElement | undefined = undefined; let draggingPickerTrack: HTMLDivElement | undefined = undefined;
let strayCloses = true; let strayCloses = $state(true);
let gradientSpectrumDragging = false; let gradientSpectrumDragging = $state(false);
let shiftPressed = false; let shiftPressed = false;
let alignedAxis: "saturation" | "value" | undefined = undefined; let alignedAxis: "saturation" | "value" | undefined = $state(undefined);
let hueBeforeDrag = 0; let hueBeforeDrag = 0;
let saturationBeforeDrag = 0; let saturationBeforeDrag = 0;
let valueBeforeDrag = 0; let valueBeforeDrag = 0;
@ -79,21 +84,9 @@
let saturationRestoreWhenShiftReleased: number | undefined = undefined; let saturationRestoreWhenShiftReleased: number | undefined = undefined;
let valueRestoreWhenShiftReleased: number | undefined = undefined; let valueRestoreWhenShiftReleased: number | undefined = undefined;
let self: FloatingMenu | undefined; let self: FloatingMenu | undefined = $state();
let hexCodeInputWidget: TextInput | undefined; let hexCodeInputWidget: TextInput | undefined = $state();
let gradientSpectrumInputWidget: SpectrumInput | undefined; let gradientSpectrumInputWidget: SpectrumInput | undefined = $state();
$: 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;
function generateColor(h: number, s: number, v: number, a: number, none: boolean) { function generateColor(h: number, s: number, v: number, a: number, none: boolean) {
if (none) return new Color("none"); if (none) return new Color("none");
@ -174,6 +167,7 @@
} }
const color = new Color({ h: hue, s: saturation, v: value, a: alpha }); const color = new Color({ h: hue, s: saturation, v: value, a: alpha });
setColor(color); setColor(color);
if (!e.shiftKey) { if (!e.shiftKey) {
@ -226,7 +220,7 @@
document.addEventListener("keydown", onKeyDown); document.addEventListener("keydown", onKeyDown);
document.addEventListener("keyup", onKeyUp); document.addEventListener("keyup", onKeyUp);
dispatch("startHistoryTransaction"); onstartHistoryTransaction?.();
} }
function removeEvents() { function removeEvents() {
@ -274,15 +268,14 @@
} }
function setColor(color?: Color) { 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); const stop = gradientSpectrumInputWidget && activeIndex !== undefined && gradient?.atIndex(activeIndex);
if (stop && gradientSpectrumInputWidget instanceof SpectrumInput) { if (stop && gradientSpectrumInputWidget !== undefined) {
stop.color = colorToEmit; stop.color = colorToEmit;
gradient = gradient;
} }
dispatch("colorOrGradient", gradient || colorToEmit); oncolorOrGradient?.(gradient ?? colorToEmit);
} }
function swapNewWithOld() { function swapNewWithOld() {
@ -338,7 +331,7 @@
} }
function setColorPreset(preset: PresetColors) { function setColorPreset(preset: PresetColors) {
dispatch("startHistoryTransaction"); onstartHistoryTransaction?.()
if (preset === "none") { if (preset === "none") {
setNewHSVA(0, 0, 0, 1, true); setNewHSVA(0, 0, 0, 1, true);
setColor(new Color("none")); setColor(new Color("none"));
@ -379,14 +372,14 @@
try { try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = await new (window as any).EyeDropper().open(); const result = await new (window as any).EyeDropper().open();
dispatch("startHistoryTransaction"); onstartHistoryTransaction?.();
setColorCode(result.sRGBHex); setColorCode(result.sRGBHex);
} catch { } catch {
// Do nothing // Do nothing
} }
} }
function gradientActiveMarkerIndexChange({ detail: index }: CustomEvent<number | undefined>) { function gradientActiveMarkerIndexChange(index: number | undefined) {
activeIndex = index; activeIndex = index;
const color = index === undefined ? undefined : gradient?.colorAtIndex(index); const color = index === undefined ? undefined : gradient?.colorAtIndex(index);
const hsva = color?.toHSVA(); const hsva = color?.toHSVA();
@ -401,9 +394,34 @@
onDestroy(() => { onDestroy(() => {
removeEvents(); 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> </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 <LayoutRow
styles={{ styles={{
"--new-color": newColor.toHexOptionalAlpha(), "--new-color": newColor.toHexOptionalAlpha(),
@ -418,9 +436,9 @@
> >
<LayoutCol class="pickers-and-gradient"> <LayoutCol class="pickers-and-gradient">
<LayoutRow class="pickers"> <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} {#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}
{#if alignedAxis} {#if alignedAxis}
<div <div
@ -429,37 +447,37 @@
class:value={alignedAxis === "value"} class:value={alignedAxis === "value"}
style:top={`${(1 - value) * 100}%`} style:top={`${(1 - value) * 100}%`}
style:left={`${saturation * 100}%`} style:left={`${saturation * 100}%`}
/> ></div>
{/if} {/if}
</LayoutCol> </LayoutCol>
<LayoutCol class="hue-picker" on:pointerdown={onPointerDown} data-hue-picker> <LayoutCol class="hue-picker" onpointerdown={onPointerDown} data-hue-picker>
{#if !isNone} {#if !isNone}
<div class="selection-needle" style:top={`${(1 - hue) * 100}%`} /> <div class="selection-needle" style:top={`${(1 - hue) * 100}%`}></div>
{/if} {/if}
</LayoutCol> </LayoutCol>
<LayoutCol class="alpha-picker" on:pointerdown={onPointerDown} data-alpha-picker> <LayoutCol class="alpha-picker" onpointerdown={onPointerDown} data-alpha-picker>
{#if !isNone} {#if !isNone}
<div class="selection-needle" style:top={`${(1 - alpha) * 100}%`} /> <div class="selection-needle" style:top={`${(1 - alpha) * 100}%`}></div>
{/if} {/if}
</LayoutCol> </LayoutCol>
</LayoutRow> </LayoutRow>
{#if gradient} {#if gradient}
<LayoutRow class="gradient"> <LayoutRow class="gradient">
<SpectrumInput <SpectrumInput
{gradient} gradient={gradient}
on:gradient={() => { ongradient={(detail) => {
gradient = gradient; gradient = detail;
if (gradient) dispatch("colorOrGradient", gradient); if (gradient) oncolorOrGradient?.(gradient);
}} }}
on:activeMarkerIndexChange={gradientActiveMarkerIndexChange} onactiveMarkerIndexChange={gradientActiveMarkerIndexChange}
activeMarkerIndex={activeIndex} activeMarkerIndex={activeIndex}
on:dragging={({ detail }) => (gradientSpectrumDragging = detail)} bind:drag={gradientSpectrumDragging}
bind:this={gradientSpectrumInputWidget} bind:this={gradientSpectrumInputWidget}
/> />
{#if gradientSpectrumInputWidget && activeIndex !== undefined} {#if gradientSpectrumInputWidget && activeIndex !== undefined}
<NumberInput <NumberInput
value={(gradient.positionAtIndex(activeIndex) || 0) * 100} value={(gradient.positionAtIndex(activeIndex) || 0) * 100}
on:value={({ detail }) => { onvalue={(detail) => {
if (gradientSpectrumInputWidget && activeIndex !== undefined && detail !== undefined) gradientSpectrumInputWidget.setPosition(activeIndex, detail / 100); if (gradientSpectrumInputWidget && activeIndex !== undefined && detail !== undefined) gradientSpectrumInputWidget.setPosition(activeIndex, detail / 100);
}} }}
displayDecimalPlaces={0} displayDecimalPlaces={0}
@ -480,7 +498,7 @@
> >
{#if !newColor.equals(oldColor)} {#if !newColor.equals(oldColor)}
<div class="swap-button-background"></div> <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} {/if}
<LayoutCol class="new-color" classes={{ none: isNone }}> <LayoutCol class="new-color" classes={{ none: isNone }}>
{#if !newColor.equals(oldColor)} {#if !newColor.equals(oldColor)}
@ -499,9 +517,9 @@
<Separator type="Related" /> <Separator type="Related" />
<LayoutRow> <LayoutRow>
<TextInput <TextInput
value={newColor.toHexOptionalAlpha() || "-"} value={newColor.toHexOptionalAlpha() ?? "-"}
on:commitText={({ detail }) => { oncommitText={(detail ) => {
dispatch("startHistoryTransaction"); onstartHistoryTransaction?.();
setColorCode(detail); setColorCode(detail);
}} }}
centered={true} centered={true}
@ -519,14 +537,12 @@
<Separator type="Related" /> <Separator type="Related" />
{/if} {/if}
<NumberInput <NumberInput
value={strength} value={rgbChannels[index][1]}
on:value={({ detail }) => { onvalue={(detail) => {
strength = detail; rgbChannels[index][1] = detail;
setColorRGB(channel, detail); setColorRGB(channel, detail);
}} }}
on:startHistoryTransaction={() => { {onstartHistoryTransaction}
dispatch("startHistoryTransaction");
}}
min={0} min={0}
max={255} max={255}
minWidth={1} minWidth={1}
@ -546,14 +562,12 @@
<Separator type="Related" /> <Separator type="Related" />
{/if} {/if}
<NumberInput <NumberInput
value={strength} value={hsvChannels[index][1]}
on:value={({ detail }) => { onvalue={(detail) => {
strength = detail; hsvChannels[index][1] = detail;
setColorHSV(channel, detail); setColorHSV(channel, detail);
}} }}
on:startHistoryTransaction={() => { {onstartHistoryTransaction}
dispatch("startHistoryTransaction");
}}
min={0} min={0}
max={channel === "h" ? 360 : 100} max={channel === "h" ? 360 : 100}
unit={channel === "h" ? "°" : "%"} unit={channel === "h" ? "°" : "%"}
@ -573,13 +587,11 @@
<Separator type="Related" /> <Separator type="Related" />
<NumberInput <NumberInput
value={!isNone ? alpha * 100 : undefined} value={!isNone ? alpha * 100 : undefined}
on:value={({ detail }) => { onvalue={(detail) => {
if (detail !== undefined) alpha = detail / 100; if (detail !== undefined) alpha = detail / 100;
setColorAlphaPercent(detail); setColorAlphaPercent(detail);
}} }}
on:startHistoryTransaction={() => { {onstartHistoryTransaction}
dispatch("startHistoryTransaction");
}}
min={0} min={0}
max={100} max={100}
rangeMin={0} rangeMin={0}
@ -593,23 +605,23 @@
<LayoutRow class="leftover-space" /> <LayoutRow class="leftover-space" />
<LayoutRow> <LayoutRow>
{#if allowNone && !gradient} {#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" /> <Separator type="Related" />
{/if} {/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" /> <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" /> <Separator type="Related" />
<button class="preset-color pure" on:click={setColorPresetSubtile} tabindex="-1"> <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 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 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 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 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 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 data-pure-tile="magenta" style="--pure-color: #ff00ff; --pure-color-gray: #696969" title="Set to magenta"></div>
</button> </button>
<Separator type="Related" /> <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> </LayoutRow>
</LayoutCol> </LayoutCol>
</LayoutRow> </LayoutRow>

View file

@ -16,7 +16,7 @@
const dialog = getContext<DialogState>("dialog"); const dialog = getContext<DialogState>("dialog");
let self: FloatingMenu | undefined; let self: FloatingMenu | undefined = $state();
onMount(() => { onMount(() => {
// Focus the button which is marked as emphasized, or otherwise the first button, in the popup // Focus the button which is marked as emphasized, or otherwise the first button, in the popup

View file

@ -1,7 +1,5 @@
<svelte:options accessors={true} />
<script lang="ts"> <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";
@ -15,41 +13,51 @@
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte"; import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
import UserInputLabel from "@graphite/components/widgets/labels/UserInputLabel.svelte"; import UserInputLabel from "@graphite/components/widgets/labels/UserInputLabel.svelte";
let self: FloatingMenu | undefined; let self: FloatingMenu | undefined = $state();
let scroller: LayoutCol | undefined; let scroller: LayoutCol | undefined = $state();
let searchTextInput: TextInput | undefined; 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[][]; let {
export let activeEntry: MenuListEntry | undefined = undefined; entries = $bindable(),
export let open: boolean; activeEntry = undefined,
export let direction: MenuDirection = "Bottom"; open = $bindable(false),
export let minWidth = 0; direction = "Bottom",
export let drawIcon = false; minWidth = 0,
export let interactive = false; drawIcon = false,
export let scrollableY = false; interactive = false,
export let virtualScrollingEntryHeight = 0; scrollableY = false,
export let tooltip: string | undefined = undefined; 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. // Keep the child references outside of the entries array so as to avoid infinite recursion.
let childReferences: MenuList[][] = []; let childReferences: MenuList[][] = $state([]);
let search = ""; let search = $state("");
let highlighted = activeEntry as MenuListEntry | undefined; let highlighted = $state(activeEntry as MenuListEntry | undefined);
let virtualScrollingEntriesStart = 0; 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: 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. // 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()); return !search || entry.label.toLowerCase().includes(search.toLowerCase());
} }
function watchOpen(open: boolean) { function watchOpen(value: boolean) {
if (open && !inNestedMenuList()) addEventListener("keydown", keydown); if (open && !inNestedMenuList()) addEventListener("keydown", keydown);
else if (!inNestedMenuList()) removeEventListener("keydown", keydown); else if (!inNestedMenuList()) removeEventListener("keydown", keydown);
highlighted = activeEntry; highlighted = activeEntry;
dispatch("open", open); // dispatch("open", value);
// open = value;
search = ""; search = "";
} }
@ -151,42 +160,43 @@
if (menuListEntry.action) menuListEntry.action(); if (menuListEntry.action) menuListEntry.action();
// Notify the parent about the clicked entry as the new active entry // Notify the parent about the clicked entry as the new active entry
dispatch("activeEntry", menuListEntry); onactiveEntry?.(menuListEntry);
// Close the containing menu // Close the containing menu
let childReference = getChildReference(menuListEntry); let childReference = getChildReference(menuListEntry);
if (childReference) { if (childReference) {
childReference.open = false; childReference.open = false;
entries = entries; // entries = entries;
} }
dispatch("open", false); // dispatch("open", false);
open = false; open = false;
} }
function onEntryPointerEnter(menuListEntry: MenuListEntry) { function onEntryPointerEnter(menuListEntry: MenuListEntry) {
if (!menuListEntry.children?.length) { if (!menuListEntry.children?.length) {
dispatch("hoverInEntry", menuListEntry); onhoverInEntry?.(menuListEntry);
return; return;
} }
let childReference = getChildReference(menuListEntry); let childReference = getChildReference(menuListEntry);
if (childReference) { if (childReference) {
childReference.open = true; childReference.open = true;
entries = entries; // entries = entries;
} else dispatch("open", true); } else open = true;
} }
function onEntryPointerLeave(menuListEntry: MenuListEntry) { function onEntryPointerLeave(menuListEntry: MenuListEntry) {
if (!menuListEntry.children?.length) { if (!menuListEntry.children?.length) {
dispatch("hoverOutEntry"); // dispatch("hoverOutEntry");
onhoverOutEntry?.();
return; return;
} }
let childReference = getChildReference(menuListEntry); let childReference = getChildReference(menuListEntry);
if (childReference) { if (childReference) {
childReference.open = false; childReference.open = false;
entries = entries; // entries = entries;
} else dispatch("open", false); } else open = false;
} }
function isEntryOpen(menuListEntry: MenuListEntry): boolean { function isEntryOpen(menuListEntry: MenuListEntry): boolean {
@ -385,30 +395,60 @@
export function scrollViewTo(distanceDown: number) { export function scrollViewTo(distanceDown: number) {
scroller?.div?.()?.scrollTo(0, distanceDown); 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> </script>
<FloatingMenu <FloatingMenu
class="menu-list" class="menu-list"
{open} bind:open
on:open={({ detail }) => (open = detail)}
on:naturalWidth
type="Dropdown" type="Dropdown"
windowEdgeMargin={0} windowEdgeMargin={0}
escapeCloses={false} escapeCloses={false}
{direction} {direction}
{minWidth} {minWidth}
scrollableY={scrollableY && virtualScrollingEntryHeight === 0} scrollableY={scrollableY && virtualScrollingEntryHeight === 0}
{onnaturalWidth}
bind:this={self} bind:this={self}
> >
{#if search.length > 0} {#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}
<!-- If we put the scrollableY on the layoutcol for non-font dropdowns then for some reason it always creates a tiny scrollbar. <!-- 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`. --> 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 <LayoutCol
bind:this={scroller} bind:this={scroller}
scrollableY={scrollableY && virtualScrollingEntryHeight !== 0} scrollableY={scrollableY && virtualScrollingEntryHeight !== 0}
on:scroll={onScroll} onscroll={onScroll}
styles={{ "min-width": virtualScrollingEntryHeight ? `${minWidth}px` : `inherit` }} styles={{ "min-width": virtualScrollingEntryHeight ? `${minWidth}px` : `inherit` }}
> >
{#if virtualScrollingEntryHeight} {#if virtualScrollingEntryHeight}
@ -424,14 +464,14 @@
classes={{ open: isEntryOpen(entry), active: entry.label === highlighted?.label, disabled: Boolean(entry.disabled) }} classes={{ open: isEntryOpen(entry), active: entry.label === highlighted?.label, disabled: Boolean(entry.disabled) }}
styles={{ height: virtualScrollingEntryHeight || "20px" }} styles={{ height: virtualScrollingEntryHeight || "20px" }}
{tooltip} {tooltip}
on:click={() => !entry.disabled && onEntryClick(entry)} onclick={() => !entry.disabled && onEntryClick(entry)}
on:pointerenter={() => !entry.disabled && onEntryPointerEnter(entry)} onpointerenter={() => !entry.disabled && onEntryPointerEnter(entry)}
on:pointerleave={() => !entry.disabled && onEntryPointerLeave(entry)} onpointerleave={() => !entry.disabled && onEntryPointerLeave(entry)}
> >
{#if entry.icon && drawIcon} {#if entry.icon && drawIcon}
<IconLabel icon={entry.icon} iconSizeOverride={16} class="entry-icon" /> <IconLabel icon={entry.icon} iconSizeOverride={16} class="entry-icon" />
{:else if drawIcon} {:else if drawIcon}
<div class="no-icon" /> <div class="no-icon"></div>
{/if} {/if}
{#if entry.font} {#if entry.font}
@ -447,18 +487,13 @@
{#if entry.children?.length} {#if entry.children?.length}
<IconLabel class="submenu-arrow" icon="DropdownArrow" /> <IconLabel class="submenu-arrow" icon="DropdownArrow" />
{:else} {:else}
<div class="no-submenu-arrow" /> <div class="no-submenu-arrow"></div>
{/if} {/if}
{#if entry.children} {#if entry.children}
<MenuList <MenuList
on:naturalWidth={({ detail }) => { {onnaturalWidth}
// We do a manual dispatch here instead of just `on:naturalWidth` as a workaround for the <script> tag open={entry?.open}
// 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}
direction="TopRight" direction="TopRight"
entries={entry.children} entries={entry.children}
{minWidth} {minWidth}

View file

@ -110,7 +110,7 @@
</script> </script>
<div class="node-catalog"> <div class="node-catalog">
<TextInput placeholder="Search Nodes..." value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} /> <TextInput placeholder="Search Nodes..." bind:value={searchTerm} bind:this={nodeSearchInput} />
<div class="list-results" on:wheel|passive|stopPropagation> <div class="list-results" on:wheel|passive|stopPropagation>
{#each nodeCategories as nodeCategory} {#each nodeCategories as nodeCategory}
<details open={nodeCategory[1].open}> <details open={nodeCategory[1].open}>

View file

@ -1,14 +1,21 @@
<script lang="ts"> <script lang="ts">
export let condition: boolean; import type { Snippet } from "svelte";
export let wrapperClass = "";
interface Props {
condition: boolean;
wrapperClass?: string;
children?: Snippet;
}
let { condition, wrapperClass = "", children }: Props = $props();
</script> </script>
{#if condition} {#if condition}
<div class={wrapperClass}> <div class={wrapperClass}>
<slot /> {@render children?.()}
</div> </div>
{:else} {:else}
<slot /> {@render children?.()}
{/if} {/if}
<style lang="scss" global></style> <style lang="scss" global></style>

View file

@ -1,6 +1,4 @@
<script lang="ts" context="module"> <script lang="ts" module>
export type MenuType = "Popover" | "Dropdown" | "Dialog" | "Cursor";
/// Prevents the escape key from closing the parent floating menu of the given element. /// 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. /// 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. /// After checking for the Escape key, it checks (in one `setTimeout`) for the attribute and ignores the key if it's present.
@ -17,9 +15,11 @@
} }
} }
</script> </script>
<script lang="ts"> <script lang="ts">
import { onMount, afterUpdate, createEventDispatcher, tick } from "svelte"; import type { SvelteHTMLElements } from 'svelte/elements';
import { onMount, tick } from "svelte";
import type { MenuDirection } from "@graphite/messages"; import type { MenuDirection } from "@graphite/messages";
import { browserVersion } from "@graphite/utility-functions/platform"; import { browserVersion } from "@graphite/utility-functions/platform";
@ -29,27 +29,47 @@
const BUTTON_LEFT = 0; const BUTTON_LEFT = 0;
const POINTER_STRAY_DISTANCE = 100; 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 = ""; let {
export { className as class }; class: className = "",
export let classes: Record<string, boolean> = {}; classes = {},
let styleName = ""; style: styleName = "",
export { styleName as style }; styles = {},
export let styles: Record<string, string | number | undefined> = {}; open = $bindable(),
export let open: boolean; type,
export let type: MenuType; direction = "Bottom",
export let direction: MenuDirection = "Bottom"; windowEdgeMargin = 6,
export let windowEdgeMargin = 6; scrollableY = false,
export let scrollableY = false; minWidth = 0,
export let minWidth = 0; escapeCloses = true,
export let escapeCloses = true; strayCloses = true,
export let strayCloses = true; children,
onnaturalWidth,
...rest
}: Props = $props();
let tail: HTMLDivElement | undefined; let tail: HTMLDivElement | undefined = $state();
let self: HTMLDivElement | undefined; let self: HTMLDivElement | undefined = $state();
let floatingMenuContainer: HTMLDivElement | undefined; let floatingMenuContainer: HTMLDivElement | undefined = $state();
let floatingMenuContent: LayoutCol | undefined; 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. // 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 // 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); resizeObserverCallback(entries);
}); });
let wasOpen = open; let wasOpen = open;
let measuringOngoing = false; let measuringOngoing = $state(false);
let measuringOngoingGuard = false; let measuringOngoingGuard = false;
let minWidthParentWidth = 0; let minWidthParentWidth = $state(0);
let pointerStillDown = false; let pointerStillDown = false;
let workspaceBounds = new DOMRect(); let workspaceBounds = new DOMRect();
let floatingMenuBounds = new DOMRect(); let floatingMenuBounds = new DOMRect();
let floatingMenuContentBounds = 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 // Called only when `open` is changed from outside this component
async function watchOpenChange(isOpen: boolean) { 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[]) { function resizeObserverCallback(entries: ResizeObserverEntry[]) {
minWidthParentWidth = entries[0].contentRect.width; minWidthParentWidth = entries[0].contentRect.width;
@ -292,7 +288,7 @@
// Notify the parent about the measured natural width // Notify the parent about the measured natural width
if (naturalWidth !== undefined && naturalWidth >= 0) { if (naturalWidth !== undefined && naturalWidth >= 0) {
dispatch("naturalWidth", naturalWidth); onnaturalWidth?.(naturalWidth);
} }
} }
@ -316,7 +312,7 @@
if (strayCloses && notHoveringOverOwnSpawner && isPointerEventOutsideFloatingMenu(e, POINTER_STRAY_DISTANCE)) { 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: 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 // 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 // Clean up any messes from lost pointerup events
@ -385,7 +381,7 @@
const foundTarget = filteredListOfDescendantSpawners.find((item: Element): boolean => item === targetSpawner); 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 the currently hovered spawner is one of the found valid hover-transferrable spawners, swap to it by clicking on it
if (foundTarget) { if (foundTarget) {
dispatch("open", false); open = false;
(foundTarget as HTMLElement).click(); (foundTarget as HTMLElement).click();
} }
@ -399,7 +395,7 @@
if (escapeCloses && e.key === "Escape") { if (escapeCloses && e.key === "Escape") {
setTimeout(() => { setTimeout(() => {
if (!floatingMenuContainer?.querySelector("[data-floating-menu-content][data-escape-does-not-close]")) { if (!floatingMenuContainer?.querySelector("[data-floating-menu-content][data-escape-does-not-close]")) {
dispatch("open", false); open = false;
} }
}, 0); }, 0);
@ -411,7 +407,7 @@
function pointerDownHandler(e: PointerEvent) { function pointerDownHandler(e: PointerEvent) {
// Close the floating menu if the pointer clicked outside the floating menu (but within stray distance) // Close the floating menu if the pointer clicked outside the floating menu (but within stray distance)
if (isPointerEventOutsideFloatingMenu(e)) { 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 // Track if the left pointer button is now down so its later click event can be canceled
const eventIsForLmb = e.button === BUTTON_LEFT; const eventIsForLmb = e.button === BUTTON_LEFT;
@ -454,21 +450,50 @@
return false; 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> </script>
<div <div
class={`floating-menu ${direction.toLowerCase()} ${type.toLowerCase()} ${className} ${extraClasses}`.trim()} class={`floating-menu ${direction.toLowerCase()} ${type.toLowerCase()} ${className} ${extraClasses}`.trim()}
style={`${styleName} ${extraStyles}`.trim() || undefined} style={`${styleName} ${extraStyles}`.trim() || undefined}
bind:this={self} bind:this={self}
{...$$restProps} {...rest}
> >
{#if displayTail} {#if displayTail}
<div class="tail" bind:this={tail} /> <div class="tail" bind:this={tail}></div>
{/if} {/if}
{#if displayContainer} {#if displayContainer}
<div class="floating-menu-container" bind:this={floatingMenuContainer}> <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> <LayoutCol class="floating-menu-content" styles={{ "min-width": minWidthStyleValue }} {scrollableY} bind:this={floatingMenuContent} data-floating-menu-content>
<slot /> {@render children?.()}
</LayoutCol> </LayoutCol>
</div> </div>
{/if} {/if}

View file

@ -1,23 +1,43 @@
<script lang="ts"> <script lang="ts">
let className = ""; import type { SvelteHTMLElements } from 'svelte/elements';
export { className as class }; import { createBubbler } from 'svelte/legacy';
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;
let self: HTMLDivElement | undefined; const bubble = createBubbler();
$: extraClasses = Object.entries(classes) 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;
}
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] : [])) .flatMap(([className, stateName]) => (stateName ? [className] : []))
.join(" "); .join(" "));
$: extraStyles = Object.entries(styles) let extraStyles = $derived(Object.entries(styles)
.flatMap((styleAndValue) => (styleAndValue[1] !== undefined ? [`${styleAndValue[0]}: ${styleAndValue[1]};`] : [])) .flatMap((styleAndValue) => (styleAndValue[1] !== undefined ? [`${styleAndValue[0]}: ${styleAndValue[1]};`] : []))
.join(" "); .join(" "));
export function div(): HTMLDivElement | undefined { export function div(): HTMLDivElement | undefined {
return self; return self;
@ -34,23 +54,23 @@
style={`${styleName} ${extraStyles}`.trim() || undefined} style={`${styleName} ${extraStyles}`.trim() || undefined}
title={tooltip} title={tooltip}
bind:this={self} bind:this={self}
on:auxclick onauxclick={bubble('auxclick')}
on:blur onblur={bubble('blur')}
on:click onclick={bubble('click')}
on:dblclick ondblclick={bubble('dblclick')}
on:dragend ondragend={bubble('dragend')}
on:dragleave ondragleave={bubble('dragleave')}
on:dragover ondragover={bubble('dragover')}
on:dragstart ondragstart={bubble('dragstart')}
on:drop ondrop={bubble('drop')}
on:mouseup onmouseup={bubble('mouseup')}
on:pointerdown onpointerdown={bubble('pointerdown')}
on:pointerenter onpointerenter={bubble('pointerenter')}
on:pointerleave onpointerleave={bubble('pointerleave')}
on:scroll onscroll={bubble('scroll')}
{...$$restProps} {...rest}
> >
<slot /> {@render children?.()}
</div> </div>
<!-- Unused (each impacts performance, see <https://github.com/GraphiteEditor/Graphite/issues/1877>): <!-- Unused (each impacts performance, see <https://github.com/GraphiteEditor/Graphite/issues/1877>):

View file

@ -1,23 +1,44 @@
<script lang="ts"> <script lang="ts">
let className = ""; import type { SvelteHTMLElements } from 'svelte/elements';
export { className as class }; import { createBubbler } from 'svelte/legacy';
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;
let self: HTMLDivElement | undefined; const bubble = createBubbler();
$: extraClasses = Object.entries(classes) 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;
[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] : [])) .flatMap(([className, stateName]) => (stateName ? [className] : []))
.join(" "); .join(" "));
$: extraStyles = Object.entries(styles) let extraStyles = $derived(Object.entries(styles)
.flatMap((styleAndValue) => (styleAndValue[1] !== undefined ? [`${styleAndValue[0]}: ${styleAndValue[1]};`] : [])) .flatMap((styleAndValue) => (styleAndValue[1] !== undefined ? [`${styleAndValue[0]}: ${styleAndValue[1]};`] : []))
.join(" "); .join(" "));
export function div(): HTMLDivElement | undefined { export function div(): HTMLDivElement | undefined {
return self; return self;
@ -34,23 +55,23 @@
style={`${styleName} ${extraStyles}`.trim() || undefined} style={`${styleName} ${extraStyles}`.trim() || undefined}
title={tooltip} title={tooltip}
bind:this={self} bind:this={self}
on:auxclick onauxclick={bubble('auxclick')}
on:blur onblur={bubble('blur')}
on:click onclick={bubble('click')}
on:dblclick ondblclick={bubble('dblclick')}
on:dragend ondragend={bubble('dragend')}
on:dragleave ondragleave={bubble('dragleave')}
on:dragover ondragover={bubble('dragover')}
on:dragstart ondragstart={bubble('dragstart')}
on:drop ondrop={bubble('drop')}
on:mouseup onmouseup={bubble('mouseup')}
on:pointerdown onpointerdown={bubble('pointerdown')}
on:pointerenter onpointerenter={bubble('pointerenter')}
on:pointerleave onpointerleave={bubble('pointerleave')}
on:scroll onscroll={bubble('scroll')}
{...$$restProps} {...rest}
> >
<slot /> {@render children?.()}
</div> </div>
<!-- Unused (each impacts performance, see <https://github.com/GraphiteEditor/Graphite/issues/1877>): <!-- Unused (each impacts performance, see <https://github.com/GraphiteEditor/Graphite/issues/1877>):

View file

@ -461,7 +461,7 @@
}); });
</script> </script>
<LayoutCol class="document" on:dragover={(e) => e.preventDefault()} on:drop={dropFile}> <LayoutCol class="document" ondragover={(e) => e.preventDefault()} ondrop={dropFile}>
<LayoutRow class="control-bar" classes={{ "for-graph": $document.graphViewOverlayOpen }} scrollableX={true}> <LayoutRow class="control-bar" classes={{ "for-graph": $document.graphViewOverlayOpen }} scrollableX={true}>
{#if !$document.graphViewOverlayOpen} {#if !$document.graphViewOverlayOpen}
<WidgetLayout layout={$document.documentModeLayout} /> <WidgetLayout layout={$document.documentModeLayout} />
@ -517,13 +517,13 @@
y={cursorTop} y={cursorTop}
/> />
{/if} {/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}> <svg class="artboards" style:width={canvasWidthCSS} style:height={canvasHeightCSS}>
{@html artworkSvg} {@html artworkSvg}
</svg> </svg>
<div class="text-input" style:width={canvasWidthCSS} style:height={canvasHeightCSS} style:pointer-events={showTextInput ? "auto" : ""}> <div class="text-input" style:width={canvasWidthCSS} style:height={canvasHeightCSS} style:pointer-events={showTextInput ? "auto" : ""}>
{#if showTextInput} {#if showTextInput}
<div bind:this={textInput} style:transform="matrix({textInputMatrix})" on:scroll={preventTextEditingScroll} /> <div bind:this={textInput} style:transform="matrix({textInputMatrix})" onscroll={preventTextEditingScroll} />
{/if} {/if}
</div> </div>
<canvas <canvas
@ -545,10 +545,10 @@
direction="Vertical" direction="Vertical"
thumbLength={scrollbarSize.y} thumbLength={scrollbarSize.y}
thumbPosition={scrollbarPos.y} thumbPosition={scrollbarPos.y}
on:trackShift={({ detail }) => editor.handle.panCanvasByFraction(0, detail)} ontrackShift={(detail) => editor.handle.panCanvasByFraction(0, detail)}
on:thumbPosition={({ detail }) => panCanvasY(detail)} onthumbPosition={(detail) => panCanvasY(detail)}
on:thumbDragStart={() => editor.handle.panCanvasAbortPrepare(false)} onthumbDragStart={() => editor.handle.panCanvasAbortPrepare(false)}
on:thumbDragAbort={() => editor.handle.panCanvasAbort(false)} onthumbDragAbort={() => editor.handle.panCanvasAbort(false)}
/> />
</LayoutCol> </LayoutCol>
</LayoutRow> </LayoutRow>
@ -557,11 +557,11 @@
direction="Horizontal" direction="Horizontal"
thumbLength={scrollbarSize.x} thumbLength={scrollbarSize.x}
thumbPosition={scrollbarPos.x} thumbPosition={scrollbarPos.x}
on:trackShift={({ detail }) => editor.handle.panCanvasByFraction(detail, 0)} ontrackShift={(detail) => editor.handle.panCanvasByFraction(detail, 0)}
on:thumbPosition={({ detail }) => panCanvasX(detail)} onthumbPosition={(detail) => panCanvasX(detail)}
on:thumbDragEnd={() => editor.handle.setGridAlignedEdges()} onthumbDragEnd={() => editor.handle.setGridAlignedEdges()}
on:thumbDragStart={() => editor.handle.panCanvasAbortPrepare(true)} onthumbDragStart={() => editor.handle.panCanvasAbortPrepare(true)}
on:thumbDragAbort={() => editor.handle.panCanvasAbort(true)} onthumbDragAbort={() => editor.handle.panCanvasAbort(true)}
/> />
</LayoutRow> </LayoutRow>
</LayoutCol> </LayoutCol>

View file

@ -564,7 +564,7 @@
<IconButton <IconButton
class={"status-toggle"} class={"status-toggle"}
classes={{ inherited: !listing.entry.parentsUnlocked }} classes={{ inherited: !listing.entry.parentsUnlocked }}
action={(e) => (toggleLayerLock(listing.entry.id), e?.stopPropagation())} onclick={(e) => (toggleLayerLock(listing.entry.id), e?.stopPropagation())}
size={24} size={24}
icon={listing.entry.unlocked ? "PadlockUnlocked" : "PadlockLocked"} icon={listing.entry.unlocked ? "PadlockUnlocked" : "PadlockLocked"}
hoverIcon={listing.entry.unlocked ? "PadlockLocked" : "PadlockUnlocked"} hoverIcon={listing.entry.unlocked ? "PadlockLocked" : "PadlockUnlocked"}
@ -574,7 +574,7 @@
<IconButton <IconButton
class={"status-toggle"} class={"status-toggle"}
classes={{ inherited: !listing.entry.parentsVisible }} classes={{ inherited: !listing.entry.parentsVisible }}
action={(e) => (toggleNodeVisibilityLayerPanel(listing.entry.id), e?.stopPropagation())} onclick={(e) => (toggleNodeVisibilityLayerPanel(listing.entry.id), e?.stopPropagation())}
size={24} size={24}
icon={listing.entry.visible ? "EyeVisible" : "EyeHidden"} icon={listing.entry.visible ? "EyeVisible" : "EyeHidden"}
hoverIcon={listing.entry.visible ? "EyeHide" : "EyeShow"} hoverIcon={listing.entry.visible ? "EyeHide" : "EyeShow"}

View file

@ -381,7 +381,7 @@
class="remove-button-import" class="remove-button-import"
data-index={index} data-index={index}
data-import-text-edge data-import-text-edge
action={() => { onclick={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}} }}
/> />
@ -401,7 +401,7 @@
<IconButton <IconButton
size={24} size={24}
icon="Add" icon="Add"
action={() => { onclick={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}} }}
/> />
@ -441,7 +441,7 @@
class="remove-button-export" class="remove-button-export"
data-index={index} data-index={index}
data-export-text-edge data-export-text-edge
action={() => { onclick={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}} }}
/> />
@ -472,7 +472,7 @@
<IconButton <IconButton
size={24} size={24}
icon={"Add"} icon={"Add"}
action={() => { onclick={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}} }}
/> />
@ -592,7 +592,7 @@
data-visibility-button data-visibility-button
size={24} size={24}
icon={node.visible ? "EyeVisible" : "EyeHidden"} icon={node.visible ? "EyeVisible" : "EyeHidden"}
action={() => { onclick={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */ /* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}} }}
tooltip={node.visible ? "Visible" : "Hidden"} tooltip={node.visible ? "Visible" : "Hidden"}

View file

@ -31,7 +31,7 @@
icon={widgetData.pinned ? "PinActive" : "PinInactive"} 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"} 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} size={24}
action={(e) => { onclick={(e) => {
editor.handle.setNodePinned(widgetData.id, !widgetData.pinned); editor.handle.setNodePinned(widgetData.id, !widgetData.pinned);
e?.stopPropagation(); e?.stopPropagation();
}} }}
@ -41,7 +41,7 @@
icon={"Trash"} icon={"Trash"}
tooltip={"Delete this node from the layer chain"} tooltip={"Delete this node from the layer chain"}
size={24} size={24}
action={(e) => { onclick={(e) => {
editor.handle.deleteNode(widgetData.id); editor.handle.deleteNode(widgetData.id);
e?.stopPropagation(); e?.stopPropagation();
}} }}
@ -52,7 +52,7 @@
hoverIcon={widgetData.visible ? "EyeHide" : "EyeShow"} hoverIcon={widgetData.visible ? "EyeHide" : "EyeShow"}
tooltip={widgetData.visible ? "Hide this node" : "Show this node"} tooltip={widgetData.visible ? "Hide this node" : "Show this node"}
size={24} size={24}
action={(e) => { onclick={(e) => {
editor.handle.toggleNodeVisibilityLayerPanel(widgetData.id); editor.handle.toggleNodeVisibilityLayerPanel(widgetData.id);
e?.stopPropagation(); e?.stopPropagation();
}} }}

View file

@ -85,32 +85,32 @@
{#each widgets as component, index} {#each widgets as component, index}
{@const checkboxInput = narrowWidgetProps(component.props, "CheckboxInput")} {@const checkboxInput = narrowWidgetProps(component.props, "CheckboxInput")}
{#if checkboxInput} {#if checkboxInput}
<CheckboxInput {...exclude(checkboxInput)} on:checked={({ detail }) => widgetValueCommitAndUpdate(index, detail)} /> <CheckboxInput {...exclude(checkboxInput)} onchecked={(detail) => widgetValueCommitAndUpdate(index, detail)} />
{/if} {/if}
{@const colorInput = narrowWidgetProps(component.props, "ColorInput")} {@const colorInput = narrowWidgetProps(component.props, "ColorInput")}
{#if 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} {/if}
{@const curvesInput = narrowWidgetProps(component.props, "CurveInput")} {@const curvesInput = narrowWidgetProps(component.props, "CurveInput")}
{#if curvesInput} {#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} {/if}
{@const dropdownInput = narrowWidgetProps(component.props, "DropdownInput")} {@const dropdownInput = narrowWidgetProps(component.props, "DropdownInput")}
{#if dropdownInput} {#if dropdownInput}
<DropdownInput <DropdownInput
{...exclude(dropdownInput)} {...exclude(dropdownInput)}
on:hoverInEntry={({ detail }) => { onhoverInEntry={(detail) => {
return widgetValueUpdate(index, detail); return widgetValueUpdate(index, detail);
}} }}
on:hoverOutEntry={({ detail }) => { onhoverOutEntry={(detail) => {
return widgetValueUpdate(index, detail); return widgetValueUpdate(index, detail);
}} }}
on:selectedIndex={({ detail }) => widgetValueCommitAndUpdate(index, detail)} onselectedIndex={(detail) => widgetValueCommitAndUpdate(index, detail)}
/> />
{/if} {/if}
{@const fontInput = narrowWidgetProps(component.props, "FontInput")} {@const fontInput = narrowWidgetProps(component.props, "FontInput")}
{#if fontInput} {#if fontInput}
<FontInput {...exclude(fontInput)} on:changeFont={({ detail }) => widgetValueCommitAndUpdate(index, detail)} /> <FontInput {...exclude(fontInput)} onchangeFont={(detail) => widgetValueCommitAndUpdate(index, detail)} />
{/if} {/if}
{@const parameterExposeButton = narrowWidgetProps(component.props, "ParameterExposeButton")} {@const parameterExposeButton = narrowWidgetProps(component.props, "ParameterExposeButton")}
{#if parameterExposeButton} {#if parameterExposeButton}
@ -118,7 +118,7 @@
{/if} {/if}
{@const iconButton = narrowWidgetProps(component.props, "IconButton")} {@const iconButton = narrowWidgetProps(component.props, "IconButton")}
{#if iconButton} {#if iconButton}
<IconButton {...exclude(iconButton)} action={() => widgetValueCommitAndUpdate(index, undefined)} /> <IconButton {...exclude(iconButton)} onclick={() => widgetValueCommitAndUpdate(index, undefined)} />
{/if} {/if}
{@const iconLabel = narrowWidgetProps(component.props, "IconLabel")} {@const iconLabel = narrowWidgetProps(component.props, "IconLabel")}
{#if iconLabel} {#if iconLabel}
@ -136,15 +136,15 @@
{#if numberInput} {#if numberInput}
<NumberInput <NumberInput
{...exclude(numberInput)} {...exclude(numberInput)}
on:value={({ detail }) => debouncer((value) => widgetValueUpdate(index, value)).debounceUpdateValue(detail)} onvalue={(detail) => debouncer((value) => widgetValueUpdate(index, value)).debounceUpdateValue(detail)}
on:startHistoryTransaction={() => widgetValueCommit(index, numberInput.value)} onstartHistoryTransaction={() => widgetValueCommit(index, numberInput.value)}
incrementCallbackIncrease={() => widgetValueCommitAndUpdate(index, "Increment")} incrementCallbackIncrease={() => widgetValueCommitAndUpdate(index, "Increment")}
incrementCallbackDecrease={() => widgetValueCommitAndUpdate(index, "Decrement")} incrementCallbackDecrease={() => widgetValueCommitAndUpdate(index, "Decrement")}
/> />
{/if} {/if}
{@const referencePointInput = narrowWidgetProps(component.props, "ReferencePointInput")} {@const referencePointInput = narrowWidgetProps(component.props, "ReferencePointInput")}
{#if referencePointInput} {#if referencePointInput}
<ReferencePointInput {...exclude(referencePointInput)} on:value={({ detail }) => widgetValueCommitAndUpdate(index, detail)} /> <ReferencePointInput {...exclude(referencePointInput)} onvalue={(detail) => widgetValueCommitAndUpdate(index, detail)} />
{/if} {/if}
{@const popoverButton = narrowWidgetProps(component.props, "PopoverButton")} {@const popoverButton = narrowWidgetProps(component.props, "PopoverButton")}
{#if popoverButton} {#if popoverButton}
@ -154,7 +154,7 @@
{/if} {/if}
{@const radioInput = narrowWidgetProps(component.props, "RadioInput")} {@const radioInput = narrowWidgetProps(component.props, "RadioInput")}
{#if radioInput} {#if radioInput}
<RadioInput {...exclude(radioInput)} on:selectedIndex={({ detail }) => widgetValueCommitAndUpdate(index, detail)} /> <RadioInput {...exclude(radioInput)} onselect={(detail) => widgetValueCommitAndUpdate(index, detail)} />
{/if} {/if}
{@const separator = narrowWidgetProps(component.props, "Separator")} {@const separator = narrowWidgetProps(component.props, "Separator")}
{#if separator} {#if separator}
@ -166,7 +166,7 @@
{/if} {/if}
{@const textAreaInput = narrowWidgetProps(component.props, "TextAreaInput")} {@const textAreaInput = narrowWidgetProps(component.props, "TextAreaInput")}
{#if textAreaInput} {#if textAreaInput}
<TextAreaInput {...exclude(textAreaInput)} on:commitText={({ detail }) => widgetValueCommitAndUpdate(index, detail)} /> <TextAreaInput {...exclude(textAreaInput)} oncommitText={(detail) => widgetValueCommitAndUpdate(index, detail)} />
{/if} {/if}
{@const textButton = narrowWidgetProps(component.props, "TextButton")} {@const textButton = narrowWidgetProps(component.props, "TextButton")}
{#if textButton} {#if textButton}
@ -178,7 +178,7 @@
{/if} {/if}
{@const textInput = narrowWidgetProps(component.props, "TextInput")} {@const textInput = narrowWidgetProps(component.props, "TextInput")}
{#if textInput} {#if textInput}
<TextInput {...exclude(textInput)} on:commitText={({ detail }) => widgetValueCommitAndUpdate(index, detail)} /> <TextInput {...exclude(textInput)} oncommitText={(detail) => widgetValueCommitAndUpdate(index, detail)} />
{/if} {/if}
{@const textLabel = narrowWidgetProps(component.props, "TextLabel")} {@const textLabel = narrowWidgetProps(component.props, "TextLabel")}
{#if textLabel} {#if textLabel}

View file

@ -2,11 +2,20 @@
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte"; import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
export let labels: string[]; interface Props {
export let disabled = false; labels: string[];
export let tooltip: string | undefined = undefined; disabled?: boolean;
// Callbacks tooltip?: string | undefined;
export let action: (index: number) => void; // Callbacks
action: (index: number) => void;
}
let {
labels,
disabled = false,
tooltip = undefined,
action
}: Props = $props();
</script> </script>
<LayoutRow class="breadcrumb-trail-buttons" {tooltip}> <LayoutRow class="breadcrumb-trail-buttons" {tooltip}>

View file

@ -1,24 +1,37 @@
<script lang="ts"> <script lang="ts">
import { type IconName, type IconSize } from "@graphite/utility-functions/icons"; 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"; import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
export let icon: IconName; type ButtonHTMLElementProps = SvelteHTMLElements["button"];
export let hoverIcon: IconName | undefined = undefined;
export let size: IconSize; interface Props extends ButtonHTMLElementProps {
export let disabled = false; class?: string;
export let active = false; classes?: Record<string, boolean>;
export let tooltip: string | undefined = undefined; icon: IconName;
// Callbacks hoverIcon?: IconName | undefined;
export let action: (e?: MouseEvent) => void; size: IconSize;
disabled?: boolean;
active?: boolean;
tooltip?: string | undefined;
}
let className = ""; let {
export { className as class }; class: className = "",
export let classes: Record<string, boolean> = {}; 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] : [])) .flatMap(([className, stateName]) => (stateName ? [className] : []))
.join(" "); .join(" "));
</script> </script>
<button <button
@ -26,11 +39,10 @@
class:hover-icon={hoverIcon && !disabled} class:hover-icon={hoverIcon && !disabled}
class:disabled class:disabled
class:active class:active
on:click={action}
{disabled} {disabled}
title={tooltip} title={tooltip}
tabindex={active ? -1 : 0} tabindex={active ? -1 : 0}
{...$$restProps} {...rest}
> >
<IconLabel {icon} /> <IconLabel {icon} />
{#if hoverIcon && !disabled} {#if hoverIcon && !disabled}
@ -55,7 +67,7 @@
} }
// The `where` pseudo-class does not contribtue to specificity // The `where` pseudo-class does not contribtue to specificity
& + :where(.icon-button) { & + :where(:global(.icon-button)) {
margin-left: 0; margin-left: 0;
} }

View file

@ -1,23 +1,33 @@
<script lang="ts"> <script lang="ts">
import { IMAGE_BASE64_STRINGS } from "@graphite/utility-functions/images"; import { IMAGE_BASE64_STRINGS } from "@graphite/utility-functions/images";
let className = ""; interface Props {
export { className as class }; class?: string;
export let classes: Record<string, boolean> = {}; classes?: Record<string, boolean>;
image: string;
width: string | undefined;
height: string | undefined;
tooltip?: string | undefined;
// Callbacks
action: (e?: MouseEvent) => void;
}
export let image: string; let {
export let width: string | undefined; class: className = "",
export let height: string | undefined; classes = {},
export let tooltip: string | undefined = undefined; image,
// Callbacks width,
export let action: (e?: MouseEvent) => void; height,
tooltip = undefined,
action
}: Props = $props();
$: extraClasses = Object.entries(classes) let extraClasses = $derived(Object.entries(classes)
.flatMap(([className, stateName]) => (stateName ? [className] : [])) .flatMap(([className, stateName]) => (stateName ? [className] : []))
.join(" "); .join(" "));
</script> </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> <style lang="scss" global>
.image-label { .image-label {

View file

@ -3,11 +3,20 @@
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
export let exposed: boolean; interface Props {
export let dataType: FrontendGraphDataType; exposed: boolean;
export let tooltip: string | undefined = undefined; dataType: FrontendGraphDataType;
// Callbacks tooltip?: string | undefined;
export let action: (e?: MouseEvent) => void; // Callbacks
action: (e?: MouseEvent) => void;
}
let {
exposed,
dataType,
tooltip = undefined,
action
}: Props = $props();
</script> </script>
<LayoutRow class="parameter-expose-button"> <LayoutRow class="parameter-expose-button">
@ -15,7 +24,7 @@
class:exposed class:exposed
style:--data-type-color={`var(--color-data-${dataType.toLowerCase()})`} style:--data-type-color={`var(--color-data-${dataType.toLowerCase()})`}
style:--data-type-color-dim={`var(--color-data-${dataType.toLowerCase()}-dim)`} style:--data-type-color-dim={`var(--color-data-${dataType.toLowerCase()}-dim)`}
on:click={action} onclick={action}
title={tooltip} title={tooltip}
tabindex="-1" tabindex="-1"
> >

View file

@ -7,17 +7,30 @@
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte"; import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte"; import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
export let style: PopoverButtonStyle = "DropdownArrow"; interface Props {
export let menuDirection: MenuDirection = "Bottom"; style?: PopoverButtonStyle;
export let icon: IconName | undefined = undefined; menuDirection?: MenuDirection;
export let tooltip: string | undefined = undefined; icon?: IconName | undefined;
export let disabled = false; tooltip?: string | undefined;
export let popoverMinWidth = 1; disabled?: boolean;
popoverMinWidth?: number;
// Callbacks
action?: (() => void) | undefined;
children?: import('svelte').Snippet;
}
// Callbacks let {
export let action: (() => void) | undefined = undefined; 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() { function onClick() {
open = true; open = true;
@ -26,13 +39,13 @@
</script> </script>
<LayoutRow class="popover-button" classes={{ "has-icon": icon !== undefined, "direction-top": menuDirection === "Top" }}> <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} {#if icon !== undefined}
<IconLabel class="descriptive-icon" classes={{ open }} {disabled} {icon} {tooltip} /> <IconLabel class="descriptive-icon" classes={{ open }} {disabled} {icon} {tooltip} />
{/if} {/if}
<FloatingMenu {open} on:open={({ detail }) => (open = detail)} minWidth={popoverMinWidth} type="Popover" direction={menuDirection || "Bottom"}> <FloatingMenu bind:open minWidth={popoverMinWidth} type="Popover" direction={menuDirection || "Bottom"}>
<slot /> {@render children?.()}
</FloatingMenu> </FloatingMenu>
</LayoutRow> </LayoutRow>

View file

@ -7,25 +7,38 @@
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte"; import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte"; import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
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. // 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. // 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; interface Props {
export let icon: IconName | undefined = undefined; label: string;
export let hoverIcon: IconName | undefined = undefined; icon?: IconName | undefined;
export let emphasized = false; hoverIcon?: IconName | undefined;
export let flush = false; emphasized?: boolean;
export let minWidth = 0; flush?: boolean;
export let disabled = false; minWidth?: number;
export let tooltip: string | undefined = undefined; disabled?: boolean;
export let menuListChildren: MenuListEntry[][] | undefined = undefined; tooltip?: string | undefined;
menuListChildren?: MenuListEntry[][] | undefined;
// TODO: Replace this with an event binding (and on other components that do this)
action: (() => void) | undefined;
}
// Callbacks let {
// TODO: Replace this with an event binding (and on other components that do this) label,
export let action: (() => void) | undefined; icon = undefined,
hoverIcon = undefined,
emphasized = false,
flush = false,
minWidth = 0,
disabled = false,
tooltip = undefined,
menuListChildren = $bindable([]),
action
}: 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 // Handles either a button click or, if applicable, the opening of the menu list floating menu
function onClick(e: MouseEvent) { function onClick(e: MouseEvent) {
@ -50,7 +63,7 @@
<ConditionalWrapper condition={menuListChildrenExists} wrapperClass="text-button-container"> <ConditionalWrapper condition={menuListChildrenExists} wrapperClass="text-button-container">
<button <button
class="text-button" class="text-button"
class:open={self?.open} class:open={open}
class:hover-icon={hoverIcon && !disabled} class:hover-icon={hoverIcon && !disabled}
class:emphasized class:emphasized
class:disabled class:disabled
@ -62,7 +75,7 @@
data-text-button data-text-button
tabindex={disabled ? -1 : 0} tabindex={disabled ? -1 : 0}
data-floating-menu-spawner={menuListChildrenExists ? "" : "no-hover-transfer"} data-floating-menu-spawner={menuListChildrenExists ? "" : "no-hover-transfer"}
on:click={onClick} onclick={onClick}
> >
{#if icon} {#if icon}
<IconLabel {icon} /> <IconLabel {icon} />
@ -76,9 +89,8 @@
</button> </button>
{#if menuListChildrenExists} {#if menuListChildrenExists}
<MenuList <MenuList
on:open={({ detail }) => self && (self.open = detail)} bind:open={open}
open={self?.open || false} entries={menuListChildren}
entries={menuListChildren || []}
direction="Bottom" direction="Bottom"
minWidth={240} minWidth={240}
drawIcon={true} drawIcon={true}

View file

@ -1,25 +1,34 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte";
import type { IconName } from "@graphite/utility-functions/icons"; import type { IconName } from "@graphite/utility-functions/icons";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte"; import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
const dispatch = createEventDispatcher<{ checked: boolean }>();
export let checked = false; interface Props {
export let disabled = false; checked?: boolean;
export let icon: IconName = "Checkmark"; disabled?: boolean;
export let tooltip: string | undefined = undefined; icon?: IconName;
export let forLabel: bigint | undefined = undefined; 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); const backupId = String(Math.random()).substring(2);
$: id = forLabel !== undefined ? String(forLabel) : backupId; let id = $derived(forLabel !== undefined ? String(forLabel) : backupId);
$: displayIcon = (!checked && icon === "Checkmark" ? "Empty12px" : icon) as IconName; let displayIcon = $derived((!checked && icon === "Checkmark" ? "Empty12px" : icon) as IconName);
export function isChecked() { export function isChecked() {
return checked; return checked;
@ -41,12 +50,12 @@
type="checkbox" type="checkbox"
id={`checkbox-input-${id}`} id={`checkbox-input-${id}`}
bind:checked bind:checked
on:change={(_) => dispatch("checked", inputElement?.checked || false)} onchange={(_) => onchecked?.(inputElement?.checked ?? false)}
{disabled} {disabled}
tabindex={disabled ? -1 : 0} tabindex={disabled ? -1 : 0}
bind:this={inputElement} 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"> <LayoutRow class="checkbox-box">
<IconLabel icon={displayIcon} /> <IconLabel icon={displayIcon} />
</LayoutRow> </LayoutRow>

View file

@ -1,6 +1,4 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte";
import type { FillChoice } from "@graphite/messages"; import type { FillChoice } from "@graphite/messages";
import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages"; import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages";
@ -8,41 +6,49 @@
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte"; import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.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; let {
export let disabled = false; value,
export let allowNone = false; disabled = false,
// export let allowTransparency = false; // TODO: Implement allowNone = false,
export let tooltip: string | undefined = undefined; 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> </script>
<LayoutCol class="color-button" classes={{ open, disabled, none, transparency, outlined }} {tooltip}> <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} {#if disabled && value instanceof Color && !value.none}
<TextLabel>sRGB</TextLabel> <TextLabel>sRGB</TextLabel>
{/if} {/if}
</button> </button>
<ColorPicker <ColorPicker
{open} bind:open
on:open={({ detail }) => (open = detail)}
colorOrGradient={value} colorOrGradient={value}
on:colorOrGradient={({ detail }) => { oncolorOrGradient={onvalue}
value = detail; onstartHistoryTransaction={() => {
dispatch("value", detail);
}}
on:startHistoryTransaction={() => {
// This event is sent to the backend so it knows to start a transaction for the history system. See discussion for some explanation: // 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> // <https://github.com/GraphiteEditor/Graphite/pull/1584#discussion_r1477592483>
dispatch("startHistoryTransaction"); onstartHistoryTransaction?.()
}} }}
{allowNone} {allowNone}
/> />

View file

@ -1,26 +1,34 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte";
import type { Curve, CurveManipulatorGroup } from "@graphite/messages"; import type { Curve, CurveManipulatorGroup } from "@graphite/messages";
import { clamp } from "@graphite/utility-functions/math"; import { clamp } from "@graphite/utility-functions/math";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; 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; value: Curve;
}>(); disabled?: boolean;
tooltip?: string | undefined;
children?: import('svelte').Snippet;
onvalue?: (curve: Curve) => void;
}
export let classes: Record<string, boolean> = {}; let {
let styleName = ""; classes = {},
export { styleName as style }; style: styleName = "",
export let styles: Record<string, string | number | undefined> = {}; styles = {},
export let value: Curve; value,
export let disabled = false; disabled = false,
export let tooltip: string | undefined = undefined; tooltip = undefined,
children,
onvalue,
}: Props = $props();
const GRID_SIZE = 4; const GRID_SIZE = 4;
let groups: CurveManipulatorGroup[] = [ let groups: CurveManipulatorGroup[] = $state([
{ {
anchor: [0, 0], anchor: [0, 0],
handles: [ handles: [
@ -42,20 +50,14 @@
[2, 2], [2, 2],
], ],
}, },
]; ]);
let selectedNodeIndex: number | undefined = undefined; let selectedNodeIndex: number | undefined = $state(undefined);
let draggedNodeIndex: number | undefined = 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() { function updateCurve() {
dispatch("value", { onvalue?.({
manipulatorGroups: groups.slice(1, groups.length - 1), manipulatorGroups: groups.slice(1, groups.length - 1),
firstHandle: groups[0].handles[1], firstHandle: groups[0].handles[1],
lastHandle: groups[groups.length - 1].handles[0], lastHandle: groups[groups.length - 1].handles[0],
@ -83,7 +85,6 @@
selectedNodeIndex = undefined; selectedNodeIndex = undefined;
groups.splice(i, 1); groups.splice(i, 1);
groups = groups;
dAttribute = recalculateSvgPath(); dAttribute = recalculateSvgPath();
@ -186,10 +187,16 @@
dAttribute = recalculateSvgPath(); dAttribute = recalculateSvgPath();
updateCurve(); 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> </script>
<LayoutRow class={"curve-input"} classes={{ disabled, ...classes }} style={styleName} {styles} {tooltip}> <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} {#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 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`} /> <path class="grid" d={`M ${(i + 1) / GRID_SIZE} 0 L ${(i + 1) / GRID_SIZE} 1`} />
@ -199,14 +206,14 @@
{@const group = groups[selectedNodeIndex]} {@const group = groups[selectedNodeIndex]}
{#each [0, 1] as i} {#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" /> <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} {/each}
{/if} {/if}
{#each groups as group, i} {#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} {/each}
</svg> </svg>
<slot /> {@render children?.()}
</LayoutRow> </LayoutRow>
<style lang="scss" global> <style lang="scss" global>

View file

@ -1,6 +1,4 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte";
import type { MenuListEntry } from "@graphite/messages"; import type { MenuListEntry } from "@graphite/messages";
import MenuList from "@graphite/components/floating-menus/MenuList.svelte"; import MenuList from "@graphite/components/floating-menus/MenuList.svelte";
@ -10,29 +8,41 @@
const DASH_ENTRY = { value: "", label: "-" }; 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; interface Props {
let self: LayoutRow | undefined; 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[][]; let {
export let selectedIndex: number | undefined = undefined; // When not provided, a dash is displayed entries,
export let drawIcon = false; selectedIndex = undefined,
export let interactive = true; drawIcon = false,
export let disabled = false; interactive = true,
export let tooltip: string | undefined = undefined; disabled = false,
export let minWidth = 0; tooltip = undefined,
export let maxWidth = 0; minWidth = $bindable(0),
maxWidth = 0,
onhoverInEntry,
onhoverOutEntry,
onselectedIndex,
}: Props = $props();
let activeEntry = makeActiveEntry(); let activeEntry = $state(makeActiveEntry());
let activeEntrySkipWatcher = false; let activeEntrySkipWatcher = false;
let initialSelectedIndex: number | undefined = undefined; let initialSelectedIndex: number | undefined = undefined;
let open = false; let open = $state(false);
$: watchSelectedIndex(selectedIndex);
$: watchEntries(entries);
$: watchActiveEntry(activeEntry);
$: watchOpen(open);
function watchOpen(open: boolean) { function watchOpen(open: boolean) {
initialSelectedIndex = open ? selectedIndex : undefined; initialSelectedIndex = open ? selectedIndex : undefined;
@ -56,17 +66,17 @@
activeEntrySkipWatcher = false; activeEntrySkipWatcher = false;
} else if (activeEntry !== DASH_ENTRY) { } 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. // 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); if (initialSelectedIndex !== undefined) onhoverInEntry?.(initialSelectedIndex);
dispatch("selectedIndex", entries.flat().indexOf(activeEntry)); onselectedIndex?.(entries.flat().indexOf(activeEntry));
} }
} }
function dispatchHoverInEntry(hoveredEntry: MenuListEntry) { function dispatchHoverInEntry(hoveredEntry: MenuListEntry) {
dispatch("hoverInEntry", entries.flat().indexOf(hoveredEntry)); onhoverInEntry?.(entries.flat().indexOf(hoveredEntry));
} }
function dispatchHoverOutEntry() { function dispatchHoverOutEntry() {
if (initialSelectedIndex !== undefined) dispatch("hoverOutEntry", initialSelectedIndex); if (initialSelectedIndex !== undefined) onhoverOutEntry?.(initialSelectedIndex);
} }
function makeActiveEntry(): MenuListEntry { function makeActiveEntry(): MenuListEntry {
@ -82,6 +92,15 @@
const blurTarget = (e.target as HTMLDivElement | undefined)?.closest("[data-dropdown-input]") || undefined; const blurTarget = (e.target as HTMLDivElement | undefined)?.closest("[data-dropdown-input]") || undefined;
if (blurTarget !== self?.div?.()) open = false; if (blurTarget !== self?.div?.()) open = false;
} }
$effect(() => {
watchSelectedIndex(selectedIndex);
});
$effect(() => {
watchActiveEntry(activeEntry);
});
$effect(() => {
watchOpen(open);
});
</script> </script>
<LayoutRow <LayoutRow
@ -94,8 +113,8 @@
class="dropdown-box" class="dropdown-box"
classes={{ disabled, open }} classes={{ disabled, open }}
{tooltip} {tooltip}
on:click={() => !disabled && (open = true)} onclick={() => !disabled && (open = true)}
on:blur={unFocusDropdownBox} onblur={unFocusDropdownBox}
tabindex={disabled ? -1 : 0} tabindex={disabled ? -1 : 0}
data-floating-menu-spawner data-floating-menu-spawner
> >
@ -106,13 +125,12 @@
<IconLabel class="dropdown-arrow" icon="DropdownArrow" /> <IconLabel class="dropdown-arrow" icon="DropdownArrow" />
</LayoutRow> </LayoutRow>
<MenuList <MenuList
on:naturalWidth={({ detail }) => (minWidth = detail)} onnaturalWidth={(detail) => (minWidth = detail)}
{activeEntry} {activeEntry}
on:activeEntry={({ detail }) => (activeEntry = detail)} onactiveEntry={(detail) => (activeEntry = detail)}
on:hoverInEntry={({ detail }) => dispatchHoverInEntry(detail)} onhoverInEntry={dispatchHoverInEntry}
on:hoverOutEntry={() => dispatchHoverOutEntry()} onhoverOutEntry={dispatchHoverOutEntry}
{open} bind:open
on:open={({ detail }) => (open = detail)}
{entries} {entries}
{drawIcon} {drawIcon}
{interactive} {interactive}

View file

@ -1,41 +1,65 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte";
import { platformIsMac } from "@graphite/utility-functions/platform"; import { platformIsMac } from "@graphite/utility-functions/platform";
import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte"; import { preventEscapeClosingParentFloatingMenu } from "@graphite/components/layout/FloatingMenu.svelte";
import LayoutRow from "@graphite/components/layout/LayoutRow.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; value: string;
textFocused: undefined; label?: string | undefined;
textChanged: undefined; spellcheck?: boolean;
textChangeCanceled: undefined; 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 = ""; type Props = CommonProps & (
export { className as class }; CommonProps['textarea'] extends true
export let classes: Record<string, boolean> = {}; ? TextAreaHTMLElementProps // If 'textarea' is explicitly true
let styleName = ""; : InputHTMLElementProps // Otherwise (false, undefined, or missing)
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;
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 id = String(Math.random()).substring(2);
let macKeyboardLayout = platformIsMac(); let macKeyboardLayout = platformIsMac();
$: inputValue = value;
$: dispatch("value", inputValue);
// Select (highlight) all the text. For technical reasons, it is necessary to pass the current text. // Select (highlight) all the text. For technical reasons, it is necessary to pass the current text.
export function selectAllText(currentText: string) { export function selectAllText(currentText: string) {
if (!inputOrTextarea) return; if (!inputOrTextarea) return;
@ -54,7 +78,7 @@
} }
export function getValue(): string { export function getValue(): string {
return inputOrTextarea?.value || ""; return inputOrTextarea?.value ?? "";
} }
export function setInputElementValue(value: string) { export function setInputElementValue(value: string) {
@ -68,10 +92,15 @@
} }
function cancel() { function cancel() {
dispatch("textChangeCanceled"); ontextChangeCanceled?.();
if (inputOrTextarea) preventEscapeClosingParentFloatingMenu(inputOrTextarea); if (inputOrTextarea) preventEscapeClosingParentFloatingMenu(inputOrTextarea);
} }
function onkeydown(e: KeyboardEvent) {
e.key === "Enter" && onchange?.(e);
e.key === "Escape" && cancel();
}
</script> </script>
<!-- This is a base component, extended by others like NumberInput and TextInput. It should not be used directly. --> <!-- This is a base component, extended by others like NumberInput and TextInput. It should not be used directly. -->
@ -85,14 +114,13 @@
{disabled} {disabled}
{placeholder} {placeholder}
bind:this={inputOrTextarea} bind:this={inputOrTextarea}
bind:value={inputValue} bind:value={value}
on:focus={() => dispatch("textFocused")} onfocus={onfocus}
on:blur={() => dispatch("textChanged")} onblur={onchange}
on:change={() => dispatch("textChanged")} onchange={onchange}
on:keydown={(e) => e.key === "Enter" && dispatch("textChanged")} onkeydown={onkeydown}
on:keydown={(e) => e.key === "Escape" && cancel()} onpointerdown={onpointerdown}
on:pointerdown oncontextmenu={(e) => hideContextMenu && e.preventDefault()}
on:contextmenu={(e) => hideContextMenu && e.preventDefault()}
data-input-element data-input-element
/> />
{:else} {:else}
@ -104,20 +132,19 @@
{spellcheck} {spellcheck}
{disabled} {disabled}
bind:this={inputOrTextarea} bind:this={inputOrTextarea}
bind:value={inputValue} bind:value={value}
on:focus={() => dispatch("textFocused")} onfocus={onfocus}
on:blur={() => dispatch("textChanged")} onblur={onchange}
on:change={() => dispatch("textChanged")} onchange={onchange}
on:keydown={(e) => (macKeyboardLayout ? e.metaKey : e.ctrlKey) && e.key === "Enter" && dispatch("textChanged")} onkeydown={onkeydown}
on:keydown={(e) => e.key === "Escape" && cancel()} onpointerdown={onpointerdown}
on:pointerdown oncontextmenu={(e) => hideContextMenu && e.preventDefault()}
on:contextmenu={(e) => hideContextMenu && e.preventDefault()} ></textarea>
/>
{/if} {/if}
{#if label} {#if label}
<label for={`field-input-${id}`} on:pointerdown>{label}</label> <label for={`field-input-${id}`} onpointerdown={onpointerdown}>{label}</label>
{/if} {/if}
<slot /> {@render children?.()}
</LayoutRow> </LayoutRow>
<style lang="scss" global> <style lang="scss" global>

View file

@ -1,5 +1,5 @@
<script lang="ts"> <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";
import type { FontsState } from "@graphite/state-providers/fonts"; import type { FontsState } from "@graphite/state-providers/fonts";
@ -11,26 +11,35 @@
const fonts = getContext<FontsState>("fonts"); const fonts = getContext<FontsState>("fonts");
const dispatch = createEventDispatcher<{ let menuList: MenuList | undefined = $state();
interface Props {
fontFamily: string; fontFamily: string;
fontStyle: 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; let open = $state(false);
export let fontStyle: string; let entries: MenuListEntry[] = $state([]);
export let isStyle = false; let activeEntry: MenuListEntry | undefined = $state(undefined);
export let disabled = false; let minWidth = $state(isStyle ? 0 : 300);
export let tooltip: string | undefined = undefined;
let open = false;
let entries: MenuListEntry[] = [];
let activeEntry: MenuListEntry | undefined = undefined;
let minWidth = isStyle ? 0 : 300;
$: watchFont(fontFamily, fontStyle);
async function watchFont(..._: string[]) { 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 // 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; let style;
if (isStyle) { if (isStyle) {
dispatch("fontStyle", newName); onfontStyle?.(newName);
family = fontFamily; family = fontFamily;
style = newName; style = newName;
} else { } else {
dispatch("fontFamily", newName); onfontFamily?.(newName);
family = newName; family = newName;
style = "Regular (400)"; style = "Regular (400)";
} }
const fontFileUrl = await fonts.getFontFileUrl(family, style); 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[]> { async function getEntries(): Promise<MenuListEntry[]> {
@ -100,6 +109,9 @@
activeEntry = getActiveEntry(entries); activeEntry = getActiveEntry(entries);
}); });
$effect(() => {
watchFont(fontFamily, fontStyle);
});
</script> </script>
<!-- TODO: Combine this widget into the DropdownInput widget --> <!-- TODO: Combine this widget into the DropdownInput widget -->
@ -110,18 +122,17 @@
styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }} styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }}
{tooltip} {tooltip}
tabindex={disabled ? -1 : 0} tabindex={disabled ? -1 : 0}
on:click={toggleOpen} onclick={toggleOpen}
data-floating-menu-spawner data-floating-menu-spawner
> >
<TextLabel class="dropdown-label">{activeEntry?.value || ""}</TextLabel> <TextLabel class="dropdown-label">{activeEntry?.value || ""}</TextLabel>
<IconLabel class="dropdown-arrow" icon="DropdownArrow" /> <IconLabel class="dropdown-arrow" icon="DropdownArrow" />
</LayoutRow> </LayoutRow>
<MenuList <MenuList
on:naturalWidth={({ detail }) => isStyle && (minWidth = detail)}
{activeEntry} {activeEntry}
on:activeEntry={({ detail }) => (activeEntry = detail)} onnaturalWidth={(detail) => isStyle && (minWidth = detail)}
{open} onactiveEntry={(detail) => (activeEntry = detail)}
on:open={({ detail }) => (open = detail)} bind:open
entries={[entries]} entries={[entries]}
minWidth={isStyle ? 0 : minWidth} minWidth={isStyle ? 0 : minWidth}
virtualScrollingEntryHeight={isStyle ? 0 : 20} virtualScrollingEntryHeight={isStyle ? 0 : 20}

View file

@ -1,5 +1,8 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, onMount, onDestroy } from "svelte"; import { createBubbler, preventDefault } from 'svelte/legacy';
const bubble = createBubbler();
import { onMount } from "svelte";
import { PRESS_REPEAT_DELAY_MS, PRESS_REPEAT_INTERVAL_MS } from "@graphite/io-managers/input"; 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";
@ -12,71 +15,91 @@
const BUTTONS_RIGHT = 0b0000_0010; const BUTTONS_RIGHT = 0b0000_0010;
const BUTTON_LEFT = 0; const BUTTON_LEFT = 0;
const BUTTON_RIGHT = 2; 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;
}
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,
}: Props = $props();
// Label let self: FieldInput | undefined = $state();
export let label: string | undefined = undefined; let inputRangeElement: HTMLInputElement | undefined = $state();
export let tooltip: string | undefined = undefined; let text = $state(displayText(value, unit));
// 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 editing = false; let editing = false;
let isDragging = false; let isDragging = false;
let pressingArrow = false; let pressingArrow = false;
let repeatTimeout: ReturnType<typeof setTimeout> | undefined = undefined; let repeatTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
// Stays in sync with a binding to the actual input range slider element. // 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. // 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. // 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. // Keeps track of the state of the slider drag as the user transitions through steps of the input process.
// - "Ready": no interaction is happening. // - "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. // - "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. // - "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. // - "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. // 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; let initialValueBeforeDragging: number | undefined = undefined;
// Stores the total value change during the process of dragging the slider. Set to 0 when not dragging. // 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. // Track whether the Ctrl key is currently held down.
let ctrlKeyDown = false; 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. // Keep track of the Ctrl key being held down.
const trackCtrl = (e: KeyboardEvent | MouseEvent) => (ctrlKeyDown = e.ctrlKey); const trackCtrl = (e: KeyboardEvent | MouseEvent) => (ctrlKeyDown = e.ctrlKey);
@ -99,12 +115,16 @@
addEventListener("keydown", trackCtrl); addEventListener("keydown", trackCtrl);
addEventListener("keyup", trackCtrl); addEventListener("keyup", trackCtrl);
addEventListener("mousemove", trackCtrl); addEventListener("mousemove", trackCtrl);
return () => {
removeEventListener("keydown", trackCtrl);
removeEventListener("keyup", trackCtrl);
removeEventListener("mousemove", trackCtrl);
}
}); });
onDestroy(() => { // onDestroy(() => {
removeEventListener("keydown", trackCtrl);
removeEventListener("keyup", trackCtrl); // });
removeEventListener("mousemove", trackCtrl);
});
// =============================== // ===============================
// TRACKING AND UPDATING THE VALUE // TRACKING AND UPDATING THE VALUE
@ -152,7 +172,7 @@
text = displayText(newValueValidated, unit); 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 // For any caller that needs to know what the value was changed to, we return it here
return newValueValidated; return newValueValidated;
@ -212,7 +232,7 @@
if (newValue !== undefined) { if (newValue !== undefined) {
const oldValue = value !== undefined && isInteger ? Math.round(value) : value; const oldValue = value !== undefined && isInteger ? Math.round(value) : value;
if (newValue !== oldValue) dispatch("startHistoryTransaction"); if (newValue !== oldValue) onstartHistoryTransaction?.();
} }
updateValue(newValue); updateValue(newValue);
@ -541,7 +561,7 @@
function startDragging() { 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: // 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> // <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. // 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("pointermove", sliderAbortFromDragging);
removeEventListener("keydown", 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> </script>
<FieldInput <FieldInput
@ -640,12 +669,11 @@
increment: mode === "Increment", increment: mode === "Increment",
range: mode === "Range", range: mode === "Range",
}} }}
value={text} bind:value={text}
on:value={({ detail }) => (text = detail)} onfocus={onTextFocused}
on:textFocused={onTextFocused} onchange={onTextChanged}
on:textChanged={onTextChanged} ontextChangeCanceled={onTextChangeCanceled}
on:textChangeCanceled={onTextChangeCanceled} onpointerdown={onDragPointerDown}
on:pointerdown={onDragPointerDown}
{label} {label}
{disabled} {disabled}
{tooltip} {tooltip}
@ -658,18 +686,18 @@
{#if mode === "Increment" && incrementBehavior !== "None"} {#if mode === "Increment" && incrementBehavior !== "None"}
<button <button
class="arrow left" class="arrow left"
on:pointerdown={(e) => onIncrementPointerDown(e, "Decrease")} onpointerdown={(e) => onIncrementPointerDown(e, "Decrease")}
on:mousedown={incrementPressAbort} onmousedown={incrementPressAbort}
on:pointerup={onIncrementPointerUp} onpointerup={onIncrementPointerUp}
on:pointerleave={onIncrementPointerUp} onpointerleave={onIncrementPointerUp}
tabindex="-1" tabindex="-1"
></button> ></button>
<button <button
class="arrow right" class="arrow right"
on:pointerdown={(e) => onIncrementPointerDown(e, "Increase")} onpointerdown={(e) => onIncrementPointerDown(e, "Increase")}
on:mousedown={incrementPressAbort} onmousedown={incrementPressAbort}
on:pointerup={onIncrementPointerUp} onpointerup={onIncrementPointerUp}
on:pointerleave={onIncrementPointerUp} onpointerleave={onIncrementPointerUp}
tabindex="-1" tabindex="-1"
></button> ></button>
{/if} {/if}
@ -684,16 +712,16 @@
max={rangeMax} max={rangeMax}
step={sliderStepValue} step={sliderStepValue}
bind:value={rangeSliderValue} bind:value={rangeSliderValue}
on:input={onSliderInput} oninput={onSliderInput}
on:pointerup={onSliderPointerUp} onpointerup={onSliderPointerUp}
on:contextmenu|preventDefault oncontextmenu={preventDefault(bubble('contextmenu'))}
on:wheel={(e) => /* Stops slider eating the scroll event in Firefox */ e.target instanceof HTMLInputElement && e.target.blur()} onwheel={(e) => /* Stops slider eating the scroll event in Firefox */ e.target instanceof HTMLInputElement && e.target.blur()}
bind:this={inputRangeElement} bind:this={inputRangeElement}
/> />
{#if rangeSliderClickDragState === "Deciding"} {#if rangeSliderClickDragState === "Deciding"}
<div class="fake-slider-thumb" /> <div class="fake-slider-thumb"></div>
{/if} {/if}
<div class="slider-progress" /> <div class="slider-progress"></div>
{/if} {/if}
{/if} {/if}
</FieldInput> </FieldInput>

View file

@ -1,24 +1,31 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte";
import { type RadioEntries, type RadioEntryData } from "@graphite/messages"; import { type RadioEntries, type RadioEntryData } from "@graphite/messages";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte"; import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
import TextLabel from "@graphite/components/widgets/labels/TextLabel.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; let {
export let selectedIndex: number | undefined = undefined; entries,
export let disabled = false; selectedIndex = undefined,
export let minWidth = 0; disabled = false,
minWidth = 0,
onselect
}: Props = $props();
$: mixed = selectedIndex === undefined && !disabled; let mixed = $derived(selectedIndex === undefined && !disabled);
function handleEntryClick(radioEntryData: RadioEntryData) { function handleEntryClick(radioEntryData: RadioEntryData) {
const index = entries.indexOf(radioEntryData); const index = entries.indexOf(radioEntryData);
dispatch("selectedIndex", index); onselect?.(index);
radioEntryData.action?.(); radioEntryData.action?.();
} }
@ -26,7 +33,7 @@
<LayoutRow class="radio-input" classes={{ disabled, mixed }} styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }}> <LayoutRow class="radio-input" classes={{ disabled, mixed }} styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }}>
{#each entries as entry, index} {#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} {#if entry.icon}
<IconLabel icon={entry.icon} /> <IconLabel icon={entry.icon} />
{/if} {/if}

View file

@ -1,28 +1,31 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte";
import type { ReferencePoint } from "@graphite/messages"; import type { ReferencePoint } from "@graphite/messages";
const dispatch = createEventDispatcher<{ value: ReferencePoint }>(); interface Props {
value: string;
disabled?: boolean;
onvalue?: (point: ReferencePoint) => void;
}
export let value: string; let { value, disabled = false, onvalue }: Props = $props();
export let disabled = false;
function setValue(newValue: ReferencePoint) { function setValue(event: MouseEvent) {
dispatch("value", newValue); let element = event.target as HTMLElement;
let position = element.dataset.position as ReferencePoint;
onvalue?.(position);
} }
</script> </script>
<div class="reference-point-input" class:disabled> <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 onclick={setValue} data-position="TopLeft" class="row-1 col-1" class:active={value === "TopLeft"} tabindex="-1" {disabled}><div></div></button>
<button on:click={() => setValue("TopCenter")} class="row-1 col-2" class:active={value === "TopCenter"} tabindex="-1" {disabled}><div /></button> <button onclick={setValue} data-position="TopCenter" class="row-1 col-2" class:active={value === "TopCenter"} tabindex="-1" {disabled}><div></div></button>
<button on:click={() => setValue("TopRight")} class="row-1 col-3" class:active={value === "TopRight"} tabindex="-1" {disabled}><div /></button> <button onclick={setValue} data-position="TopRight" class="row-1 col-3" class:active={value === "TopRight"} tabindex="-1" {disabled}><div></div></button>
<button on:click={() => setValue("CenterLeft")} class="row-2 col-1" class:active={value === "CenterLeft"} tabindex="-1" {disabled}><div /></button> <button onclick={setValue} data-position="CenterLeft" class="row-2 col-1" class:active={value === "CenterLeft"} tabindex="-1" {disabled}><div></div></button>
<button on:click={() => setValue("Center")} class="row-2 col-2" class:active={value === "Center"} tabindex="-1" {disabled}><div /></button> <button onclick={setValue} data-position="Center" class="row-2 col-2" class:active={value === "Center"} tabindex="-1" {disabled}><div></div></button>
<button on:click={() => setValue("CenterRight")} class="row-2 col-3" class:active={value === "CenterRight"} tabindex="-1" {disabled}><div /></button> <button onclick={setValue} data-position="CenterRight" class="row-2 col-3" class:active={value === "CenterRight"} tabindex="-1" {disabled}><div></div></button>
<button on:click={() => setValue("BottomLeft")} class="row-3 col-1" class:active={value === "BottomLeft"} tabindex="-1" {disabled}><div /></button> <button onclick={setValue} data-position="BottomLeft" class="row-3 col-1" class:active={value === "BottomLeft"} tabindex="-1" {disabled}><div></div></button>
<button on:click={() => setValue("BottomCenter")} class="row-3 col-2" class:active={value === "BottomCenter"} tabindex="-1" {disabled}><div /></button> <button onclick={setValue} data-position="BottomCenter" class="row-3 col-2" class:active={value === "BottomCenter"} tabindex="-1" {disabled}><div></div></button>
<button on:click={() => setValue("BottomRight")} class="row-3 col-3" class:active={value === "BottomRight"} tabindex="-1" {disabled}><div /></button> <button onclick={setValue} data-position="BottomRight" class="row-3 col-3" class:active={value === "BottomRight"} tabindex="-1" {disabled}><div></div></button>
</div> </div>
<style lang="scss" global> <style lang="scss" global>

View file

@ -1,7 +1,3 @@
<script lang="ts" context="module">
export type RulerDirection = "Horizontal" | "Vertical";
</script>
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
@ -10,21 +6,30 @@
const MINOR_MARK_THICKNESS = 6; const MINOR_MARK_THICKNESS = 6;
const MICRO_MARK_THICKNESS = 3; const MICRO_MARK_THICKNESS = 3;
export let direction: RulerDirection = "Vertical"; interface Props {
export let origin: number; direction?: Graphite.Axis;
export let numberInterval: number; origin: number;
export let majorMarkSpacing: number; numberInterval: number;
export let minorDivisions = 5; majorMarkSpacing: number;
export let microDivisions = 2; minorDivisions?: number;
microDivisions?: number;
}
let rulerInput: HTMLDivElement | undefined; let {
let rulerLength = 0; direction = "Vertical",
let svgBounds = { width: "0px", height: "0px" }; origin,
numberInterval,
majorMarkSpacing,
minorDivisions = 5,
microDivisions = 2
}: Props = $props();
$: svgPath = computeSvgPath(direction, origin, majorMarkSpacing, minorDivisions, microDivisions, rulerLength); let rulerInput: HTMLDivElement | undefined = $state();
$: svgTexts = computeSvgTexts(direction, origin, majorMarkSpacing, numberInterval, rulerLength); 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 isVertical = direction === "Vertical";
const lineDirection = isVertical ? "H" : "V"; const lineDirection = isVertical ? "H" : "V";
@ -51,7 +56,7 @@
return dPathAttribute; 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 isVertical = direction === "Vertical";
const offsetStart = mod(origin, majorMarkSpacing); const offsetStart = mod(origin, majorMarkSpacing);
@ -102,6 +107,8 @@
} }
onMount(resize); onMount(resize);
let svgPath = $derived(computeSvgPath(direction, origin, majorMarkSpacing, minorDivisions, microDivisions, rulerLength));
let svgTexts = $derived(computeSvgTexts(direction, origin, majorMarkSpacing, numberInterval, rulerLength));
</script> </script>
<div class={`ruler-input ${direction.toLowerCase()}`} bind:this={rulerInput}> <div class={`ruler-input ${direction.toLowerCase()}`} bind:this={rulerInput}>

View file

@ -1,10 +1,4 @@
<script lang="ts" context="module">
export type ScrollbarDirection = "Horizontal" | "Vertical";
</script>
<script lang="ts"> <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"; 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; const ARROW_CLICK_DISTANCE = 0.05;
@ -19,26 +13,38 @@
const clamp01 = (value: number): number => Math.min(Math.max(value, 0), 1); 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"; let {
export let thumbPosition = 0.5; direction = "Vertical",
export let thumbLength = 0.5; thumbPosition = 0.5,
thumbLength = 0.5,
ontrackShift,
onthumbPosition,
onthumbDragEnd,
onthumbDragStart,
onthumbDragAbort,
}: Props = $props();
let scrollTrack: HTMLDivElement | undefined; let scrollTrack: HTMLDivElement | undefined = $state();
let dragging = false; let dragging = $state(false);
let pressingTrack = false; let pressingTrack = false;
let pressingArrow = false; let pressingArrow = false;
let repeatTimeout: ReturnType<typeof setTimeout> | undefined = undefined; let repeatTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
let pointerPositionLastFrame = 0; 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; let start = $derived(thumbToTrack(thumbLength, thumbPosition) - thumbLength / 2);
$: end = 1 - thumbToTrack(thumbLength, thumbPosition) - thumbLength / 2; let end = $derived(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 [thumbTop, thumbBottom, thumbLeft, thumbRight] = $derived(direction === "Vertical" ? [`${start * 100}%`, `${end * 100}%`, "0%", "0%"] : ["0%", "0%", `${start * 100}%`, `${end * 100}%`]);
function trackLength(): number | undefined { function trackLength(): number | undefined {
if (scrollTrack === undefined) return undefined; if (scrollTrack === undefined) return undefined;
@ -54,7 +60,7 @@
if (dragging) return; if (dragging) return;
dragging = true; dragging = true;
dispatch("thumbDragStart"); onthumbDragStart?.();
pointerPositionLastFrame = pointerPosition(e); pointerPositionLastFrame = pointerPosition(e);
addEvents(); addEvents();
@ -65,14 +71,14 @@
if (!pressingArrow) return; if (!pressingArrow) return;
const distance = afterInitialDelay ? ARROW_REPEAT_DISTANCE : ARROW_CLICK_DISTANCE; 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); if (afterInitialDelay) repeatTimeout = setTimeout(sendMove, PRESS_REPEAT_INTERVAL_RAPID_MS);
afterInitialDelay = true; afterInitialDelay = true;
}; };
pressingArrow = true; pressingArrow = true;
dispatch("thumbDragStart"); onthumbDragStart?.();
let afterInitialDelay = false; let afterInitialDelay = false;
sendMove(); sendMove();
repeatTimeout = setTimeout(sendMove, PRESS_REPEAT_DELAY_MS); repeatTimeout = setTimeout(sendMove, PRESS_REPEAT_DELAY_MS);
@ -108,13 +114,13 @@
} }
const move = newPointer - oldPointer < 0 ? 1 : -1; const move = newPointer - oldPointer < 0 ? 1 : -1;
dispatch("trackShift", move); ontrackShift?.(move);
if (afterInitialDelay) repeatTimeout = setTimeout(sendMove, PRESS_REPEAT_INTERVAL_MS); if (afterInitialDelay) repeatTimeout = setTimeout(sendMove, PRESS_REPEAT_INTERVAL_MS);
afterInitialDelay = true; afterInitialDelay = true;
}; };
dispatch("thumbDragStart"); onthumbDragStart?.();
pressingTrack = true; pressingTrack = true;
let afterInitialDelay = false; let afterInitialDelay = false;
sendMove(); sendMove();
@ -128,17 +134,17 @@
pressingTrack = false; pressingTrack = false;
pressingArrow = false; pressingArrow = false;
clearTimeout(repeatTimeout); clearTimeout(repeatTimeout);
dispatch("thumbDragAbort"); onthumbDragAbort?.();
} }
if (dragging) { if (dragging) {
dragging = false; dragging = false;
dispatch("thumbDragAbort"); onthumbDragAbort?.();
} }
} }
function onPointerUp() { function onPointerUp() {
if (dragging) dispatch("thumbDragEnd"); if (dragging) onthumbDragEnd?.();
dragging = false; dragging = false;
pressingTrack = false; pressingTrack = false;
@ -172,7 +178,7 @@
const dragDelta = positionPositionThisFrame - pointerPositionLastFrame; const dragDelta = positionPositionThisFrame - pointerPositionLastFrame;
const movement = dragDelta / (length * (1 - thumbLength)); const movement = dragDelta / (length * (1 - thumbLength));
const newThumbPosition = clamp01(thumbPosition + movement); const newThumbPosition = clamp01(thumbPosition + movement);
dispatch("thumbPosition", newThumbPosition); onthumbPosition?.(newThumbPosition)
pointerPositionLastFrame = positionPositionThisFrame; pointerPositionLastFrame = positionPositionThisFrame;
@ -207,11 +213,11 @@
</script> </script>
<div class={`scrollbar-input ${direction.toLowerCase()}`}> <div class={`scrollbar-input ${direction.toLowerCase()}`}>
<button class="arrow decrease" on:pointerdown={() => pressArrow(-1)} tabindex="-1" data-scrollbar-arrow></button> <button class="arrow decrease" onpointerdown={() => pressArrow(-1)} tabindex="-1" data-scrollbar-arrow></button>
<div class="scroll-track" on:pointerdown={pressTrack} bind:this={scrollTrack}> <div class="scroll-track" onpointerdown={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} /> <div class="scroll-thumb" onpointerdown={dragThumb} class:dragging style:top={thumbTop} style:bottom={thumbBottom} style:left={thumbLeft} style:right={thumbRight}></div>
</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> </div>
<style lang="scss" global> <style lang="scss" global>

View file

@ -1,5 +1,5 @@
<script lang="ts"> <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";
@ -10,22 +10,33 @@
const BUTTON_LEFT = 0; const BUTTON_LEFT = 0;
const BUTTON_RIGHT = 2; 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; let {
export let activeMarkerIndex = 0 as number | undefined; activeMarkerIndex = 0,
drag = $bindable(),
gradient,
ongradient,
onactiveMarkerIndexChange,
}: Props = $props();
// export let disabled = false; // export let disabled = false;
// export let tooltip: string | undefined = undefined; // export let tooltip: string | undefined = undefined;
let markerTrack: LayoutRow | undefined = undefined; let markerTrack: LayoutRow | undefined = $state(undefined);
let positionRestore: number | undefined = undefined; let positionRestore: number | undefined = undefined;
let deletionRestore: boolean | undefined = undefined; let deletionRestore: boolean | undefined = undefined;
function markerPointerDown(e: PointerEvent, index: number) { function markerPointerDown(e: PointerEvent, index: number) {
// Left-click to select and begin potentially dragging // Left-click to select and begin potentially dragging
if (e.button === BUTTON_LEFT) { if (e.button === BUTTON_LEFT) {
activeMarkerIndex = index; // activeMarkerIndex = index;
dispatch("activeMarkerIndexChange", index); onactiveMarkerIndexChange?.(index);
addEvents(); addEvents();
return; return;
} }
@ -69,11 +80,9 @@
if (index === -1) index = gradient.stops.length; if (index === -1) index = gradient.stops.length;
gradient.stops.splice(index, 0, { position, color }); gradient.stops.splice(index, 0, { position, color });
activeMarkerIndex = index;
deletionRestore = true; deletionRestore = true;
onactiveMarkerIndexChange?.(index);
dispatch("activeMarkerIndexChange", index); ongradient?.(gradient)
dispatch("gradient", gradient);
addEvents(); addEvents();
} }
@ -91,15 +100,10 @@
if (gradient.stops.length <= 2) return; if (gradient.stops.length <= 2) return;
gradient.stops.splice(index, 1); gradient.stops.splice(index, 1);
if (gradient.stops.length === 0) { let newMarkerIndex = gradient.stops.length === 0 ? undefined : Math.max(0, Math.min(gradient.stops.length - 1, index));
activeMarkerIndex = undefined;
} else {
activeMarkerIndex = Math.max(0, Math.min(gradient.stops.length - 1, index));
}
deletionRestore = undefined; deletionRestore = undefined;
onactiveMarkerIndexChange?.(newMarkerIndex);
dispatch("activeMarkerIndexChange", activeMarkerIndex); ongradient?.(gradient);
dispatch("gradient", gradient);
} }
function moveMarker(e: PointerEvent, index: number) { function moveMarker(e: PointerEvent, index: number) {
@ -113,7 +117,7 @@
if (deletionRestore === undefined) { if (deletionRestore === undefined) {
deletionRestore = false; deletionRestore = false;
dispatch("dragging", true); drag = true;
} }
setPosition(index, position); setPosition(index, position);
@ -124,10 +128,9 @@
active.position = position; active.position = position;
gradient.stops.sort((a, b) => a.position - b.position); gradient.stops.sort((a, b) => a.position - b.position);
if (gradient.stops.indexOf(active) !== activeMarkerIndex) { if (gradient.stops.indexOf(active) !== activeMarkerIndex) {
activeMarkerIndex = gradient.stops.indexOf(active); onactiveMarkerIndexChange?.(gradient.stops.indexOf(active));
dispatch("activeMarkerIndexChange", gradient.stops.indexOf(active));
} }
dispatch("gradient", gradient); ongradient?.(gradient);
} }
function abortDrag() { function abortDrag() {
@ -148,7 +151,7 @@
positionRestore = undefined; positionRestore = undefined;
deletionRestore = undefined; deletionRestore = undefined;
dispatch("dragging", false); drag = false;
} }
function onPointerMove(e: PointerEvent) { function onPointerMove(e: PointerEvent) {
@ -187,11 +190,14 @@
document.removeEventListener("keydown", onKeyDown); document.removeEventListener("keydown", onKeyDown);
} }
document.addEventListener("keydown", deleteStop); onMount(() => {
onDestroy(() => { document.addEventListener("keydown", deleteStop);
removeEvents();
document.removeEventListener("keydown", deleteStop); return () => {
}); removeEvents();
document.removeEventListener("keydown", deleteStop);
}
})
// # Backend -> Frontend // # Backend -> Frontend
// Populate(gradient, { position, color }[], active) // The only way indexes get changed. Frontend drops marker if it's being dragged. // 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(), "--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}> <LayoutRow class="marker-track" bind:this={markerTrack}>
{#each gradient.stops as marker, index} {#each gradient.stops as marker, index}
<svg <svg
@ -226,7 +232,7 @@
class:active={index === activeMarkerIndex} class:active={index === activeMarkerIndex}
style:--marker-position={marker.position} style:--marker-position={marker.position}
style:--marker-color={marker.color.toRgbCSS()} style:--marker-color={marker.color.toRgbCSS()}
on:pointerdown={(e) => markerPointerDown(e, index)} onpointerdown={(e) => markerPointerDown(e, index)}
data-gradient-marker data-gradient-marker
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 12 12" viewBox="0 0 12 12"

View file

@ -1,16 +1,23 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte";
import FieldInput from "@graphite/components/widgets/inputs/FieldInput.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; let {
export let label: string | undefined = undefined; value = $bindable(),
export let tooltip: string | undefined = undefined; label = undefined,
export let disabled = false; tooltip = undefined,
disabled = false,
oncommitText
}: Props = $props();
let self: FieldInput | undefined; let self: FieldInput | undefined = $state();
let editing = false; let editing = false;
function onTextFocused() { function onTextFocused() {
@ -26,7 +33,7 @@
onTextChangeCanceled(); onTextChangeCanceled();
// TODO: Find a less hacky way to do this // 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 // Required if value is not changed by the parent component upon update:value event
self?.setInputElementValue(self.getValue()); self?.setInputElementValue(self.getValue());
@ -46,11 +53,10 @@
<FieldInput <FieldInput
class="text-area-input" class="text-area-input"
classes={{ "has-label": Boolean(label) }} classes={{ "has-label": Boolean(label) }}
{value} bind:value
on:value onfocus={onTextFocused}
on:textFocused={onTextFocused} onchange={onTextChanged}
on:textChanged={onTextChanged} ontextChangeCanceled={onTextChangeCanceled}
on:textChangeCanceled={onTextChangeCanceled}
textarea={true} textarea={true}
spellcheck={true} spellcheck={true}
{label} {label}

View file

@ -1,27 +1,37 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from "svelte";
import FieldInput from "@graphite/components/widgets/inputs/FieldInput.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 let {
export let label: string | undefined = undefined; label = undefined,
export let tooltip: string | undefined = undefined; tooltip = undefined,
export let placeholder: string | undefined = undefined; placeholder = undefined,
// Disabled disabled = false,
export let disabled = false; value = $bindable(),
// Value centered = false,
export let value: string; minWidth = 0,
// Styling class: className = "",
export let centered = false; classes = {},
export let minWidth = 0; oncommitText,
}: Props = $props();
let className = ""; let self: FieldInput | undefined = $state();
export { className as class };
export let classes: Record<string, boolean> = {};
let self: FieldInput | undefined;
let editing = false; let editing = false;
function onTextFocused() { function onTextFocused() {
@ -39,7 +49,7 @@
onTextChangeCanceled(); onTextChangeCanceled();
// TODO: Find a less hacky way to do this // 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 // Required if value is not changed by the parent component upon update:value event
self?.setInputElementValue(self.getValue()); self?.setInputElementValue(self.getValue());
@ -64,11 +74,10 @@
class={`text-input ${className}`.trim()} class={`text-input ${className}`.trim()}
classes={{ centered, ...classes }} classes={{ centered, ...classes }}
styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }} styles={{ ...(minWidth > 0 ? { "min-width": `${minWidth}px` } : {}) }}
{value} bind:value
on:value onfocus={onTextFocused}
on:textFocused={onTextFocused} onchange={onTextChanged}
on:textChanged={onTextChanged} ontextChangeCanceled={onTextChangeCanceled}
on:textChangeCanceled={onTextChangeCanceled}
spellcheck={true} spellcheck={true}
{label} {label}
{disabled} {disabled}

View file

@ -10,11 +10,15 @@
const editor = getContext<Editor>("editor"); const editor = getContext<Editor>("editor");
export let primary: Color; interface Props {
export let secondary: Color; primary: Color;
secondary: Color;
}
let primaryOpen = false; let { primary, secondary }: Props = $props();
let secondaryOpen = false;
let primaryOpen = $state(false);
let secondaryOpen = $state(false);
function clickPrimarySwatch() { function clickPrimarySwatch() {
primaryOpen = true; primaryOpen = true;
@ -37,22 +41,20 @@
<LayoutCol class="working-colors-button"> <LayoutCol class="working-colors-button">
<LayoutRow class="primary swatch"> <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 <ColorPicker
open={primaryOpen} bind:open={primaryOpen}
on:open={({ detail }) => (primaryOpen = detail)}
colorOrGradient={primary} colorOrGradient={primary}
on:colorOrGradient={({ detail }) => detail instanceof Color && primaryColorChanged(detail)} oncolorOrGradient={(detail) => detail instanceof Color && primaryColorChanged(detail)}
direction="Right" direction="Right"
/> />
</LayoutRow> </LayoutRow>
<LayoutRow class="secondary swatch"> <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 <ColorPicker
open={secondaryOpen} bind:open={secondaryOpen}
on:open={({ detail }) => (secondaryOpen = detail)}
colorOrGradient={secondary} colorOrGradient={secondary}
on:colorOrGradient={({ detail }) => detail instanceof Color && secondaryColorChanged(detail)} oncolorOrGradient={(detail) => detail instanceof Color && secondaryColorChanged(detail)}
direction="Right" direction="Right"
/> />
</LayoutRow> </LayoutRow>

View file

@ -3,15 +3,25 @@
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte"; import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
let className = ""; interface Props {
export { className as class }; class?: string;
export let classes: Record<string, boolean> = {}; classes?: Record<string, boolean>;
export let icon: IconName; icon: IconName;
export let iconSizeOverride: number | undefined = undefined; iconSizeOverride?: number | undefined;
export let disabled = false; disabled?: boolean;
export let tooltip: string | undefined = undefined; 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]; const iconData = ICONS[icon];
if (!iconData) { if (!iconData) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -20,10 +30,10 @@
} }
if (iconData.size === undefined) return ""; if (iconData.size === undefined) return "";
return `size-${iconSizeOverride || iconData.size}`; return `size-${iconSizeOverride || iconData.size}`;
})(icon); })(icon));
$: extraClasses = Object.entries(classes) let extraClasses = $derived(Object.entries(classes)
.flatMap(([className, stateName]) => (stateName ? [className] : [])) .flatMap(([className, stateName]) => (stateName ? [className] : []))
.join(" "); .join(" "));
</script> </script>
<LayoutRow class={`icon-label ${iconSizeClass} ${className} ${extraClasses}`.trim()} classes={{ disabled }} {tooltip}> <LayoutRow class={`icon-label ${iconSizeClass} ${className} ${extraClasses}`.trim()} classes={{ disabled }} {tooltip}>

View file

@ -1,13 +1,17 @@
<script lang="ts"> <script lang="ts">
import { type SeparatorDirection, type SeparatorType } from "@graphite/messages"; import { type SeparatorDirection, type SeparatorType } from "@graphite/messages";
export let direction: SeparatorDirection = "Horizontal"; interface Props {
export let type: SeparatorType = "Unrelated"; direction?: SeparatorDirection;
type?: SeparatorType;
}
let { direction = "Horizontal", type = "Unrelated" }: Props = $props();
</script> </script>
<div class={`separator ${direction.toLowerCase()} ${type.toLowerCase()}`}> <div class={`separator ${direction.toLowerCase()} ${type.toLowerCase()}`}>
{#if type === "Section"} {#if type === "Section"}
<div /> <div></div>
{/if} {/if}
</div> </div>

View file

@ -1,26 +1,48 @@
<script lang="ts"> <script lang="ts">
let className = ""; import type { SvelteHTMLElements } from 'svelte/elements';
export { className as class };
export let classes: Record<string, boolean> = {}; type LabelHTMLElementProps = SvelteHTMLElements["label"];
let styleName = "";
export { styleName as style }; interface Props extends LabelHTMLElementProps {
export let styles: Record<string, string | number | undefined> = {}; class?: string;
export let disabled = false; classes?: Record<string, boolean>;
export let bold = false; style?: string;
export let italic = false; styles?: Record<string, string | number | undefined>;
export let centerAlign = false; disabled?: boolean;
export let tableAlign = false; bold?: boolean;
export let minWidth = 0; italic?: boolean;
export let multiline = false; centerAlign?: boolean;
export let tooltip: string | undefined = undefined; tableAlign?: boolean;
export let checkboxId: bigint | undefined = undefined; 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] : [])) .flatMap(([className, stateName]) => (stateName ? [className] : []))
.join(" "); .join(" "));
$: extraStyles = Object.entries(styles) let extraStyles = $derived(Object.entries(styles)
.flatMap((styleAndValue) => (styleAndValue[1] !== undefined ? [`${styleAndValue[0]}: ${styleAndValue[1]};`] : [])) .flatMap((styleAndValue) => (styleAndValue[1] !== undefined ? [`${styleAndValue[0]}: ${styleAndValue[1]};`] : []))
.join(" "); .join(" "));
</script> </script>
<label <label
@ -36,7 +58,7 @@
title={tooltip} title={tooltip}
for={checkboxId !== undefined ? `checkbox-input-${checkboxId}` : undefined} for={checkboxId !== undefined ? `checkbox-input-${checkboxId}` : undefined}
> >
<slot /> {@render children?.()}
</label> </label>
<style lang="scss" global> <style lang="scss" global>

View file

@ -34,14 +34,21 @@
const fullscreen = getContext<FullscreenState>("fullscreen"); const fullscreen = getContext<FullscreenState>("fullscreen");
export let keysWithLabelsGroups: LayoutKeysGroup[] = []; interface Props {
export let mouseMotion: MouseMotion | undefined = undefined; keysWithLabelsGroups?: LayoutKeysGroup[];
export let requiresLock = false; mouseMotion?: MouseMotion | undefined;
export let textOnly = false; requiresLock?: boolean;
textOnly?: boolean;
children?: import('svelte').Snippet;
}
$: keyboardLockInfoMessage = watchKeyboardLockInfoMessage(fullscreen.keyboardLockApiSupported); let {
keysWithLabelsGroups = [],
$: displayKeyboardLockNotice = requiresLock && !$fullscreen.keyboardLocked; mouseMotion = undefined,
requiresLock = false,
textOnly = false,
children
}: Props = $props();
function watchKeyboardLockInfoMessage(keyboardLockApiSupported: boolean): string { function watchKeyboardLockInfoMessage(keyboardLockApiSupported: boolean): string {
const RESERVED = "This hotkey is reserved by the browser. "; const RESERVED = "This hotkey is reserved by the browser. ";
@ -114,6 +121,8 @@
return undefined; return undefined;
} }
} }
let keyboardLockInfoMessage = $derived(watchKeyboardLockInfoMessage(fullscreen.keyboardLockApiSupported));
let displayKeyboardLockNotice = $derived(requiresLock && !$fullscreen.keyboardLocked);
</script> </script>
{#if displayKeyboardLockNotice} {#if displayKeyboardLockNotice}
@ -139,9 +148,9 @@
<IconLabel icon={mouseHintIcon(mouseMotion)} /> <IconLabel icon={mouseHintIcon(mouseMotion)} />
</div> </div>
{/if} {/if}
{#if $$slots.default} {#if children}
<div class="hint-text"> <div class="hint-text">
<slot /> {@render children?.()}
</div> </div>
{/if} {/if}
</LayoutRow> </LayoutRow>

View file

@ -1,7 +1,3 @@
<script lang="ts" context="module">
export type Platform = "Windows" | "Mac" | "Linux" | "Web";
</script>
<script lang="ts"> <script lang="ts">
import { getContext, onMount } from "svelte"; import { getContext, onMount } from "svelte";
@ -17,7 +13,7 @@
import WindowButtonsWindows from "@graphite/components/window/title-bar/WindowButtonsWindows.svelte"; import WindowButtonsWindows from "@graphite/components/window/title-bar/WindowButtonsWindows.svelte";
import WindowTitle from "@graphite/components/window/title-bar/WindowTitle.svelte"; import WindowTitle from "@graphite/components/window/title-bar/WindowTitle.svelte";
export let platform: Platform; export let platform: Graphite.Platform;
export let maximized: boolean; export let maximized: boolean;
const editor = getContext<Editor>("editor"); const editor = getContext<Editor>("editor");

View file

@ -1,4 +1,4 @@
<script lang="ts" context="module"> <script lang="ts" module>
import Document from "@graphite/components/panels/Document.svelte"; import Document from "@graphite/components/panels/Document.svelte";
import Layers from "@graphite/components/panels/Layers.svelte"; import Layers from "@graphite/components/panels/Layers.svelte";
import Properties from "@graphite/components/panels/Properties.svelte"; import Properties from "@graphite/components/panels/Properties.svelte";
@ -34,15 +34,27 @@
const editor = getContext<Editor>("editor"); const editor = getContext<Editor>("editor");
export let tabMinWidths = false; interface Props {
export let tabCloseButtons = false; tabMinWidths?: boolean;
export let tabLabels: { name: string; tooltip?: string }[]; tabCloseButtons?: boolean;
export let tabActiveIndex: number; tabLabels: { name: string; tooltip?: string }[];
export let panelType: PanelType | undefined = undefined; tabActiveIndex: number;
export let clickAction: ((index: number) => void) | undefined = undefined; panelType?: PanelType | undefined;
export let closeAction: ((index: number) => void) | undefined = 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 { function platformModifiers(reservedKey: boolean): LayoutKeysGroup {
// TODO: Remove this by properly feeding these keys from a layout provided by the backend // TODO: Remove this by properly feeding these keys from a layout provided by the backend
@ -90,7 +102,7 @@
} }
</script> </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-bar" classes={{ "min-widths": tabMinWidths }}>
<LayoutRow class="tab-group" scrollableX={true}> <LayoutRow class="tab-group" scrollableX={true}>
{#each tabLabels as tabLabel, tabIndex} {#each tabLabels as tabLabel, tabIndex}
@ -98,18 +110,18 @@
class="tab" class="tab"
classes={{ active: tabIndex === tabActiveIndex }} classes={{ active: tabIndex === tabActiveIndex }}
tooltip={tabLabel.tooltip || undefined} tooltip={tabLabel.tooltip || undefined}
on:click={(e) => { onclick={(e) => {
e.stopPropagation(); e.stopPropagation();
clickAction?.(tabIndex); clickAction?.(tabIndex);
}} }}
on:auxclick={(e) => { onauxclick={(e) => {
// Middle mouse button click // Middle mouse button click
if (e.button === BUTTON_MIDDLE) { if (e.button === BUTTON_MIDDLE) {
e.stopPropagation(); e.stopPropagation();
closeAction?.(tabIndex); closeAction?.(tabIndex);
} }
}} }}
on:mouseup={(e) => { onmouseup={(e) => {
// Middle mouse button click fallback for Safari: // Middle mouse button click fallback for Safari:
// https://developer.mozilla.org/en-US/docs/Web/API/Element/auxclick_event#browser_compatibility // 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. // 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> <TextLabel>{tabLabel.name}</TextLabel>
{#if tabCloseButtons} {#if tabCloseButtons}
<IconButton <IconButton
action={(e) => { onclick={(e) => {
e?.stopPropagation(); e?.stopPropagation();
closeAction?.(tabIndex); closeAction?.(tabIndex);
}} }}
@ -142,41 +154,44 @@
</LayoutRow> </LayoutRow>
<LayoutCol class="panel-body"> <LayoutCol class="panel-body">
{#if panelType} {#if panelType}
<svelte:component this={PANEL_COMPONENTS[panelType]} /> {@const SvelteComponent = PANEL_COMPONENTS[panelType]}
<SvelteComponent />
{:else} {: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"> <LayoutCol class="content">
<LayoutRow class="logotype"> <LayoutRow class="logotype">
<IconLabel icon="GraphiteLogotypeSolid" /> <IconLabel icon="GraphiteLogotypeSolid" />
</LayoutRow> </LayoutRow>
<LayoutRow class="actions"> <LayoutRow class="actions">
<table> <table>
<tr> <tbody>
<td> <tr>
<TextButton label="New Document" icon="File" flush={true} action={() => editor.handle.newDocumentDialog()} /> <td>
</td> <TextButton label="New Document" icon="File" flush={true} action={() => editor.handle.newDocumentDialog()} />
<td> </td>
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(true), { key: "KeyN", label: "N" }]]} /> <td>
</td> <UserInputLabel keysWithLabelsGroups={[[...platformModifiers(true), { key: "KeyN", label: "N" }]]} />
</tr> </td>
<tr> </tr>
<td> <tr>
<TextButton label="Open Document" icon="Folder" flush={true} action={() => editor.handle.openDocument()} /> <td>
</td> <TextButton label="Open Document" icon="Folder" flush={true} action={() => editor.handle.openDocument()} />
<td> </td>
<UserInputLabel keysWithLabelsGroups={[[...platformModifiers(false), { key: "KeyO", label: "O" }]]} /> <td>
</td> <UserInputLabel keysWithLabelsGroups={[[...platformModifiers(false), { key: "KeyO", label: "O" }]]} />
</tr> </td>
<tr> </tr>
<td colspan="2"> <tr>
<TextButton label="Open Demo Artwork" icon="Image" flush={true} action={() => editor.handle.demoArtworkDialog()} /> <td colspan="2">
</td> <TextButton label="Open Demo Artwork" icon="Image" flush={true} action={() => editor.handle.demoArtworkDialog()} />
</tr> </td>
<tr> </tr>
<td colspan="2"> <tr>
<TextButton label="Support the Development Fund" icon="Heart" flush={true} action={() => editor.handle.visitUrl("https://graphite.rs/donate/")} /> <td colspan="2">
</td> <TextButton label="Support the Development Fund" icon="Heart" flush={true} action={() => editor.handle.visitUrl("https://graphite.rs/donate/")} />
</tr> </td>
</tr>
</tbody>
</table> </table>
</LayoutRow> </LayoutRow>
</LayoutCol> </LayoutCol>

View file

@ -35,17 +35,21 @@ const ALLOWED_LICENSES = [
]; ];
const runesGlobs = [ const runesGlobs = [
"**/components/*.svelte" "**/components/layout/*.svelte",
"**/components/*.svelte" "**/components/widgets/labels/*.svelte",
"**/components/*.svelte" "**/components/widgets/buttons/*.svelte",
"**/components/widgets/inputs/*.svelte",
"**/components/floating-menus/MenuList.svelte",
"**/components/floating-menus/ColorPicker.svelte",
"**/components/floating-menus/Dialog.svelte",
"**/components/window/workspace/Panel.svelte",
]; ];
function forceRunes(filePath: string): boolean { function forceRunes(filePath: string): boolean {
const relativePath = filePath.slice(filePath.indexOf("src")); const relativePath = filePath.slice(filePath.indexOf("src"));
// Test the file path against each glob pattern // Test the file path against each glob pattern
return runesGlobs.some((min) => { return runesGlobs.some((min) => {
console.log("🚀 ~ forceRunes ~ filePath:", relativePath, minimatch(filePath, min)); return minimatch(relativePath, min);
return minimatch(filePath, min);
}); });
} }
@ -62,9 +66,12 @@ export default defineConfig({
defaultHandler?.(warning); defaultHandler?.(warning);
}, },
dynamicCompileOptions({ filename, compileOptions }) { dynamicCompileOptions({ filename, compileOptions }) {
console.log("🚀 ~ dynamicCompileOptions ~ compileOptions:", compileOptions.runes);
if (forceRunes(filename) && !compileOptions.runes) { if (forceRunes(filename) && !compileOptions.runes) {
console.log(`🚀 ~ runes ~`, filename, true);
return { runes: true }; return { runes: true };
} else {
console.log(`🚀 ~ runes ~`, filename, false);
} }
}, },
}), }),