mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-23 21:48:12 +08:00
Clean up generic widget handling code in the frontend (#3856)
* Clean up WidgetSpan component code to avoid the giant if-chain for choosing a widget * Improve typing support
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
import { getContext } from "svelte";
|
||||
|
||||
import type { Editor } from "@graphite/editor";
|
||||
import type { LayoutTarget, WidgetInstance, WidgetSpanColumn, WidgetSpanRow } from "@graphite/messages";
|
||||
import { narrowWidgetProps, isWidgetSpanColumn, isWidgetSpanRow } from "@graphite/messages";
|
||||
import type { LayoutTarget, WidgetInstance, WidgetPropsNames, WidgetPropsSet, WidgetTypes, WidgetSpanColumn, WidgetSpanRow } from "@graphite/messages";
|
||||
import { isWidgetSpanColumn, isWidgetSpanRow } from "@graphite/messages";
|
||||
import { debouncer } from "@graphite/utility-functions/debounce";
|
||||
|
||||
import NodeCatalog from "@graphite/components/floating-menus/NodeCatalog.svelte";
|
||||
@@ -71,134 +71,189 @@
|
||||
editor.handle.widgetValueCommitAndUpdate(layoutTarget, widgets[widgetIndex].widgetId, value, resendWidget);
|
||||
}
|
||||
|
||||
// TODO: This seems to work, but verify the correctness and terseness of this, it's adapted from https://stackoverflow.com/a/67434028/775283
|
||||
function exclude<T extends object>(props: T, additional?: (keyof T)[]): Omit<T, typeof additional extends Array<infer K> ? K : never> {
|
||||
const exclusions = ["kind", ...(additional || [])];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return Object.fromEntries(Object.entries(props).filter((entry) => !exclusions.includes(entry[0]))) as any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function exclude(props: WidgetPropsSet, additional?: string[]): Record<string, any> {
|
||||
const exclusions = new Set(["kind", ...(additional || [])]);
|
||||
return Object.fromEntries(Object.entries(props).filter(([key]) => !exclusions.has(key)));
|
||||
}
|
||||
|
||||
type WidgetConfig = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
component: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getProps(props: WidgetPropsSet, widgetIndex: number): Record<string, any> | undefined;
|
||||
getSlotContent?(props: WidgetPropsSet): string;
|
||||
};
|
||||
|
||||
const widgetRegistry: Record<WidgetPropsNames, WidgetConfig> = {
|
||||
CheckboxInput: {
|
||||
component: CheckboxInput,
|
||||
getProps: (props: WidgetTypes["CheckboxInput"], index) => ({
|
||||
...exclude(props),
|
||||
$$events: { checked: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, true) },
|
||||
}),
|
||||
},
|
||||
ColorInput: {
|
||||
component: ColorInput,
|
||||
getProps: (props: WidgetTypes["ColorInput"], index) => ({
|
||||
...exclude(props),
|
||||
$$events: {
|
||||
value: (e: CustomEvent) => widgetValueUpdate(index, e.detail, false),
|
||||
startHistoryTransaction: () => widgetValueCommit(index, props.value),
|
||||
},
|
||||
}),
|
||||
},
|
||||
CurveInput: {
|
||||
// TODO: CurvesInput is currently unused
|
||||
component: CurveInput,
|
||||
getProps: (props: WidgetTypes["CurveInput"], index) => ({
|
||||
...exclude(props),
|
||||
$$events: {
|
||||
value: (e: CustomEvent) => debouncer((value: unknown) => widgetValueCommitAndUpdate(index, value, false), { debounceTime: 120 }).debounceUpdateValue(e.detail),
|
||||
},
|
||||
}),
|
||||
},
|
||||
DropdownInput: {
|
||||
component: DropdownInput,
|
||||
getProps: (props: WidgetTypes["DropdownInput"], index) => ({
|
||||
...exclude(props),
|
||||
$$events: {
|
||||
hoverInEntry: (e: CustomEvent) => widgetValueUpdate(index, e.detail, false),
|
||||
hoverOutEntry: (e: CustomEvent) => widgetValueUpdate(index, e.detail, false),
|
||||
selectedIndex: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, true),
|
||||
},
|
||||
}),
|
||||
},
|
||||
ParameterExposeButton: {
|
||||
component: ParameterExposeButton,
|
||||
getProps: (props: WidgetTypes["ParameterExposeButton"], index) => ({
|
||||
...exclude(props),
|
||||
action: () => widgetValueCommitAndUpdate(index, undefined, true),
|
||||
}),
|
||||
},
|
||||
IconButton: {
|
||||
component: IconButton,
|
||||
getProps: (props: WidgetTypes["IconButton"], index) => ({
|
||||
...exclude(props),
|
||||
action: () => widgetValueCommitAndUpdate(index, undefined, true),
|
||||
}),
|
||||
},
|
||||
IconLabel: {
|
||||
component: IconLabel,
|
||||
getProps: (props: WidgetTypes["IconLabel"]) => exclude(props),
|
||||
},
|
||||
ShortcutLabel: {
|
||||
component: ShortcutLabel,
|
||||
getProps: (props: WidgetTypes["ShortcutLabel"]) => {
|
||||
if (!props.shortcut) return undefined;
|
||||
return exclude(props);
|
||||
},
|
||||
},
|
||||
ImageLabel: {
|
||||
component: ImageLabel,
|
||||
getProps: (props: WidgetTypes["ImageLabel"]) => exclude(props),
|
||||
},
|
||||
ImageButton: {
|
||||
component: ImageButton,
|
||||
getProps: (props: WidgetTypes["ImageButton"], index) => ({
|
||||
...exclude(props),
|
||||
action: () => widgetValueCommitAndUpdate(index, undefined, true),
|
||||
}),
|
||||
},
|
||||
NodeCatalog: {
|
||||
component: NodeCatalog,
|
||||
getProps: (props: WidgetTypes["NodeCatalog"], index) => ({
|
||||
...exclude(props),
|
||||
$$events: { selectNodeType: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, false) },
|
||||
}),
|
||||
},
|
||||
NumberInput: {
|
||||
component: NumberInput,
|
||||
getProps: (props: WidgetTypes["NumberInput"], index) => ({
|
||||
...exclude(props),
|
||||
incrementCallbackIncrease: () => widgetValueCommitAndUpdate(index, "Increment", false),
|
||||
incrementCallbackDecrease: () => widgetValueCommitAndUpdate(index, "Decrement", false),
|
||||
$$events: {
|
||||
value: (e: CustomEvent) => debouncer((value: unknown) => widgetValueUpdate(index, value, true)).debounceUpdateValue(e.detail),
|
||||
startHistoryTransaction: () => widgetValueCommit(index, props.value),
|
||||
},
|
||||
}),
|
||||
},
|
||||
ReferencePointInput: {
|
||||
component: ReferencePointInput,
|
||||
getProps: (props: WidgetTypes["ReferencePointInput"], index) => ({
|
||||
...exclude(props),
|
||||
$$events: { value: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, true) },
|
||||
}),
|
||||
},
|
||||
PopoverButton: {
|
||||
component: PopoverButton,
|
||||
getProps: (props: WidgetTypes["PopoverButton"]) => ({
|
||||
...exclude(props),
|
||||
layoutTarget,
|
||||
}),
|
||||
},
|
||||
RadioInput: {
|
||||
component: RadioInput,
|
||||
getProps: (props: WidgetTypes["RadioInput"], index) => ({
|
||||
...exclude(props),
|
||||
$$events: { selectedIndex: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, true) },
|
||||
}),
|
||||
},
|
||||
Separator: {
|
||||
component: Separator,
|
||||
getProps: (props: WidgetTypes["Separator"]) => exclude(props),
|
||||
},
|
||||
WorkingColorsInput: {
|
||||
component: WorkingColorsInput,
|
||||
getProps: (props: WidgetTypes["WorkingColorsInput"]) => exclude(props),
|
||||
},
|
||||
TextAreaInput: {
|
||||
component: TextAreaInput,
|
||||
getProps: (props: WidgetTypes["TextAreaInput"], index) => ({
|
||||
...exclude(props),
|
||||
$$events: { commitText: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, false) },
|
||||
}),
|
||||
},
|
||||
TextButton: {
|
||||
component: TextButton,
|
||||
getProps: (props: WidgetTypes["TextButton"], index) => ({
|
||||
...exclude(props),
|
||||
action: () => widgetValueCommitAndUpdate(index, [], true),
|
||||
$$events: { selectedEntryValuePath: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, false) },
|
||||
}),
|
||||
},
|
||||
BreadcrumbTrailButtons: {
|
||||
component: BreadcrumbTrailButtons,
|
||||
getProps: (props: WidgetTypes["BreadcrumbTrailButtons"], index) => ({
|
||||
...exclude(props),
|
||||
action: (breadcrumbIndex: number) => widgetValueCommitAndUpdate(index, breadcrumbIndex, true),
|
||||
}),
|
||||
},
|
||||
TextInput: {
|
||||
component: TextInput,
|
||||
getProps: (props: WidgetTypes["TextInput"], index) => ({
|
||||
...exclude(props),
|
||||
$$events: { commitText: (e: CustomEvent) => widgetValueCommitAndUpdate(index, e.detail, true) },
|
||||
}),
|
||||
},
|
||||
TextLabel: {
|
||||
component: TextLabel,
|
||||
getProps: (props: WidgetTypes["TextLabel"]) => exclude(props, ["value"]),
|
||||
getSlotContent: (props: WidgetTypes["TextLabel"]) => props.value,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- TODO: Refactor this component to use `<svelte:component this={attributesObject} />` to avoid all the separate conditional components -->
|
||||
|
||||
<div class={`widget-span ${className} ${extraClasses}`.trim()} class:narrow class:row={direction === "row"} class:column={direction === "column"}>
|
||||
{#each widgets as component, widgetIndex}
|
||||
{@const checkboxInput = narrowWidgetProps(component.props, "CheckboxInput")}
|
||||
{#if checkboxInput}
|
||||
<CheckboxInput {...exclude(checkboxInput)} on:checked={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} />
|
||||
{/if}
|
||||
{@const colorInput = narrowWidgetProps(component.props, "ColorInput")}
|
||||
{#if colorInput}
|
||||
<ColorInput
|
||||
{...exclude(colorInput)}
|
||||
on:value={({ detail }) => widgetValueUpdate(widgetIndex, detail, false)}
|
||||
on:startHistoryTransaction={() => widgetValueCommit(widgetIndex, colorInput.value)}
|
||||
/>
|
||||
{/if}
|
||||
<!-- TODO: Curves Input is currently unused -->
|
||||
{@const curvesInput = narrowWidgetProps(component.props, "CurveInput")}
|
||||
{#if curvesInput}
|
||||
<CurveInput
|
||||
{...exclude(curvesInput)}
|
||||
on:value={({ detail }) => debouncer((value) => widgetValueCommitAndUpdate(widgetIndex, value, false), { debounceTime: 120 }).debounceUpdateValue(detail)}
|
||||
/>
|
||||
{/if}
|
||||
{@const dropdownInput = narrowWidgetProps(component.props, "DropdownInput")}
|
||||
{#if dropdownInput}
|
||||
<DropdownInput
|
||||
{...exclude(dropdownInput)}
|
||||
on:hoverInEntry={({ detail }) => {
|
||||
return widgetValueUpdate(widgetIndex, detail, false);
|
||||
}}
|
||||
on:hoverOutEntry={({ detail }) => {
|
||||
return widgetValueUpdate(widgetIndex, detail, false);
|
||||
}}
|
||||
on:selectedIndex={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)}
|
||||
/>
|
||||
{/if}
|
||||
{@const parameterExposeButton = narrowWidgetProps(component.props, "ParameterExposeButton")}
|
||||
{#if parameterExposeButton}
|
||||
<ParameterExposeButton {...exclude(parameterExposeButton)} action={() => widgetValueCommitAndUpdate(widgetIndex, undefined, true)} />
|
||||
{/if}
|
||||
{@const iconButton = narrowWidgetProps(component.props, "IconButton")}
|
||||
{#if iconButton}
|
||||
<IconButton {...exclude(iconButton)} action={() => widgetValueCommitAndUpdate(widgetIndex, undefined, true)} />
|
||||
{/if}
|
||||
{@const iconLabel = narrowWidgetProps(component.props, "IconLabel")}
|
||||
{#if iconLabel}
|
||||
<IconLabel {...exclude(iconLabel)} />
|
||||
{/if}
|
||||
{@const shortcutLabel = narrowWidgetProps(component.props, "ShortcutLabel")}
|
||||
{@const shortcutLabelShortcut = shortcutLabel?.shortcut ? { ...shortcutLabel, shortcut: shortcutLabel.shortcut } : undefined}
|
||||
{#if shortcutLabel && shortcutLabelShortcut}
|
||||
<ShortcutLabel {...exclude(shortcutLabelShortcut)} />
|
||||
{/if}
|
||||
{@const imageLabel = narrowWidgetProps(component.props, "ImageLabel")}
|
||||
{#if imageLabel}
|
||||
<ImageLabel {...exclude(imageLabel)} />
|
||||
{/if}
|
||||
{@const imageButton = narrowWidgetProps(component.props, "ImageButton")}
|
||||
{#if imageButton}
|
||||
<ImageButton {...exclude(imageButton)} action={() => widgetValueCommitAndUpdate(widgetIndex, undefined, true)} />
|
||||
{/if}
|
||||
{@const nodeCatalog = narrowWidgetProps(component.props, "NodeCatalog")}
|
||||
{#if nodeCatalog}
|
||||
<NodeCatalog {...exclude(nodeCatalog)} on:selectNodeType={(e) => widgetValueCommitAndUpdate(widgetIndex, e.detail, false)} />
|
||||
{/if}
|
||||
{@const numberInput = narrowWidgetProps(component.props, "NumberInput")}
|
||||
{#if numberInput}
|
||||
<NumberInput
|
||||
{...exclude(numberInput)}
|
||||
on:value={({ detail }) => debouncer((value) => widgetValueUpdate(widgetIndex, value, true)).debounceUpdateValue(detail)}
|
||||
on:startHistoryTransaction={() => widgetValueCommit(widgetIndex, numberInput.value)}
|
||||
incrementCallbackIncrease={() => widgetValueCommitAndUpdate(widgetIndex, "Increment", false)}
|
||||
incrementCallbackDecrease={() => widgetValueCommitAndUpdate(widgetIndex, "Decrement", false)}
|
||||
/>
|
||||
{/if}
|
||||
{@const referencePointInput = narrowWidgetProps(component.props, "ReferencePointInput")}
|
||||
{#if referencePointInput}
|
||||
<ReferencePointInput {...exclude(referencePointInput)} on:value={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} />
|
||||
{/if}
|
||||
{@const popoverButton = narrowWidgetProps(component.props, "PopoverButton")}
|
||||
{#if popoverButton}
|
||||
<PopoverButton {...exclude(popoverButton)} {layoutTarget} />
|
||||
{/if}
|
||||
{@const radioInput = narrowWidgetProps(component.props, "RadioInput")}
|
||||
{#if radioInput}
|
||||
<RadioInput {...exclude(radioInput)} on:selectedIndex={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} />
|
||||
{/if}
|
||||
{@const separator = narrowWidgetProps(component.props, "Separator")}
|
||||
{#if separator}
|
||||
<Separator {...exclude(separator)} />
|
||||
{/if}
|
||||
{@const workingColorsInput = narrowWidgetProps(component.props, "WorkingColorsInput")}
|
||||
{#if workingColorsInput}
|
||||
<WorkingColorsInput {...exclude(workingColorsInput)} />
|
||||
{/if}
|
||||
{@const textAreaInput = narrowWidgetProps(component.props, "TextAreaInput")}
|
||||
{#if textAreaInput}
|
||||
<TextAreaInput {...exclude(textAreaInput)} on:commitText={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, false)} />
|
||||
{/if}
|
||||
{@const textButton = narrowWidgetProps(component.props, "TextButton")}
|
||||
{#if textButton}
|
||||
<TextButton
|
||||
{...exclude(textButton)}
|
||||
action={() => widgetValueCommitAndUpdate(widgetIndex, [], true)}
|
||||
on:selectedEntryValuePath={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, false)}
|
||||
/>
|
||||
{/if}
|
||||
{@const breadcrumbTrailButtons = narrowWidgetProps(component.props, "BreadcrumbTrailButtons")}
|
||||
{#if breadcrumbTrailButtons}
|
||||
<BreadcrumbTrailButtons {...exclude(breadcrumbTrailButtons)} action={(breadcrumbIndex) => widgetValueCommitAndUpdate(widgetIndex, breadcrumbIndex, true)} />
|
||||
{/if}
|
||||
{@const textInput = narrowWidgetProps(component.props, "TextInput")}
|
||||
{#if textInput}
|
||||
<TextInput {...exclude(textInput)} on:commitText={({ detail }) => widgetValueCommitAndUpdate(widgetIndex, detail, true)} />
|
||||
{/if}
|
||||
{@const textLabel = narrowWidgetProps(component.props, "TextLabel")}
|
||||
{#if textLabel}
|
||||
<TextLabel {...exclude(textLabel, ["value"])}>{textLabel.value}</TextLabel>
|
||||
{#each widgets as widget, widgetIndex}
|
||||
{@const config = widgetRegistry[widget.props.kind]}
|
||||
{@const props = config?.getProps(widget.props, widgetIndex)}
|
||||
{@const slot = config?.getSlotContent?.(widget.props)}
|
||||
{#if props !== undefined && slot !== undefined}
|
||||
<svelte:component this={config.component} {...props}>{slot}</svelte:component>
|
||||
{:else if props !== undefined}
|
||||
<svelte:component this={config.component} {...props} />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
@@ -213,8 +268,8 @@
|
||||
.widget-span.row {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
--row-height: 32px;
|
||||
min-height: var(--row-height);
|
||||
--row-height: 32px;
|
||||
|
||||
&.narrow {
|
||||
--row-height: 24px;
|
||||
|
||||
Reference in New Issue
Block a user