Virtual list in dropdowns

This commit is contained in:
space-nuko
2023-05-10 09:54:04 -05:00
parent 127768f04d
commit ce9ee44f45
7 changed files with 214 additions and 45 deletions

View File

@@ -54,6 +54,7 @@
"@litegraph-ts/nodes-math": "workspace:*", "@litegraph-ts/nodes-math": "workspace:*",
"@litegraph-ts/nodes-strings": "workspace:*", "@litegraph-ts/nodes-strings": "workspace:*",
"@litegraph-ts/tsconfig": "workspace:*", "@litegraph-ts/tsconfig": "workspace:*",
"@sveltejs/svelte-virtual-list": "^3.0.1",
"@sveltejs/vite-plugin-svelte": "^2.1.1", "@sveltejs/vite-plugin-svelte": "^2.1.1",
"@tsconfig/svelte": "^4.0.1", "@tsconfig/svelte": "^4.0.1",
"events": "^3.3.0", "events": "^3.3.0",

7
pnpm-lock.yaml generated
View File

@@ -64,6 +64,9 @@ importers:
'@litegraph-ts/tsconfig': '@litegraph-ts/tsconfig':
specifier: workspace:* specifier: workspace:*
version: link:litegraph/packages/tsconfig version: link:litegraph/packages/tsconfig
'@sveltejs/svelte-virtual-list':
specifier: ^3.0.1
version: 3.0.1
'@sveltejs/vite-plugin-svelte': '@sveltejs/vite-plugin-svelte':
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1(svelte@3.58.0)(vite@4.3.1) version: 2.1.1(svelte@3.58.0)(vite@4.3.1)
@@ -2225,6 +2228,10 @@ packages:
- supports-color - supports-color
dev: true dev: true
/@sveltejs/svelte-virtual-list@3.0.1:
resolution: {integrity: sha512-aF9TptS7NKKS7/TqpsxQBSDJ9Q0XBYzBehCeIC5DzdMEgrJZpIYao9LRLnyyo6SVodpapm2B7FE/Lj+FSA5/SQ==}
dev: false
/@sveltejs/vite-plugin-svelte@2.1.1(svelte@3.58.0): /@sveltejs/vite-plugin-svelte@2.1.1(svelte@3.58.0):
resolution: {integrity: sha512-7YeBDt4us0FiIMNsVXxyaP4Hwyn2/v9x3oqStkHU3ZdIc5O22pGwUwH33wUqYo+7Itdmo8zxJ45Qvfm3H7UUjQ==} resolution: {integrity: sha512-7YeBDt4us0FiIMNsVXxyaP4Hwyn2/v9x3oqStkHU3ZdIc5O22pGwUwH33wUqYo+7Itdmo8zxJ45Qvfm3H7UUjQ==}
engines: {node: ^14.18.0 || >= 16} engines: {node: ^14.18.0 || >= 16}

View File

@@ -764,6 +764,16 @@ export default class ComfyApp {
async refreshComboInNodes(flashUI: boolean = false) { async refreshComboInNodes(flashUI: boolean = false) {
const defs = await this.api.getNodeDefs(); const defs = await this.api.getNodeDefs();
for (let nodeNum in this.lGraph._nodes) {
const node = this.lGraph._nodes[nodeNum];
if (node.type === "ui/combo") {
(node as nodes.ComfyComboNode).valuesForCombo = null;
(node as nodes.ComfyComboNode).comboRefreshed.set(true);
}
}
let seen = new Set()
for (let nodeNum in this.lGraph._nodes) { for (let nodeNum in this.lGraph._nodes) {
const node = this.lGraph._nodes[nodeNum]; const node = this.lGraph._nodes[nodeNum];
@@ -775,13 +785,18 @@ export default class ComfyApp {
const comfyInput = input as IComfyInputSlot; const comfyInput = input as IComfyInputSlot;
if (comfyInput.defaultWidgetNode == nodes.ComfyComboNode && def["input"]["required"][comfyInput.name] !== undefined) { if (comfyInput.defaultWidgetNode == nodes.ComfyComboNode && def["input"]["required"][comfyInput.name] !== undefined) {
comfyInput.config.values = def["input"]["required"][comfyInput.name][0]; const rawValues = def["input"]["required"][comfyInput.name][0];
comfyInput.config.values = rawValues;
const inputNode = node.getInputNode(index) const inputNode = node.getInputNode(index)
if (inputNode && "doAutoConfig" in inputNode && comfyInput.widgetNodeType === inputNode.type) { if (inputNode && "doAutoConfig" in inputNode && comfyInput.widgetNodeType === inputNode.type && !seen.has(inputNode.id)) {
console.debug("[ComfyApp] Reconfiguring combo widget", inputNode.type, comfyInput.config.values) seen.add(inputNode.id)
console.warn("[ComfyApp] Reconfiguring combo widget", inputNode.type, comfyInput.config.values.length)
const comfyComboNode = inputNode as nodes.ComfyComboNode; const comfyComboNode = inputNode as nodes.ComfyComboNode;
comfyComboNode.doAutoConfig(comfyInput, { includeProperties: new Set(["values"]), setWidgetTitle: false }) comfyComboNode.doAutoConfig(comfyInput, { includeProperties: new Set(["values"]), setWidgetTitle: false })
comfyComboNode.formatValues(rawValues)
if (!comfyInput.config.values.includes(get(comfyComboNode.value))) { if (!comfyInput.config.values.includes(get(comfyComboNode.value))) {
comfyComboNode.setValue(comfyInput.config.defaultValue || comfyInput.config.values[0]) comfyComboNode.setValue(comfyInput.config.defaultValue || comfyInput.config.values[0])
} }

View File

@@ -268,7 +268,7 @@
on:input={(e) => updateAttribute(spec, target, e.detail)} on:input={(e) => updateAttribute(spec, target, e.detail)}
disabled={!$uiState.uiUnlocked || !spec.editable} disabled={!$uiState.uiUnlocked || !spec.editable}
label={spec.name} label={spec.name}
max_lines={1} max_lines={spec.multiline ? 5 : 1}
/> />
{:else if spec.type === "boolean"} {:else if spec.type === "boolean"}
<Checkbox <Checkbox
@@ -307,7 +307,7 @@
on:input={(e) => updateProperty(spec, e.detail)} on:input={(e) => updateProperty(spec, e.detail)}
label={spec.name} label={spec.name}
disabled={!$uiState.uiUnlocked || !spec.editable} disabled={!$uiState.uiUnlocked || !spec.editable}
max_lines={1} max_lines={spec.multiline ? 5 : 1}
/> />
{:else if spec.type === "boolean"} {:else if spec.type === "boolean"}
<Checkbox <Checkbox
@@ -345,7 +345,7 @@
on:input={(e) => updateVar(spec, e.detail)} on:input={(e) => updateVar(spec, e.detail)}
label={spec.name} label={spec.name}
disabled={!$uiState.uiUnlocked || !spec.editable} disabled={!$uiState.uiUnlocked || !spec.editable}
max_lines={1} max_lines={spec.multiline ? 5 : 1}
/> />
{:else if spec.type === "boolean"} {:else if spec.type === "boolean"}
<Checkbox <Checkbox
@@ -384,7 +384,7 @@
on:input={(e) => updateWorkflowAttribute(spec, e.detail)} on:input={(e) => updateWorkflowAttribute(spec, e.detail)}
label={spec.name} label={spec.name}
disabled={!$uiState.uiUnlocked || !spec.editable} disabled={!$uiState.uiUnlocked || !spec.editable}
max_lines={1} max_lines={spec.multiline ? 5 : 1}
/> />
{:else if spec.type === "boolean"} {:else if spec.type === "boolean"}
<Checkbox <Checkbox

View File

@@ -140,7 +140,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
} }
private onValueUpdated(value: any) { private onValueUpdated(value: any) {
console.debug("[Widget] valueUpdated", this, value) // console.debug("[Widget] valueUpdated", this, value)
this.displayWidget.value = this.formatValue(value) this.displayWidget.value = this.formatValue(value)
if (this.outputIndex !== null && this.outputs.length >= this.outputIndex) { if (this.outputIndex !== null && this.outputs.length >= this.outputIndex) {
@@ -151,7 +151,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
if (!this.delayChangedEvent) if (!this.delayChangedEvent)
this.triggerChangeEvent(get(this.value)) this.triggerChangeEvent(get(this.value))
else { else {
console.debug("[Widget] queueChangeEvent", this, value) // console.debug("[Widget] queueChangeEvent", this, value)
this._aboutToChange = 2; // wait 1.5-2 frames, in case we're already in the middle of executing the graph this._aboutToChange = 2; // wait 1.5-2 frames, in case we're already in the middle of executing the graph
this._aboutToChangeValue = get(this.value); this._aboutToChangeValue = get(this.value);
} }
@@ -402,13 +402,17 @@ LiteGraph.registerNodeType({
export interface ComfyComboProperties extends ComfyWidgetProperties { export interface ComfyComboProperties extends ComfyWidgetProperties {
values: string[] values: string[]
/* JS Function body that takes a parameter named "value" as a parameter and returns the label for each combo entry */
convertValueToLabelCode: string
} }
export class ComfyComboNode extends ComfyWidgetNode<string> { export class ComfyComboNode extends ComfyWidgetNode<string> {
override properties: ComfyComboProperties = { override properties: ComfyComboProperties = {
tags: [], tags: [],
defaultValue: "A", defaultValue: "A",
values: ["A", "B", "C", "D"] values: ["A", "B", "C", "D"],
convertValueToLabelCode: ""
} }
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
@@ -428,11 +432,52 @@ export class ComfyComboNode extends ComfyWidgetNode<string> {
comboRefreshed: Writable<boolean>; comboRefreshed: Writable<boolean>;
valuesForCombo: any[] | null = null;
constructor(name?: string) { constructor(name?: string) {
super(name, "A") super(name, "A")
this.comboRefreshed = writable(false) this.comboRefreshed = writable(false)
} }
override onPropertyChanged(property: any, value: any) {
if (property === "values" || property === "convertValueToLabelCode") {
this.formatValues(this.properties.values)
}
}
formatValues(values: string[]) {
if (values == null)
return;
this.properties.values = values;
let formatter: any;
if (this.properties.convertValueToLabelCode)
formatter = new Function("value", this.properties.convertValueToLabelCode) as (v: string) => string;
else
formatter = (value) => `${value}`;
try {
this.valuesForCombo = this.properties.values.map(value => {
return {
value,
label: formatter(value)
}
})
}
catch (err) {
console.error("Failed formatting!", err)
this.valuesForCombo = this.properties.values.map(value => {
return {
value,
label: `${value}`
}
})
}
this.comboRefreshed.set(true);
}
onConnectOutput( onConnectOutput(
outputIndex: number, outputIndex: number,
inputType: INodeInputSlot["type"], inputType: INodeInputSlot["type"],
@@ -476,6 +521,12 @@ export class ComfyComboNode extends ComfyWidgetNode<string> {
} }
} }
override onSerialize(o: SerializedLGraphNode) {
super.onSerialize(o);
// TODO fix saving combo nodes with huge values lists
o.properties.values = []
}
override stripUserState(o: SerializedLGraphNode) { override stripUserState(o: SerializedLGraphNode) {
super.stripUserState(o); super.stripUserState(o);
o.properties.values = [] o.properties.values = []

View File

@@ -219,6 +219,11 @@ export type AttributesSpec = {
*/ */
max?: number, max?: number,
/*
* If `type` is "string", display as a textarea.
*/
multiline?: boolean,
/* /*
* Valid `LGraphNode.type`s this property applies to if it's located in a node. * Valid `LGraphNode.type`s this property applies to if it's located in a node.
* These are like "ui/button", "ui/slider". * These are like "ui/button", "ui/slider".
@@ -386,6 +391,17 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
defaultValue: "large" defaultValue: "large"
}, },
// Combo
{
name: "convertValueToLabelCode",
type: "string",
location: "nodeProps",
editable: true,
multiline: true,
validNodeTypes: ["ui/combo"],
defaultValue: ""
},
// Gallery // Gallery
{ {
name: "variant", name: "variant",

View File

@@ -1,9 +1,11 @@
<script lang="ts"> <script lang="ts">
import { BlockTitle } from "@gradio/atoms"; import { BlockTitle } from "@gradio/atoms";
import Select from 'svelte-select'; import Select from 'svelte-select';
import VirtualList from '@sveltejs/svelte-virtual-list';
import ListItem from "./ListItem.svelte"
import type { ComfyComboNode } from "$lib/nodes/index"; import type { ComfyComboNode } from "$lib/nodes/index";
import { type WidgetLayout } from "$lib/stores/layoutState"; import { type WidgetLayout } from "$lib/stores/layoutState";
import { get, type Writable } from "svelte/store"; import { get, writable, type Writable } from "svelte/store";
import { isDisabled } from "./utils" import { isDisabled } from "./utils"
export let widget: WidgetLayout | null = null; export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false; export let isMobile: boolean = false;
@@ -12,22 +14,21 @@
let propsChanged: Writable<number> | null = null; let propsChanged: Writable<number> | null = null;
let comboRefreshed: Writable<boolean> | null = null; let comboRefreshed: Writable<boolean> | null = null;
let wasComboRefreshed: boolean = false; let wasComboRefreshed: boolean = false;
let option: any
export let debug: boolean = false; export let debug: boolean = false;
let input: HTMLInputElement | null = null let input: HTMLInputElement | null = null
$: widget && setNodeValue(widget); $: widget && setNodeValue(widget);
$: if (nodeValue !== null && (!$propsChanged || $propsChanged)) { $: if (nodeValue !== null) {
if (node.properties.values.indexOf(option.value) === -1) { // if (option == null || node.properties.values.indexOf(option.value) === -1) {
setOption($nodeValue) // setOption($nodeValue)
$nodeValue = option // $nodeValue = option
} // }
else { // else {
$nodeValue = option // $nodeValue = option
setOption($nodeValue) // setOption($nodeValue)
} // }
setNodeValue(widget) setNodeValue(widget)
node.properties = node.properties node.properties = node.properties
} }
@@ -40,18 +41,10 @@
comboRefreshed = node.comboRefreshed; comboRefreshed = node.comboRefreshed;
if ($comboRefreshed) if ($comboRefreshed)
flashOnRefreshed(); flashOnRefreshed();
setOption($nodeValue) // don't react on option // setOption($nodeValue) // don't react on option
} }
} }
function setOption(value: any) {
option = value;
}
$: if (nodeValue && option && option.value) {
$nodeValue = option.value;
}
$: $comboRefreshed && flashOnRefreshed(); $: $comboRefreshed && flashOnRefreshed();
function flashOnRefreshed() { function flashOnRefreshed() {
@@ -76,39 +69,81 @@
input.blur(); input.blur();
navigator.vibrate(20) navigator.vibrate(20)
} }
let start = 0;
let end = 0;
let listOpen: boolean = false
function selectItem(item: any) {
$nodeValue = item.value;
listOpen = false;
document.activeElement?.blur();
}
let option: any = null;
let rebuild = writable(0);
let virtualList = null;
function onFilter() {
// $rebuild += 1
if (virtualList) {
// force refresh virtual list
const viewport = virtualList.querySelector("svelte-virtual-list-viewport")
viewport.scrollTo(0, 1)
viewport.scrollTo(0, 0)
}
else {
console.log("no")
}
}
</script> </script>
<div class="wrapper comfy-combo" class:updated={$comboRefreshed}> <div class="wrapper comfy-combo" class:updated={$comboRefreshed}>
{#key $propsChanged} {#key $comboRefreshed}
{#key $comboRefreshed} {#if node !== null && nodeValue !== null}
{#if node !== null && nodeValue !== null} {#if node.valuesForCombo == null}
<span>Loading...</span>
{:else}
<span>Count {node.valuesForCombo.length}</span>
<label> <label>
{#if widget.attrs.title !== ""} {#if widget.attrs.title !== ""}
<BlockTitle show_label={true}>{widget.attrs.title}</BlockTitle> <BlockTitle show_label={true}>{widget.attrs.title}</BlockTitle>
{/if} {/if}
<Select <Select
bind:value={option} value={$nodeValue}
items={node.properties.values} bind:justValue={option}
disabled={isDisabled(widget) || node.properties.values.length === 0} bind:listOpen
items={node.valuesForCombo}
disabled={isDisabled(widget)}
clearable={false} clearable={false}
showChevron={true} showChevron={true}
listAutoWidth={true}
inputAttributes={{ autocomplete: 'off' }} inputAttributes={{ autocomplete: 'off' }}
bind:input bind:input
on:change on:change
on:focus={onFocus} on:focus={onFocus}
on:select={onSelect} on:select={onSelect}
on:filter on:filter={onFilter}
on:blur on:blur
/> >
{#if debug} <div slot="list" class="list" let:filteredItems>
<div>Value: {option?.value}</div> {#key $rebuild}
<div>Items: {node.properties.values}</div> <div class="container" bind:this={virtualList}>
<div>NodeValue: {$nodeValue}</div> <VirtualList items={filteredItems} bind:start bind:end let:item>
<div>LinkValue: {getLinkValue()}</div> <div class="item"
{/if} class:selected={option === item.value}
on:click={() => selectItem(item)}>
{item.label}
</div>
</VirtualList>
<p class="details">showing items {start}-{end}</p>
</div>
{/key}
</div>
</Select>
</label> </label>
{/if} {/if}
{/key} {/if}
{/key} {/key}
</div> </div>
@@ -116,6 +151,12 @@
.wrapper { .wrapper {
padding: 2px; padding: 2px;
width: 100%; width: 100%;
:global(.selected-item) {
// no idea how to get the select box to shrink in the flexbox otherwise...
position: absolute !important;
width: -webkit-fill-available !important;
}
} }
@keyframes -global-light-up { @keyframes -global-light-up {
@@ -142,5 +183,43 @@
:global(.svelte-select-list) { :global(.svelte-select-list) {
z-index: var(--layer-top) !important; z-index: var(--layer-top) !important;
overflow-y: initial !important;
width: auto !important; // seems floating-ui overrides listAutoWidth
}
.container {
border-top: 1px solid #333;
border-bottom: 1px solid #333;
height: 100%
}
.list {
height: 30rem;
width: 30rem;
background-color: white;
.item {
font-size: 16px;
&.selected {
color: white;
background: var(--color-yellow-500);
}
}
.details {
background: white;
border: 1px solid grey;
}
:global(svelte-virtual-list-row) {
white-space: nowrap;
}
:global(svelte-virtual-list-row:hover) {
color: white;
background: var(--color-blue-500);
cursor: pointer;
}
} }
</style> </style>