Improve 'add node' menu (#1312)

* improved node adding list with dropdowns and scrolling

* changed arrow icon from default details arrow

* made 'add node' menu appear above mouse when clicking at bottom of nodegraph. Searching automatically opens the dropdowns you need. Set fixed 'add node' menu size

* updated code style to be more clear

* undo mistake changes

---------

Co-authored-by: 0HyperCube <78500760+0HyperCube@users.noreply.github.com>
This commit is contained in:
Samyat Gautam
2023-06-23 19:01:39 +01:00
committed by GitHub
co-authored by 0HyperCube
parent 7f2347f842
commit 30fc9d065b
2 changed files with 122 additions and 42 deletions
+98 -17
View File
@@ -18,6 +18,8 @@
const WHEEL_RATE = (1 / 600) * 3; const WHEEL_RATE = (1 / 600) * 3;
const GRID_COLLAPSE_SPACING = 10; const GRID_COLLAPSE_SPACING = 10;
const GRID_SIZE = 24; const GRID_SIZE = 24;
const ADD_NODE_MENU_WIDTH = 180;
const ADD_NODE_MENU_HEIGHT = 200;
const editor = getContext<Editor>("editor"); const editor = getContext<Editor>("editor");
const nodeGraph = getContext<NodeGraphState>("nodeGraph"); const nodeGraph = getContext<NodeGraphState>("nodeGraph");
@@ -46,6 +48,19 @@
$: nodeCategories = buildNodeCategories($nodeGraph.nodeTypes, searchTerm); $: nodeCategories = buildNodeCategories($nodeGraph.nodeTypes, searchTerm);
$: nodeListX = ((nodeListLocation?.x || 0) * GRID_SIZE + transform.x) * transform.scale; $: nodeListX = ((nodeListLocation?.x || 0) * GRID_SIZE + transform.x) * transform.scale;
$: nodeListY = ((nodeListLocation?.y || 0) * GRID_SIZE + transform.y) * transform.scale; $: nodeListY = ((nodeListLocation?.y || 0) * GRID_SIZE + transform.y) * transform.scale;
let appearAboveMouse = false;
let appearRightOfMouse = false;
$: (() => {
const bounds = graph?.div()?.getBoundingClientRect();
if (!bounds) return;
const { width, height } = bounds;
appearRightOfMouse = nodeListX > width - ADD_NODE_MENU_WIDTH / 2;
appearAboveMouse = nodeListY > height - ADD_NODE_MENU_HEIGHT / 2;
})();
$: linkPathInProgress = createLinkPathInProgress(linkInProgressFromConnector, linkInProgressToConnector); $: linkPathInProgress = createLinkPathInProgress(linkInProgressFromConnector, linkInProgressToConnector);
$: linkPaths = createLinkPaths(linkPathInProgress, nodeLinkPaths); $: linkPaths = createLinkPaths(linkPathInProgress, nodeLinkPaths);
@@ -60,16 +75,35 @@
return sparse; return sparse;
} }
function buildNodeCategories(nodeTypes: FrontendNodeType[], searchTerm: string): [string, FrontendNodeType[]][] { type NodeCategoryDetails = {
const categories = new Map(); nodes: FrontendNodeType[];
open: boolean;
};
function buildNodeCategories(nodeTypes: FrontendNodeType[], searchTerm: string): [string, NodeCategoryDetails][] {
const categories = new Map<string, NodeCategoryDetails>();
nodeTypes.forEach((node) => { nodeTypes.forEach((node) => {
if (searchTerm.length > 0 && !node.name.toLowerCase().includes(searchTerm.toLowerCase()) && !node.category.toLowerCase().includes(searchTerm.toLowerCase())) { const nameIncludesSearchTerm = node.name.toLowerCase().includes(searchTerm.toLowerCase());
if (searchTerm.length > 0 && !nameIncludesSearchTerm && !node.category.toLowerCase().includes(searchTerm.toLowerCase())) {
return; return;
} }
const category = categories.get(node.category); const category = categories.get(node.category);
if (category) category.push(node); let open = nameIncludesSearchTerm;
else categories.set(node.category, [node]); if (searchTerm.length === 0) {
open = false;
}
if (category) {
category.open = open;
category.nodes.push(node);
} else
categories.set(node.category, {
open: open,
nodes: [node],
});
}); });
return Array.from(categories); return Array.from(categories);
@@ -497,18 +531,32 @@
}} }}
> >
{#if nodeListLocation} {#if nodeListLocation}
<LayoutCol class="node-list" data-node-list styles={{ "margin-left": `${nodeListX}px`, "margin-top": `${nodeListY}px` }}> <LayoutCol
class="node-list"
data-node-list
styles={{
left: `${nodeListX}px`,
top: `${nodeListY}px`,
transform: `translate(${appearRightOfMouse ? -100 : 0}%, ${appearAboveMouse ? -100 : 0}%)`,
width: `${ADD_NODE_MENU_WIDTH}px`,
}}
>
<TextInput placeholder="Search Nodes..." value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} /> <TextInput placeholder="Search Nodes..." value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} />
{#each nodeCategories as nodeCategory} <div class="list-nodes" style={`height: ${ADD_NODE_MENU_HEIGHT}px;`} on:wheel|stopPropagation>
<LayoutCol> {#each nodeCategories as nodeCategory}
<TextLabel>{nodeCategory[0]}</TextLabel> <details style="display: flex; flex-direction: column;" open={nodeCategory[1].open}>
{#each nodeCategory[1] as nodeType} <summary>
<TextButton label={nodeType.name} action={() => createNode(nodeType.name)} /> <IconLabel icon="DropdownArrow" />
{/each} <TextLabel>{nodeCategory[0]}</TextLabel>
</LayoutCol> </summary>
{:else} {#each nodeCategory[1].nodes as nodeType}
<TextLabel>No search results</TextLabel> <TextButton label={nodeType.name} action={() => createNode(nodeType.name)} />
{/each} {/each}
</details>
{:else}
<div style="margin-right: 4px;"><TextLabel>No search results</TextLabel></div>
{/each}
</div>
</LayoutCol> </LayoutCol>
{/if} {/if}
<div class="nodes" style:transform={`scale(${transform.scale}) translate(${transform.x}px, ${transform.y}px)`} style:transform-origin={`0 0`} bind:this={nodesContainer}> <div class="nodes" style:transform={`scale(${transform.scale}) translate(${transform.x}px, ${transform.y}px)`} style:transform-origin={`0 0`} bind:this={nodesContainer}>
@@ -607,12 +655,45 @@
.node-list { .node-list {
width: max-content; width: max-content;
position: fixed; position: absolute;
padding: 5px; padding: 5px;
z-index: 3; z-index: 3;
background-color: var(--color-3-darkgray); background-color: var(--color-3-darkgray);
.text-button {
width: 100%;
}
.list-nodes {
overflow-y: scroll;
}
details {
margin-right: 4px;
cursor: pointer;
}
summary {
list-style-type: none;
display: flex;
align-items: center;
gap: 2px;
span {
white-space: break-spaces;
}
}
details summary svg {
transform: rotate(-90deg);
}
details[open] summary svg {
transform: rotate(0deg);
}
.text-button + .text-button { .text-button + .text-button {
display: block;
margin-left: 0; margin-left: 0;
margin-top: 4px; margin-top: 4px;
} }
+24 -25
View File
@@ -42,7 +42,6 @@ export class UpdateNodeGraphSelection extends JsMessage {
readonly selected!: bigint[]; readonly selected!: bigint[];
} }
export class UpdateOpenDocumentsList extends JsMessage { export class UpdateOpenDocumentsList extends JsMessage {
@Type(() => FrontendDocumentDetails) @Type(() => FrontendDocumentDetails)
readonly openDocuments!: FrontendDocumentDetails[]; readonly openDocuments!: FrontendDocumentDetails[];
@@ -493,22 +492,22 @@ const mouseCursorIconCSSNames = {
Rotate: "custom-rotate", Rotate: "custom-rotate",
} as const; } as const;
export type MouseCursor = keyof typeof mouseCursorIconCSSNames; export type MouseCursor = keyof typeof mouseCursorIconCSSNames;
export type MouseCursorIcon = typeof mouseCursorIconCSSNames[MouseCursor]; export type MouseCursorIcon = (typeof mouseCursorIconCSSNames)[MouseCursor];
export class UpdateMouseCursor extends JsMessage { export class UpdateMouseCursor extends JsMessage {
@Transform(({ value }: { value: MouseCursor }) => mouseCursorIconCSSNames[value] || "alias") @Transform(({ value }: { value: MouseCursor }) => mouseCursorIconCSSNames[value] || "alias")
readonly cursor!: MouseCursorIcon; readonly cursor!: MouseCursorIcon;
} }
export class TriggerLoadAutoSaveDocuments extends JsMessage { } export class TriggerLoadAutoSaveDocuments extends JsMessage {}
export class TriggerLoadPreferences extends JsMessage { } export class TriggerLoadPreferences extends JsMessage {}
export class TriggerOpenDocument extends JsMessage { } export class TriggerOpenDocument extends JsMessage {}
export class TriggerImport extends JsMessage { } export class TriggerImport extends JsMessage {}
export class TriggerPaste extends JsMessage { } export class TriggerPaste extends JsMessage {}
export class TriggerCopyToClipboardBlobUrl extends JsMessage { export class TriggerCopyToClipboardBlobUrl extends JsMessage {
readonly blobUrl!: string; readonly blobUrl!: string;
@@ -547,7 +546,7 @@ export class TriggerRasterizeRegionBelowLayer extends JsMessage {
readonly size!: [number, number]; readonly size!: [number, number];
} }
export class TriggerRefreshBoundsOfViewports extends JsMessage { } export class TriggerRefreshBoundsOfViewports extends JsMessage {}
export class TriggerRevokeBlobUrl extends JsMessage { export class TriggerRevokeBlobUrl extends JsMessage {
readonly url!: string; readonly url!: string;
@@ -557,7 +556,7 @@ export class TriggerSavePreferences extends JsMessage {
readonly preferences!: Record<string, unknown>; readonly preferences!: Record<string, unknown>;
} }
export class DocumentChanged extends JsMessage { } export class DocumentChanged extends JsMessage {}
export class UpdateDocumentLayerTreeStructureJs extends JsMessage { export class UpdateDocumentLayerTreeStructureJs extends JsMessage {
constructor(readonly layerId: bigint, readonly children: UpdateDocumentLayerTreeStructureJs[]) { constructor(readonly layerId: bigint, readonly children: UpdateDocumentLayerTreeStructureJs[]) {
@@ -650,7 +649,7 @@ export class UpdateImageData extends JsMessage {
readonly imageData!: ImaginateImageData[]; readonly imageData!: ImaginateImageData[];
} }
export class DisplayRemoveEditableTextbox extends JsMessage { } export class DisplayRemoveEditableTextbox extends JsMessage {}
export class UpdateDocumentLayerDetails extends JsMessage { export class UpdateDocumentLayerDetails extends JsMessage {
@Type(() => LayerPanelEntry) @Type(() => LayerPanelEntry)
@@ -701,7 +700,7 @@ export class ImaginateImageData {
readonly transform!: Float64Array; readonly transform!: Float64Array;
} }
export class DisplayDialogDismiss extends JsMessage { } export class DisplayDialogDismiss extends JsMessage {}
export class Font { export class Font {
fontFamily!: string; fontFamily!: string;
@@ -720,7 +719,7 @@ export class TriggerVisitLink extends JsMessage {
url!: string; url!: string;
} }
export class TriggerTextCommit extends JsMessage { } export class TriggerTextCommit extends JsMessage {}
export class TriggerTextCopy extends JsMessage { export class TriggerTextCopy extends JsMessage {
readonly copyText!: string; readonly copyText!: string;
@@ -730,7 +729,7 @@ export class TriggerAboutGraphiteLocalizedCommitDate extends JsMessage {
readonly commitDate!: string; readonly commitDate!: string;
} }
export class TriggerViewportResize extends JsMessage { } export class TriggerViewportResize extends JsMessage {}
// WIDGET PROPS // WIDGET PROPS
@@ -1098,13 +1097,13 @@ const widgetSubTypes = [
{ value: TextLabel, name: "TextLabel" }, { value: TextLabel, name: "TextLabel" },
] as const; ] as const;
type WidgetSubTypes = typeof widgetSubTypes[number]; type WidgetSubTypes = (typeof widgetSubTypes)[number];
type WidgetKindMap = { [T in WidgetSubTypes as T["name"]]: InstanceType<T["value"]> }; type WidgetKindMap = { [T in WidgetSubTypes as T["name"]]: InstanceType<T["value"]> };
export type WidgetPropsNames = keyof WidgetKindMap; export type WidgetPropsNames = keyof WidgetKindMap;
export type WidgetPropsSet = WidgetKindMap[WidgetPropsNames]; export type WidgetPropsSet = WidgetKindMap[WidgetPropsNames];
export function narrowWidgetProps<K extends WidgetPropsNames>(props: WidgetPropsSet, kind: K) { export function narrowWidgetProps<K extends WidgetPropsNames>(props: WidgetPropsSet, kind: K) {
if (props.kind === kind) return props as WidgetKindMap[K] if (props.kind === kind) return props as WidgetKindMap[K];
else return undefined; else return undefined;
} }
@@ -1258,25 +1257,25 @@ function createLayoutGroup(layoutGroup: any): LayoutGroup {
} }
// WIDGET LAYOUTS // WIDGET LAYOUTS
export class UpdateDialogDetails extends WidgetDiffUpdate { } export class UpdateDialogDetails extends WidgetDiffUpdate {}
export class UpdateDocumentModeLayout extends WidgetDiffUpdate { } export class UpdateDocumentModeLayout extends WidgetDiffUpdate {}
export class UpdateToolOptionsLayout extends WidgetDiffUpdate { } export class UpdateToolOptionsLayout extends WidgetDiffUpdate {}
export class UpdateDocumentBarLayout extends WidgetDiffUpdate { } export class UpdateDocumentBarLayout extends WidgetDiffUpdate {}
export class UpdateToolShelfLayout extends WidgetDiffUpdate { } export class UpdateToolShelfLayout extends WidgetDiffUpdate {}
export class UpdateWorkingColorsLayout extends WidgetDiffUpdate { } export class UpdateWorkingColorsLayout extends WidgetDiffUpdate {}
export class UpdatePropertyPanelOptionsLayout extends WidgetDiffUpdate { } export class UpdatePropertyPanelOptionsLayout extends WidgetDiffUpdate {}
export class UpdatePropertyPanelSectionsLayout extends WidgetDiffUpdate { } export class UpdatePropertyPanelSectionsLayout extends WidgetDiffUpdate {}
export class UpdateLayerTreeOptionsLayout extends WidgetDiffUpdate { } export class UpdateLayerTreeOptionsLayout extends WidgetDiffUpdate {}
export class UpdateNodeGraphBarLayout extends WidgetDiffUpdate { } export class UpdateNodeGraphBarLayout extends WidgetDiffUpdate {}
export class UpdateMenuBarLayout extends JsMessage { export class UpdateMenuBarLayout extends JsMessage {
layoutTarget!: unknown; layoutTarget!: unknown;