mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-28 11:48:12 +08:00
Add a node insertion button and layer renaming from the Properties panel (#2072)
* Add node button * Improve css a bit * Add layer renaming to the Properties panel and move New Layer to that, plus add unpinning to properties sections * Add tooltip * Re-add layer itself in listing * Final code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
co-authored by
Keavon Chambers
parent
3c839ffd2b
commit
5aa6716910
@@ -0,0 +1,161 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, getContext, onMount } from "svelte";
|
||||
|
||||
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
|
||||
import type { FrontendNodeType } from "@graphite/wasm-communication/messages";
|
||||
|
||||
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
|
||||
import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher<{ selectNodeType: string }>();
|
||||
const nodeGraph = getContext<NodeGraphState>("nodeGraph");
|
||||
|
||||
export let disabled = false;
|
||||
|
||||
let nodeSearchInput: TextInput | undefined = undefined;
|
||||
let searchTerm = "";
|
||||
|
||||
$: nodeCategories = buildNodeCategories($nodeGraph.nodeTypes, searchTerm);
|
||||
|
||||
type NodeCategoryDetails = {
|
||||
nodes: FrontendNodeType[];
|
||||
open: boolean;
|
||||
};
|
||||
|
||||
function buildNodeCategories(nodeTypes: FrontendNodeType[], searchTerm: string): [string, NodeCategoryDetails][] {
|
||||
const categories = new Map<string, NodeCategoryDetails>();
|
||||
|
||||
nodeTypes.forEach((node) => {
|
||||
let nameIncludesSearchTerm = node.name.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
// Quick and dirty hack to alias "Layer" to "Merge" in the search
|
||||
if (node.name === "Merge") {
|
||||
nameIncludesSearchTerm = nameIncludesSearchTerm || "Layer".toLowerCase().includes(searchTerm.toLowerCase());
|
||||
}
|
||||
|
||||
if (searchTerm.length > 0 && !nameIncludesSearchTerm && !node.category.toLowerCase().includes(searchTerm.toLowerCase())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const category = categories.get(node.category);
|
||||
let open = nameIncludesSearchTerm;
|
||||
if (searchTerm.length === 0) {
|
||||
open = false;
|
||||
}
|
||||
|
||||
if (category) {
|
||||
category.open = open;
|
||||
category.nodes.push(node);
|
||||
} else
|
||||
categories.set(node.category, {
|
||||
open,
|
||||
nodes: [node],
|
||||
});
|
||||
});
|
||||
|
||||
const START_CATEGORIES_ORDER = ["UNCATEGORIZED", "General", "Value", "Math", "Style"];
|
||||
const END_CATEGORIES_ORDER = ["Debug"];
|
||||
return Array.from(categories)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.sort((a, b) => {
|
||||
const aIndex = START_CATEGORIES_ORDER.findIndex((x) => a[0].startsWith(x));
|
||||
const bIndex = START_CATEGORIES_ORDER.findIndex((x) => b[0].startsWith(x));
|
||||
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
|
||||
if (aIndex !== -1) return -1;
|
||||
if (bIndex !== -1) return 1;
|
||||
return 0;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const aIndex = END_CATEGORIES_ORDER.findIndex((x) => a[0].startsWith(x));
|
||||
const bIndex = END_CATEGORIES_ORDER.findIndex((x) => b[0].startsWith(x));
|
||||
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
|
||||
if (aIndex !== -1) return 1;
|
||||
if (bIndex !== -1) return -1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(() => nodeSearchInput?.focus(), 0);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="node-catalog">
|
||||
<TextInput placeholder="Search Nodes..." value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} />
|
||||
<div class="list-results" on:wheel|passive|stopPropagation>
|
||||
{#each nodeCategories as nodeCategory}
|
||||
<details open={nodeCategory[1].open}>
|
||||
<summary>
|
||||
<TextLabel>{nodeCategory[0]}</TextLabel>
|
||||
</summary>
|
||||
{#each nodeCategory[1].nodes as nodeType}
|
||||
<TextButton {disabled} label={nodeType.name} action={() => dispatch("selectNodeType", nodeType.name)} />
|
||||
{/each}
|
||||
</details>
|
||||
{:else}
|
||||
<TextLabel>No search results</TextLabel>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss" global>
|
||||
.node-catalog {
|
||||
max-height: 40vh;
|
||||
min-width: 250px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
.text-input {
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.list-results {
|
||||
overflow-y: auto;
|
||||
flex: 1 1 auto;
|
||||
// Together with the `margin-right: 4px;` on `details` below, this keeps a gap between the listings and the scrollbar
|
||||
margin-right: -4px;
|
||||
|
||||
details {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
// Together with the `margin-right: -4px;` on `.list-results` above, this keeps a gap between the listings and the scrollbar
|
||||
margin-right: 4px;
|
||||
|
||||
&[open] summary .text-label::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
|
||||
.text-label {
|
||||
padding-left: 16px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
margin: auto;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--icon-expand-collapse-arrow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text-button {
|
||||
width: 100%;
|
||||
margin: 4px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user