Merge pull request #26 from space-nuko/container-variants

Container variants
This commit is contained in:
space-nuko
2023-05-06 19:58:10 -05:00
committed by GitHub
31 changed files with 5291 additions and 1890 deletions

View File

@@ -33,12 +33,14 @@
},
"type": "module",
"dependencies": {
"@gradio/accordion": "workspace:*",
"@gradio/atoms": "workspace:*",
"@gradio/button": "workspace:*",
"@gradio/client": "workspace:*",
"@gradio/form": "workspace:*",
"@gradio/gallery": "workspace:*",
"@gradio/icons": "workspace:*",
"@gradio/tabs": "workspace:*",
"@gradio/theme": "workspace:*",
"@gradio/upload": "workspace:*",
"@gradio/utils": "workspace:*",

6
pnpm-lock.yaml generated
View File

@@ -4,6 +4,9 @@ importers:
.:
dependencies:
'@gradio/accordion':
specifier: workspace:*
version: link:gradio/js/accordion
'@gradio/atoms':
specifier: workspace:*
version: link:gradio/js/atoms
@@ -22,6 +25,9 @@ importers:
'@gradio/icons':
specifier: workspace:*
version: link:gradio/js/icons
'@gradio/tabs':
specifier: workspace:*
version: link:gradio/js/tabs
'@gradio/theme':
specifier: workspace:*
version: link:gradio/js/theme

View File

@@ -50,6 +50,7 @@ export class ImageViewer {
showModal(event: Event) {
const source = (event.target || event.srcElement) as HTMLImageElement;
const galleryElem = source.closest<HTMLDivElement>("div.block")
console.debug("[ImageViewer] showModal", event, source, galleryElem);
if (!galleryElem || ImageViewer.all_gallery_buttons(galleryElem).length === 0) {
console.error("No buttons found on gallery element!", galleryElem)
return;

View File

@@ -0,0 +1,202 @@
<script lang="ts">
import { Block, BlockTitle } from "@gradio/atoms";
import { Accordion } from "@gradio/accordion";
import uiState from "$lib/stores/uiState";
import WidgetContainer from "./WidgetContainer.svelte"
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
import {fade} from 'svelte/transition';
// notice - fade in works fine but don't add svelte's fade-out (known issue)
import {cubicIn} from 'svelte/easing';
import { flip } from 'svelte/animate';
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { startDrag, stopDrag } from "$lib/utils"
import type { Writable } from "svelte/store";
export let container: ContainerLayout | null = null;
export let zIndex: number = 0;
export let classes: string[] = [];
export let showHandles: boolean = false;
export let edit: boolean = false;
export let dragDisabled: boolean = false;
let attrsChanged: Writable<boolean> | null = null;
let children: IDragItem[] | null = null;
const flipDurationMs = 100;
let selectedIndex: number = 0;
$: if (container) {
children = $layoutState.allItems[container.id].children;
attrsChanged = container.attrsChanged
}
else {
children = null;
attrsChanged = null
}
function handleConsider(evt: any) {
children = layoutState.updateChildren(container, evt.detail.items)
// console.log(dragItems);
};
function handleFinalize(evt: any) {
children = layoutState.updateChildren(container, evt.detail.items)
// Ensure dragging is stopped on drag finish
};
</script>
{#if container && children}
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}"
class:hide-block={container.attrs.containerVariant === "hidden"}
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(container.id)}
class:root-container={zIndex === 0}
class:is-executing={container.isNodeExecuting}
class:edit={edit}>
{#if edit}
<Block elem_classes={["gradio-accordion"]}>
<Accordion label={container.attrs.title} open={true}>
<div class="v-pane"
class:empty={children.length === 0}
class:edit={edit}
use:dndzone="{{
items: children,
flipDurationMs,
centreDraggedOnCursor: true,
morphDisabled: true,
dropFromOthersDisabled: zIndex === 0,
dragDisabled
}}"
on:consider="{handleConsider}"
on:finalize="{handleFinalize}"
>
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
{@const hidden = item?.attrs?.hidden}
<div class="animation-wrapper"
class:hidden={hidden}
animate:flip={{duration:flipDurationMs}}
style={item?.attrs?.flexGrow ? `flex-grow: ${item.attrs.flexGrow}` : ""}
>
<WidgetContainer dragItem={item} zIndex={zIndex+1} />
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
<div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/>
{/if}
</div>
{/each}
</div>
{#if container.attrs.hidden && edit}
<div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/>
{/if}
{#if showHandles}
<div class="handle handle-container" style="z-index: {zIndex+100}" data-drag-item-id={container.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>
{/if}
</Accordion>
</Block>
{:else}
<Block elem_classes={["gradio-accordion"]}>
<Accordion label={container.attrs.title} open={container.attrs.openOnStartup}>
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
<WidgetContainer dragItem={item} zIndex={zIndex+1} />
{/each}
</Accordion>
</Block>
{/if}
</div>
{/if}
<style lang="scss">
.container {
> :global(*) {
border-radius: 0;
}
:global(.v-pane > .block) {
height: fit-content;
}
.edit > :global(.v-pane > .block) {
border-color: var(--color-pink-500);
border-width: 2px;
border-style: dashed !important;
margin: 0.2em;
padding: 1.4em;
}
/* :global(.hide-block > .v-pane > .block) {
padding: 0.5em 0.25em;
box-shadow: unset;
border-width: 0;
border-color: unset;
border-radius: unset;
background: var(--block-background-fill);
width: 100%;
line-height: var(--line-sm);
} */
&.horizontal {
flex-wrap: wrap;
gap: var(--layout-gap);
width: var(--size-full);
> :global(.block > .v-pane) {
flex-direction: row;
}
> :global(*), > :global(.form > *) {
flex: 1 1 0%;
flex-wrap: wrap;
min-width: min(160px, 100%);
}
}
&.vertical {
position: relative;
> :global(.block > .v-pane) {
flex-direction: column;
}
> :global(*), > :global(.form > *), .v-pane {
width: var(--size-full);
}
}
}
:global(.label-wrap > span:not(.icon)) {
color: var(--block-title-text-color);
}
.handle {
cursor: grab;
z-index: 99999;
position: absolute;
right: 0;
top: 0;
width: 100%;
height: 100%;
}
.animation-wrapper {
position: relative;
flex-grow: 100;
}
.handle-widget:hover {
background-color: #add8e680;
}
.handle-container:hover {
background-color: #d8ade680;
}
.container.selected > :global(.block) {
background: var(--color-yellow-300);
}
.gradio-accordion {
.widget, .container {
padding: 5px;
}
}
</style>

View File

@@ -11,11 +11,15 @@
import { flip } from 'svelte/animate';
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { startDrag, stopDrag } from "$lib/utils"
import type { Writable } from "svelte/store";
export let container: ContainerLayout | null = null;
export let zIndex: number = 0;
export let classes: string[] = [];
export let showHandles: boolean = false;
export let edit: boolean = false;
export let dragDisabled: boolean = false;
let attrsChanged: Writable<boolean> | null = null;
let children: IDragItem[] | null = null;
const flipDurationMs = 100;
@@ -38,60 +42,59 @@
children = layoutState.updateChildren(container, evt.detail.items)
// Ensure dragging is stopped on drag finish
};
const tt = "asd\nasdlkj"
</script>
{#if container && children}
{@const edit = $uiState.uiEditMode === "widgets" && zIndex > 1}
{#key $attrsChanged}
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}"
class:hide-block={container.attrs.blockVariant === "hidden"}
class:selected={$uiState.uiEditMode !== "disabled" && $layoutState.currentSelection.includes(container.id)}
class:root-container={zIndex === 0}
class:is-executing={container.isNodeExecuting}
class:edit={edit}>
<Block>
{#if container.attrs.title !== ""}
<label for={String(container.id)} class={$uiState.uiEditMode === "widgets" ? "edit-title-label" : ""}>
<BlockTitle>{container.attrs.title}</BlockTitle>
</label>
{/if}
<div class="v-pane"
class:empty={children.length === 0}
class:edit={edit}
use:dndzone="{{
items: children,
flipDurationMs,
centreDraggedOnCursor: true,
morphDisabled: true,
dropFromOthersDisabled: zIndex === 0,
dragDisabled: zIndex === 0 || $layoutState.currentSelection.length > 2 || $uiState.uiEditMode === "disabled"
}}"
on:consider="{handleConsider}"
on:finalize="{handleFinalize}"
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}"
class:hide-block={container.attrs.containerVariant === "hidden"}
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(container.id)}
class:root-container={zIndex === 0}
class:is-executing={container.isNodeExecuting}
class:edit={edit}>
<Block>
{#if container.attrs.title && container.attrs.title !== ""}
<label for={String(container.id)} class={($uiState.uiUnlocked && $uiState.uiEditMode === "widgets") ? "edit-title-label" : ""}>
<BlockTitle>{container.attrs.title}</BlockTitle>
</label>
{/if}
<div class="v-pane"
class:empty={children.length === 0}
class:edit={edit}
use:dndzone="{{
items: children,
flipDurationMs,
centreDraggedOnCursor: true,
morphDisabled: true,
dropFromOthersDisabled: zIndex === 0,
dragDisabled
}}"
on:consider="{handleConsider}"
on:finalize="{handleFinalize}"
>
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
{@const hidden = item?.attrs?.hidden}
<div class="animation-wrapper"
class:hidden={hidden}
animate:flip={{duration:flipDurationMs}}
style={item?.attrs?.flexGrow ? `flex-grow: ${item.attrs.flexGrow}` : ""}
>
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
{@const hidden = item?.attrs?.hidden}
<div class="animation-wrapper"
class:hidden={hidden}
animate:flip={{duration:flipDurationMs}}
style={item?.attrs?.flexGrow ? `flex-grow: ${item.attrs.flexGrow}` : ""}
>
<WidgetContainer dragItem={item} zIndex={zIndex+1} />
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
<div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/>
{/if}
</div>
{/each}
</div>
{#if container.attrs.hidden && edit}
<div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/>
{/if}
{#if showHandles}
<div class="handle handle-container" style="z-index: {zIndex+100}" data-drag-item-id={container.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>
{/if}
</Block>
</div>
{/key}
<WidgetContainer dragItem={item} zIndex={zIndex+1} />
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
<div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/>
{/if}
</div>
{/each}
</div>
{#if container.attrs.hidden && edit}
<div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/>
{/if}
{#if showHandles}
<div class="handle handle-container" style="z-index: {zIndex+100}" data-drag-item-id={container.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>
{/if}
</Block>
</div>
{/if}
<style lang="scss">
@@ -158,7 +161,7 @@
}
}
:global(.block) {
> :global(.block) {
height: fit-content;
}
@@ -170,7 +173,7 @@
padding: 1.4em;
}
:global(.hide-block > .block) {
> :global(.hide-block > .block) {
padding: 0.5em 0.25em;
box-shadow: unset;
border-width: 0;
@@ -289,12 +292,6 @@
color: var(--input-placeholder-color);
}
.widget-edit-outline {
border: 2px dashed var(--color-blue-400);
margin: 0.2em;
padding: 0.2em;
}
.root-container > :global(.block) {
padding: 0px;
}

View File

@@ -19,6 +19,8 @@
import ComfyQueue from "./ComfyQueue.svelte";
import ComfyProperties from "./ComfyProperties.svelte";
import queueState from "$lib/stores/queueState";
import ComfyUnlockUIButton from "./ComfyUnlockUIButton.svelte";
import ComfyGraphView from "./ComfyGraphView.svelte";
export let app: ComfyApp = undefined;
let imageViewer: ImageViewer;
@@ -29,7 +31,7 @@
let containerElem: HTMLDivElement;
let resizeTimeout: NodeJS.Timeout | null;
let hasShownUIHelpToast: boolean = false;
let uiTheme: string = "anapnoe";
let uiTheme: string = "";
let debugLayout: boolean = false;
@@ -58,6 +60,7 @@
$layoutState.currentSelection = []
let graphSize = 0;
let graphTransitioning = false;
function toggleGraph() {
if (graphSize == 0) {
@@ -69,7 +72,7 @@
}
}
let propsSidebarSize = 0; //15;
let propsSidebarSize = 15; //15;
function toggleProps() {
if (propsSidebarSize == 0) {
@@ -120,11 +123,7 @@
}
}
function doRecenter(): void {
app.lCanvas.recenter();
}
$: if ($uiState.uiEditMode !== "disabled" && !hasShownUIHelpToast) {
$: if ($uiState.uiUnlocked && !hasShownUIHelpToast) {
hasShownUIHelpToast = true;
toast.push("Right-click to open context menu.")
}
@@ -144,11 +143,17 @@
}
$: if (containerElem) {
let wrappers = containerElem.querySelectorAll<HTMLDivElement>(".pane-wrapper")
for (const wrapper of wrappers) {
const paneNode = wrapper.parentNode as HTMLElement; // get the node inside the <Pane/>
paneNode.ontransitionend = () => {
app.resizeCanvas()
const canvas = containerElem.querySelector<HTMLDivElement>("#graph-canvas")
if (canvas) {
const paneNode = canvas.closest(".splitpanes__pane")
if (paneNode) {
(paneNode as HTMLElement).ontransitionstart = () => {
graphTransitioning = true
}
(paneNode as HTMLElement).ontransitionend = () => {
graphTransitioning = false
app.resizeCanvas()
}
}
}
}
@@ -158,7 +163,7 @@
(window as any).app = app;
(window as any).appPane = uiPane;
await import('../../scss/ux.scss');
// await import('../../scss/ux.scss');
refreshView();
})
@@ -189,9 +194,7 @@
<ComfyUIPane bind:this={uiPane} {app} />
</Pane>
<Pane bind:size={graphSize}>
<div class="canvas-wrapper pane-wrapper">
<canvas id="graph-canvas" />
</div>
<ComfyGraphView {app} transitioning={graphTransitioning} />
</Pane>
</Splitpanes>
</Pane>
@@ -203,50 +206,53 @@
</Splitpanes>
</div>
<div id="bottombar">
<Button variant="primary" on:click={queuePrompt}>
Queue Prompt
</Button>
<Button variant="secondary" on:click={toggleGraph}>
Toggle Graph
</Button>
<Button variant="secondary" on:click={toggleProps}>
Toggle Props
</Button>
<Button variant="secondary" on:click={toggleQueue}>
Toggle Queue
</Button>
<Button variant="secondary" on:click={doSave}>
Save
</Button>
<Button variant="secondary" on:click={doReset}>
Reset
</Button>
<Button variant="secondary" on:click={doLoadDefault}>
Load Default
</Button>
<Button variant="secondary" on:click={doRecenter}>
Recenter
</Button>
<Button variant="secondary" on:click={doRefreshCombos}>
🔄
</Button>
<!-- <Checkbox label="Lock Nodes" bind:value={$uiState.nodesLocked}/>
<Checkbox label="Disable Interaction" bind:value={$uiState.graphLocked}/> -->
<Checkbox label="Auto-Add UI" bind:value={$uiState.autoAddUI}/>
<label class="label" for="enable-ui-editing">
<BlockTitle>Enable UI Editing</BlockTitle>
<select id="enable-ui-editing" name="enable-ui-editing" bind:value={$uiState.uiEditMode}>
<option value="disabled">Disabled</option>
<option value="widgets">Widgets</option>
</select>
</label>
<label class="label" for="ui-theme">
<BlockTitle>Theme</BlockTitle>
<select id="ui-theme" name="ui-theme" bind:value={uiTheme}>
<option value="">None</option>
<option value="anapnoe">Anapnoe</option>
</select>
</label>
<div class="left">
<Button variant="primary" on:click={queuePrompt}>
Queue Prompt
</Button>
<Button variant="secondary" on:click={toggleGraph}>
Toggle Graph
</Button>
<Button variant="secondary" on:click={toggleProps}>
Toggle Props
</Button>
<Button variant="secondary" on:click={toggleQueue}>
Toggle Queue
</Button>
<Button variant="secondary" on:click={doSave}>
Save
</Button>
<Button variant="secondary" on:click={doReset}>
Reset
</Button>
<Button variant="secondary" on:click={doLoadDefault}>
Load Default
</Button>
<Button variant="secondary" on:click={doRefreshCombos}>
🔄
</Button>
<!-- <Checkbox label="Lock Nodes" bind:value={$uiState.nodesLocked}/>
<Checkbox label="Disable Interaction" bind:value={$uiState.graphLocked}/> -->
<span style="display: inline-flex !important">
<Checkbox label="Auto-Add UI" bind:value={$uiState.autoAddUI}/>
</span>
<span class="label" for="ui-edit-mode">
<BlockTitle>UI Edit mode</BlockTitle>
<select id="ui-edit-mode" name="ui-edit-mode" bind:value={$uiState.uiEditMode}>
<option value="widgets">Widgets</option>
</select>
</span>
<span class="label" for="ui-theme">
<BlockTitle>Theme</BlockTitle>
<select id="ui-theme" name="ui-theme" bind:value={uiTheme}>
<option value="">None</option>
<option value="anapnoe">Anapnoe</option>
</select>
</span>
</div>
<div class="right">
<ComfyUnlockUIButton bind:toggled={$uiState.uiUnlocked} />
</div>
</div>
<LightboxModal />
</div>
@@ -266,26 +272,24 @@
height: 100vh;
}
#comfy-ui {
}
#comfy-graph {
}
#graph-canvas {
}
#bottombar {
padding-top: 0.5em;
display: flex;
flex-wrap: wrap;
gap: var(--layout-gap);
margin: 10px;
}
.canvas-wrapper {
align-items: center;
width: 100%;
height: 100%;
background-color: #333;
gap: var(--layout-gap);
padding-left: 1em;
padding-right: 1em;
margin-top: auto;
overflow-x: auto;
> .left {
flex-shrink: 0;
}
> .right {
margin-left: auto
}
}
.sidebar-wrapper {
@@ -350,4 +354,8 @@
label.label > :global(span) {
top: 20%;
}
span.left {
right: 0px;
}
</style>

View File

@@ -106,6 +106,8 @@ export default class ComfyApp {
LiteGraph.release_link_on_empty_shows_menu = true;
LiteGraph.alt_drag_do_clone_nodes = true;
(window as any).LiteGraph = LiteGraph;
// await this.#invokeExtensionsAsync("init");
await this.registerNodes();
@@ -230,17 +232,17 @@ export default class ComfyApp {
}
private addDropHandler() {
this.dropZone = document.getElementById("dropzone");
// this.dropZone = document.getElementById("dropzone");
if (this.dropZone) {
window.addEventListener('dragenter', this.allowDrag.bind(this));
this.dropZone.addEventListener('dragover', this.allowDrag.bind(this));
this.dropZone.addEventListener('dragleave', this.hideDropZone.bind(this));
this.dropZone.addEventListener('drop', this.handleDrop.bind(this));
}
else {
console.warn("No dropzone detected (probably on mobile).")
}
// if (this.dropZone) {
// window.addEventListener('dragenter', this.allowDrag.bind(this));
// this.dropZone.addEventListener('dragover', this.allowDrag.bind(this));
// this.dropZone.addEventListener('dragleave', this.hideDropZone.bind(this));
// this.dropZone.addEventListener('drop', this.handleDrop.bind(this));
// }
// else {
// console.warn("No dropzone detected (probably on mobile).")
// }
}
/**
@@ -437,7 +439,7 @@ export default class ComfyApp {
const n = workflow.nodes.find((n) => n.id === node_.id);
if (!node_.isBackendNode) {
console.debug("Not serializing node: ", node_.type)
// console.debug("Not serializing node: ", node_.type)
continue;
}
@@ -562,8 +564,8 @@ export default class ComfyApp {
}
}
console.warn({ workflow, output })
console.warn(promptToGraphVis({ workflow, output }))
// console.debug({ workflow, output })
// console.debug(promptToGraphVis({ workflow, output }))
return { workflow, output };
}
@@ -588,7 +590,7 @@ export default class ComfyApp {
for (let i = 0; i < batchCount; i++) {
for (const node of this.lGraph._nodes_in_order) {
if ("beforeQueued" in node) {
(node as ComfyGraphNode).beforeQueued();
(node as ComfyGraphNode).beforeQueued(tag);
}
}
@@ -611,7 +613,7 @@ export default class ComfyApp {
for (const n of p.workflow.nodes) {
const node = this.lGraph.getNodeById(n.id);
if ("afterQueued" in node) {
(node as ComfyGraphNode).afterQueued(p);
(node as ComfyGraphNode).afterQueued(p, tag);
}
}

View File

@@ -5,6 +5,7 @@
export let value: string = "";
export let values: string[] = [""];
export let name: string = "";
export let disabled: boolean = false;
let value_: string = ""
$: value;
@@ -27,7 +28,7 @@
<label class="select-wrapper">
<BlockTitle>{name}</BlockTitle>
<div class="select">
<select on:blur bind:value>
<select on:blur bind:value {disabled}>
{#each values as value}
<option {value}>
{value}
@@ -53,4 +54,8 @@
.select-title {
padding: 0.2rem;
}
input:disabled {
cursor: not-allowed;
}
</style>

View File

@@ -0,0 +1,76 @@
<script lang="ts">
import { Button } from "@gradio/button";
import type ComfyApp from "./ComfyApp";
export let app: ComfyApp;
export let transitioning: boolean = false;
function doRecenter(): void {
app?.lCanvas?.recenter();
}
</script>
<div class="wrapper">
<div class="canvas-wrapper pane-wrapper">
<canvas id="graph-canvas" />
</div>
<div class="bar">
{#if !transitioning}
<span class="left">
<button on:click={doRecenter}>Recenter</button>
</span>
{/if}
</div>
</div>
<style lang="scss">
$bar-height: 3em;
.wrapper {
width: 100%;
height: 100%;
}
.canvas-wrapper {
width: 100%;
height: calc(100% - $bar-height);
background-color: #333;
}
.bar {
padding: 0.2em 0.5em;
display: flex;
align-items: center;
width: 100%;
height: $bar-height;
background-color: #3A3A3A;
border: 2px solid #2A2A2A;
gap: var(--layout-gap);
margin-top: auto;
overflow-x: auto;
> .left {
flex-shrink: 0;
}
> .right {
margin-left: auto
}
button {
color: #EEE;
border: 1px solid #888;
padding: 2px 5px;
background-color: #666;
&:hover {
background-color: #999;
border-color: #AAA;
}
&:active {
background-color: #555;
border-color: #777;
}
}
}
</style>

View File

@@ -3,8 +3,11 @@
import { createEventDispatcher } from "svelte";
export let value: number = 0;
export let min: number = -1024
export let max: number = 1024
export let step: number = 1;
export let name: string = "";
export let disabled: boolean = false;
let value_: number = 0;
$: value;
@@ -26,7 +29,7 @@
<label class="number-wrapper">
<BlockTitle>{name}</BlockTitle>
<div class="number">
<input type="number" bind:value {step}>
<input type="number" bind:value {min} {max} {step} {disabled}>
</div>
</label>
@@ -42,4 +45,7 @@
}
}
}
input[disabled] {
cursor: not-allowed;
}
</style>

View File

@@ -1,310 +1,447 @@
<script lang="ts">
import { Block, BlockTitle } from "@gradio/atoms";
import { TextBox, Checkbox } from "@gradio/form";
import { LGraphNode } from "@litegraph-ts/core"
import layoutState, { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec } from "$lib/stores/layoutState"
import { get } from "svelte/store"
import type { ComfyWidgetNode } from "$lib/nodes";
import ComfyNumberProperty from "./ComfyNumberProperty.svelte";
import ComfyComboProperty from "./ComfyComboProperty.svelte";
let target: IDragItem | null = null;
let node: LGraphNode | null = null;
$: if ($layoutState.currentSelection.length > 0) {
const targetId = $layoutState.currentSelection.slice(-1)[0]
target = $layoutState.allItems[targetId].dragItem
if (target.type === "widget") {
node = (target as WidgetLayout).node
}
else {
node = null;
}
}
else if ($layoutState.currentSelectionNodes.length > 0) {
target = null;
node = $layoutState.currentSelectionNodes[0]
}
else {
target = null
node = null;
}
let targetType: string = "???"
$: {
if (node != null)
targetType = node.type || "Node"
else if (target)
targetType = "Group"
else
targetType = ""
}
function validNodeProperty(spec: AttributesSpec, node: LGraphNode): boolean {
if (spec.validNodeTypes) {
return spec.validNodeTypes.indexOf(node.type) !== -1;
}
return spec.name in node.properties
}
function updateAttribute(entry: AttributesSpec, target: IDragItem, value: any) {
if (target) {
const name = entry.name
console.warn("updateAttribute", name, value)
target.attrs[name] = value
target.attrsChanged.set(!get(target.attrsChanged))
if (node && "propsChanged" in node) {
const comfyNode = node as ComfyWidgetNode
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
}
}
}
function updateProperty(entry: AttributesSpec, value: any) {
if (node) {
const name = entry.name
console.warn("updateProperty", name, value)
node.properties[name] = value;
if ("propsChanged" in node) {
const comfyNode = node as ComfyWidgetNode
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
}
}
}
function getVar(node: LGraphNode, entry: AttributesSpec) {
let value = node[entry.name]
if (entry.serialize)
value = entry.serialize(value)
console.debug("[ComfyProperties] getVar", entry, value, node)
return value
}
function updateVar(entry: any, value: any) {
if (node) {
const name = entry.name
console.warn("updateProperty", name, value)
if (entry.deserialize)
value = entry.deserialize(value)
console.debug("[ComfyProperties] updateVar", entry, value, name, node)
node[name] = value;
if ("propsChanged" in node) {
const comfyNode = node as ComfyWidgetNode
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
}
}
}
function updateWorkflowAttribute(entry: AttributesSpec, value: any) {
const name = entry.name
console.warn("updateWorkflowAttribute", name, value)
$layoutState.attrs[name] = value
$layoutState = $layoutState
}
</script>
<div class="props">
<div class="top">
<div class="target-name">
<span>
<span class="title">{target?.attrs?.title || node?.title || "Workflow"}<span>
{#if targetType !== ""}
<span class="type">({targetType})</span>
{/if}
</span>
</div>
</div>
<div class="props-entries">
{#each ALL_ATTRIBUTES as category(category.categoryName)}
<div class="category-name">
<span>
<span class="title">{category.categoryName}</span>
</span>
</div>
{#each category.specs as spec(spec.name)}
{#if spec.location === "widget" && target && spec.name in target.attrs}
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={target.attrs[spec.name]}
on:change={(e) => updateAttribute(spec, target, e.detail)}
on:input={(e) => updateAttribute(spec, target, e.detail)}
label={spec.name}
max_lines={1}
/>
{:else if spec.type === "boolean"}
<Checkbox
value={target.attrs[spec.name]}
on:change={(e) => updateAttribute(spec, target, e.detail)}
label={spec.name}
/>
{:else if spec.type === "number"}
<ComfyNumberProperty
name={spec.name}
value={target.attrs[spec.name]}
step={1}
on:change={(e) => updateAttribute(spec, target, e.detail)}
/>
{:else if spec.type === "enum"}
<ComfyComboProperty
name={spec.name}
value={target.attrs[spec.name]}
values={spec.values}
on:change={(e) => updateAttribute(spec, target, e.detail)}
/>
{/if}
</div>
{:else if node}
{#if spec.location === "nodeProps" && validNodeProperty(spec, node)}
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={node.properties[spec.name]}
on:change={(e) => updateProperty(spec, e.detail)}
on:input={(e) => updateProperty(spec, e.detail)}
label={spec.name}
max_lines={1}
/>
{:else if spec.type === "boolean"}
<Checkbox
value={node.properties[spec.name]}
label={spec.name}
on:change={(e) => updateProperty(spec, e.detail)}
/>
{:else if spec.type === "number"}
<ComfyNumberProperty
name={spec.name}
value={node.properties[spec.name]}
step={1}
on:change={(e) => updateProperty(spec, e.detail)}
/>
{:else if spec.type === "enum"}
<ComfyComboProperty
name={spec.name}
value={node.properties[spec.name]}
values={spec.values}
on:change={(e) => updateProperty(spec, e.detail)}
/>
{/if}
</div>
{:else if spec.location === "nodeVars" && spec.name in node}
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={getVar(node, spec)}
on:change={(e) => updateVar(spec, e.detail)}
on:input={(e) => updateVar(spec, e.detail)}
label={spec.name}
max_lines={1}
/>
{:else if spec.type === "boolean"}
<Checkbox
value={getVar(node, spec)}
on:change={(e) => updateVar(spec, e.detail)}
label={spec.name}
/>
{:else if spec.type === "number"}
<ComfyNumberProperty
name={spec.name}
value={getVar(node, spec)}
step={1}
on:change={(e) => updateVar(spec, e.detail)}
/>
{:else if spec.type === "enum"}
<ComfyComboProperty
name={spec.name}
value={getVar(node, spec)}
values={spec.values}
on:change={(e) => updateVar(spec, e.detail)}
/>
{/if}
</div>
{/if}
{:else if spec.location === "workflow" && spec.name in $layoutState.attrs}
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={$layoutState.attrs[spec.name]}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
on:input={(e) => updateWorkflowAttribute(spec, e.detail)}
label={spec.name}
max_lines={1}
/>
{:else if spec.type === "boolean"}
<Checkbox
value={$layoutState.attrs[spec.name]}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
label={spec.name}
/>
{:else if spec.type === "number"}
<ComfyNumberProperty
name={spec.name}
value={$layoutState.attrs[spec.name]}
step={1}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
/>
{:else if spec.type === "enum"}
<ComfyComboProperty
name={spec.name}
value={$layoutState.attrs[spec.name]}
values={spec.values}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
/>
{/if}
</div>
{/if}
{/each}
{/each}
</div>
</div>
<style lang="scss">
.props-entry {
padding-bottom: 0.5rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
display: flex;
flex-direction: row;
}
.target-name {
border-color: var(--neutral-400);
background: var(--neutral-300);
padding: 0.8rem 1.0rem;
.title {
font-weight: bold;
}
}
.category-name {
padding: 0.4rem 1.0rem;
border-color: var(--neutral-300);
background: var(--neutral-200);
}
.target-name, .category-name {
border-width: var(--block-border-width);
.type {
color: var(--neutral-500);
}
}
.bottom {
/* width: 100%;
height: auto;
position: absolute;
bottom: 0;
padding: 0.5em; */
}
</style>
<script lang="ts">
import { Block, BlockTitle } from "@gradio/atoms";
import { TextBox, Checkbox } from "@gradio/form";
import { LGraphNode } from "@litegraph-ts/core"
import layoutState, { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec } from "$lib/stores/layoutState"
import uiState from "$lib/stores/uiState"
import { get, type Writable, writable } from "svelte/store"
import type { ComfyWidgetNode } from "$lib/nodes";
import ComfyNumberProperty from "./ComfyNumberProperty.svelte";
import ComfyComboProperty from "./ComfyComboProperty.svelte";
let target: IDragItem | null = null;
let node: LGraphNode | null = null;
let attrsChanged: Writable<boolean> | null = null;
let refreshPanel: Writable<number> = writable(0);
$: if ($layoutState.currentSelection.length > 0) {
const targetId = $layoutState.currentSelection.slice(-1)[0]
target = $layoutState.allItems[targetId].dragItem
attrsChanged = target.attrsChanged;
if (target.type === "widget") {
node = (target as WidgetLayout).node
}
else {
node = null;
}
}
else if ($layoutState.currentSelectionNodes.length > 0) {
target = null;
node = $layoutState.currentSelectionNodes[0]
attrsChanged = null;
}
else {
target = null
node = null;
attrsChanged = null;
}
$: if (target) {
for (const cat of Object.values(ALL_ATTRIBUTES)) {
for (const spec of Object.values(cat.specs)) {
if (spec.location === "widget" && target.attrs[spec.name] == null) {
if (!spec.editable)
continue;
if (spec.canShow && !spec.canShow(target))
continue;
console.warn("Set default widget attr", spec.name, spec.defaultValue, target)
let value = spec.defaultValue;
target.attrs[spec.name] = value;
if (spec.refreshPanelOnChange)
$refreshPanel += 1;
}
}
}
}
let targetType: string = "???"
$: {
if (node != null)
targetType = node.type || "Node"
else if (target)
targetType = "Group"
else
targetType = ""
}
function validNodeProperty(spec: AttributesSpec, node: LGraphNode | null): boolean {
if (node == null || spec.location !== "nodeProps")
return false;
if (spec.canShow && !spec.canShow(node))
return false;
if (spec.validNodeTypes) {
return spec.validNodeTypes.indexOf(node.type) !== -1;
}
return spec.name in node.properties
}
function validNodeVar(spec: AttributesSpec, node: LGraphNode | null): boolean {
if (node == null || spec.location !== "nodeVars")
return false;
if (spec.canShow && !spec.canShow(node))
return false;
if (spec.validNodeTypes) {
return spec.validNodeTypes.indexOf(node.type) !== -1;
}
return spec.name in node
}
function validWidgetAttribute(spec: AttributesSpec, widget: IDragItem | null): boolean {
if (widget == null || spec.location !== "widget")
return false;
if (spec.canShow)
return spec.canShow(widget);
if (spec.validNodeTypes) {
if (widget.type === "widget") {
const node = (widget as WidgetLayout).node
if (!node)
return false;
return spec.validNodeTypes.indexOf(node.type) !== -1;
}
else if (widget.type === "container") {
return false;
}
}
return spec.name in widget.attrs
}
function validWorkflowAttribute(spec: AttributesSpec): boolean {
if (spec.location !== "workflow")
return false;
return spec.name in $layoutState.attrs
}
function getAttribute(target: IDragItem, spec: AttributesSpec): any {
let value = target.attrs[spec.name]
if (value == null)
value = spec.defaultValue
else if (spec.serialize)
value = spec.serialize(value)
console.debug("[ComfyProperties] getAttribute", spec.name, value, target, spec)
return value
}
function updateAttribute(spec: AttributesSpec, target: IDragItem | null, value: any) {
if (target == null || !spec.editable)
return;
const name = spec.name
console.debug("[ComfyProperties] updateAttribute", spec, value, name, node)
if (spec.deserialize)
value = spec.deserialize(value)
target.attrs[name] = value
target.attrsChanged.set(!get(target.attrsChanged))
if (node && "propsChanged" in node) {
const comfyNode = node as ComfyWidgetNode
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
}
console.warn(spec)
if (spec.refreshPanelOnChange) {
console.error("A! refresh")
$refreshPanel += 1;
}
}
function updateProperty(spec: AttributesSpec, value: any) {
if (node == null || !spec.editable)
return
const name = spec.name
console.warn("updateProperty", name, value)
node.properties[name] = value;
if ("propsChanged" in node) {
const comfyNode = node as ComfyWidgetNode
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
}
if (spec.refreshPanelOnChange)
$refreshPanel += 1;
}
function getVar(node: LGraphNode, spec: AttributesSpec) {
let value = node[spec.name]
if (value == null)
value = spec.defaultValue
else if (spec.serialize)
value = spec.serialize(value)
console.debug("[ComfyProperties] getVar", spec, value, node)
return value
}
function updateVar(spec: AttributesSpec, value: any) {
if (node == null || !spec.editable)
return;
const name = spec.name
console.debug("[ComfyProperties] updateVar", spec, value, name, node)
if (spec.deserialize)
value = spec.deserialize(value)
node[name] = value;
if ("propsChanged" in node) {
const comfyNode = node as ComfyWidgetNode
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
}
if (spec.refreshPanelOnChange)
$refreshPanel += 1;
}
function updateWorkflowAttribute(spec: AttributesSpec, value: any) {
if (!spec.editable)
return;
const name = spec.name
console.warn("updateWorkflowAttribute", name, value)
$layoutState.attrs[name] = value
$layoutState = $layoutState
}
</script>
<div class="props">
<div class="top">
<div class="target-name">
<span>
<span class="title">{target?.attrs?.title || node?.title || "Workflow"}<span>
{#if targetType !== ""}
<span class="type">({targetType})</span>
{/if}
</span>
</span>
</div>
</div>
<div class="props-entries">
{#key $refreshPanel}
{#each ALL_ATTRIBUTES as category(category.categoryName)}
<div class="category-name">
<span>
<span class="title">{category.categoryName}</span>
</span>
</div>
{#each category.specs as spec(spec.id)}
{#if validWidgetAttribute(spec, target)}
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={getAttribute(target, spec)}
on:change={(e) => updateAttribute(spec, target, e.detail)}
on:input={(e) => updateAttribute(spec, target, e.detail)}
disabled={!$uiState.uiUnlocked || !spec.editable}
label={spec.name}
max_lines={1}
/>
{:else if spec.type === "boolean"}
<Checkbox
value={getAttribute(target, spec)}
on:change={(e) => updateAttribute(spec, target, e.detail)}
disabled={!$uiState.uiUnlocked || !spec.editable}
label={spec.name}
/>
{:else if spec.type === "number"}
<ComfyNumberProperty
name={spec.name}
value={getAttribute(target, spec)}
step={spec.step || 1}
min={spec.min || -1024}
max={spec.max || 1024}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateAttribute(spec, target, e.detail)}
/>
{:else if spec.type === "enum"}
<ComfyComboProperty
name={spec.name}
value={getAttribute(target, spec)}
values={spec.values}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateAttribute(spec, target, e.detail)}
/>
{/if}
</div>
{:else if node}
{#if validNodeProperty(spec, node)}
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={node.properties[spec.name] || spec.defaultValue}
on:change={(e) => updateProperty(spec, e.detail)}
on:input={(e) => updateProperty(spec, e.detail)}
label={spec.name}
disabled={!$uiState.uiUnlocked || !spec.editable}
max_lines={1}
/>
{:else if spec.type === "boolean"}
<Checkbox
value={node.properties[spec.name] || spec.defaultValue}
label={spec.name}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateProperty(spec, e.detail)}
/>
{:else if spec.type === "number"}
<ComfyNumberProperty
name={spec.name}
value={node.properties[spec.name] || spec.defaultValue}
step={spec.step || 1}
min={spec.min || -1024}
max={spec.max || 1024}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateProperty(spec, e.detail)}
/>
{:else if spec.type === "enum"}
<ComfyComboProperty
name={spec.name}
value={node.properties[spec.name] || spec.defaultValue}
values={spec.values}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateProperty(spec, e.detail)}
/>
{/if}
</div>
{:else if validNodeVar(spec, node)}
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={getVar(node, spec)}
on:change={(e) => updateVar(spec, e.detail)}
on:input={(e) => updateVar(spec, e.detail)}
label={spec.name}
disabled={!$uiState.uiUnlocked || !spec.editable}
max_lines={1}
/>
{:else if spec.type === "boolean"}
<Checkbox
value={getVar(node, spec)}
on:change={(e) => updateVar(spec, e.detail)}
disabled={!$uiState.uiUnlocked || !spec.editable}
label={spec.name}
/>
{:else if spec.type === "number"}
<ComfyNumberProperty
name={spec.name}
value={getVar(node, spec)}
step={spec.step || 1}
min={spec.min || -1024}
max={spec.max || 1024}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateVar(spec, e.detail)}
/>
{:else if spec.type === "enum"}
<ComfyComboProperty
name={spec.name}
value={getVar(node, spec)}
values={spec.values}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateVar(spec, e.detail)}
/>
{/if}
</div>
{/if}
{:else if !node && !target && validWorkflowAttribute(spec)}
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={$layoutState.attrs[spec.name] || spec.defaultValue}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
on:input={(e) => updateWorkflowAttribute(spec, e.detail)}
label={spec.name}
disabled={!$uiState.uiUnlocked || !spec.editable}
max_lines={1}
/>
{:else if spec.type === "boolean"}
<Checkbox
value={$layoutState.attrs[spec.name] || spec.defaultValue}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
disabled={!$uiState.uiUnlocked || !spec.editable}
label={spec.name}
/>
{:else if spec.type === "number"}
<ComfyNumberProperty
name={spec.name}
value={$layoutState.attrs[spec.name] || spec.defaultValue}
step={spec.step || 1}
min={spec.min || -1024}
max={spec.max || 1024}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
/>
{:else if spec.type === "enum"}
<ComfyComboProperty
name={spec.name}
value={$layoutState.attrs[spec.name] || spec.defaultValue}
values={spec.values}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
/>
{/if}
</div>
{/if}
{/each}
{/each}
{/key}
</div>
</div>
<style lang="scss">
.props-entry {
padding-bottom: 0.5rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
display: flex;
flex-direction: row;
}
.target-name {
border-color: var(--neutral-400);
background: var(--neutral-300);
padding: 0.8rem 1.0rem;
.title {
font-weight: bold;
.type {
padding-left: 0.25rem;
font-weight: normal;
}
}
}
.category-name {
padding: 0.4rem 1.0rem;
border-color: var(--neutral-300);
background: var(--neutral-200);
}
.target-name, .category-name {
border-width: var(--block-border-width);
.type {
color: var(--neutral-500);
}
}
.bottom {
/* width: 100%;
height: auto;
position: absolute;
bottom: 0;
padding: 0.5em; */
}

View File

@@ -50,7 +50,7 @@
function ungroup() {
const item = layoutState.getCurrentSelection()[0]
if (item.type !== "container")
if (!item || item.type !== "container")
return;
$layoutState.currentSelection = []
@@ -69,7 +69,7 @@
}
async function onRightClick(e) {
if ($uiState.uiEditMode === "disabled")
if (!$uiState.uiUnlocked)
return;
e.preventDefault();

View File

@@ -0,0 +1,46 @@
<script lang="ts">
import { Button } from "@gradio/button"
import { LockOpen2, LockClosed } from "radix-icons-svelte"
export let toggled: boolean = false;
function toggle() {
toggled = !toggled;
}
</script>
<div class="comfy-toggle-button" class:toggled>
<Button on:click={toggle} variant={toggled ? "primary" : "secondary"}>
{#if toggled}
<LockOpen2 />
{:else}
<LockClosed />
{/if}
</Button>
</div>
<style lang="scss">
.comfy-toggle-button {
display: inline-flex;
width: var(--size-12);
height: var(--size-12);
> :global(.secondary.lg) {
border: var(--button-border-width) solid var(--neutral-400);
}
> :global(.primary.lg) {
border: var(--button-border-width) solid var(--primary-400);
}
&.toggled {
:global(svg) {
color: var(--button-primary-text-color);
}
}
:global(svg) {
color: var(--button-secondary-text-color);
}
}
</style>

View File

@@ -0,0 +1,44 @@
<script lang="ts">
import { Block, BlockTitle } from "@gradio/atoms";
import uiState from "$lib/stores/uiState";
import WidgetContainer from "./WidgetContainer.svelte"
import BlockContainer from "./BlockContainer.svelte"
import AccordionContainer from "./AccordionContainer.svelte"
import TabsContainer from "./TabsContainer.svelte"
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
import {fade} from 'svelte/transition';
// notice - fade in works fine but don't add svelte's fade-out (known issue)
import {cubicIn} from 'svelte/easing';
import { flip } from 'svelte/animate';
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { startDrag, stopDrag } from "$lib/utils"
export let container: ContainerLayout | null = null;
export let zIndex: number = 0;
export let classes: string[] = [];
export let showHandles: boolean = false;
let attrsChanged: Writable<boolean> | null = null;
$: if (container) {
attrsChanged = container.attrsChanged
}
else {
attrsChanged = null
}
</script>
{#if container}
{@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets" && zIndex > 1}
{@const dragDisabled = zIndex === 0 || $layoutState.currentSelection.length > 2 || !$uiState.uiUnlocked}
{#key $attrsChanged}
{#if container.attrs.variant === "tabs"}
<TabsContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} />
{:else if container.attrs.variant === "accordion"}
<AccordionContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} />
{:else}
<BlockContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} />
{/if}
{/key}
{/if}

View File

@@ -0,0 +1,212 @@
<script lang="ts">
import { Block, BlockTitle } from "@gradio/atoms";
import { Tabs, TabItem } from "@gradio/tabs";
import uiState from "$lib/stores/uiState";
import WidgetContainer from "./WidgetContainer.svelte"
import { dndzone, SHADOW_ITEM_MARKER_PROPERTY_NAME, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
import {fade} from 'svelte/transition';
// notice - fade in works fine but don't add svelte's fade-out (known issue)
import {cubicIn} from 'svelte/easing';
import { flip } from 'svelte/animate';
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { startDrag, stopDrag } from "$lib/utils"
import type { Writable } from "svelte/store";
export let container: ContainerLayout | null = null;
export let zIndex: number = 0;
export let classes: string[] = [];
export let showHandles: boolean = false;
export let edit: boolean = false;
export let dragDisabled: boolean = false;
let attrsChanged: Writable<boolean> | null = null;
let children: IDragItem[] | null = null;
const flipDurationMs = 100;
let selectedIndex: number = 0;
$: if (container) {
children = $layoutState.allItems[container.id].children;
attrsChanged = container.attrsChanged
}
else {
children = null;
attrsChanged = null
}
function handleConsider(evt: any) {
children = layoutState.updateChildren(container, evt.detail.items)
// console.log(dragItems);
};
function handleFinalize(evt: any) {
children = layoutState.updateChildren(container, evt.detail.items)
// Ensure dragging is stopped on drag finish
};
function getTabName(container: ContainerLayout, i: number): string {
const title = container.attrs.title
if (!title)
return `Tab ${i+1}`
const tabNames = title.split(",").map(s => s.trim());
const tabName = tabNames[i]
if (tabName == null || tabName === "")
return `Tab ${i+1}`
return tabName
}
</script>
{#if container && children}
<div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}"
class:hide-block={container.attrs.containerVariant === "hidden"}
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(container.id)}
class:root-container={zIndex === 0}
class:is-executing={container.isNodeExecuting}
class:edit={edit}>
{#if edit}
<Block>
<div class="v-pane"
class:empty={children.length === 0}
class:edit={edit}
use:dndzone="{{
items: children,
flipDurationMs,
centreDraggedOnCursor: true,
morphDisabled: true,
dropFromOthersDisabled: zIndex === 0,
dragDisabled
}}"
on:consider="{handleConsider}"
on:finalize="{handleFinalize}"
>
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item, i(item.id)}
{@const hidden = item?.attrs?.hidden}
{@const tabName = getTabName(container, i)}
<div class="animation-wrapper"
class:hidden={hidden}
animate:flip={{duration:flipDurationMs}}
style={item?.attrs?.flexGrow ? `flex-grow: ${item.attrs.flexGrow}` : ""}>
<Block>
<label for={String(item.id)}>
<BlockTitle><strong>Tab {i+1}:</strong> {tabName}</BlockTitle>
</label>
<WidgetContainer dragItem={item} zIndex={zIndex+1} />
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
<div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/>
{/if}
</Block>
</div>
{/each}
</div>
{#if container.attrs.hidden && edit}
<div class="handle handle-hidden" class:hidden={!edit} style="z-index: {zIndex+100}"/>
{/if}
{#if showHandles}
<div class="handle handle-container" style="z-index: {zIndex+100}" data-drag-item-id={container.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>
{/if}
</Block>
{:else}
<Tabs elem_classes={["gradio-tabs"]}>
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item, i(item.id)}
{@const tabName = getTabName(container, i)}
<TabItem name={tabName} on:select={() => console.log("tab " + i)}>
<WidgetContainer dragItem={item} zIndex={zIndex+1} />
</TabItem>
{/each}
</Tabs>
{/if}
</div>
{/if}
<style lang="scss">
.container {
display: flex;
> :global(*) {
border-radius: 0;
}
:global(.v-pane > .block) {
height: fit-content;
}
.edit > :global(.v-pane > .block) {
border-color: var(--color-pink-500);
border-width: 2px;
border-style: dashed !important;
margin: 0.2em;
padding: 1.4em;
}
/* :global(.hide-block > .v-pane > .block) {
padding: 0.5em 0.25em;
box-shadow: unset;
border-width: 0;
border-color: unset;
border-radius: unset;
background: var(--block-background-fill);
width: 100%;
line-height: var(--line-sm);
} */
&.horizontal {
flex-wrap: wrap;
gap: var(--layout-gap);
width: var(--size-full);
> :global(.block > .v-pane) {
flex-direction: row;
}
> :global(*), > :global(.form > *) {
flex: 1 1 0%;
flex-wrap: wrap;
min-width: min(160px, 100%);
}
}
&.vertical {
position: relative;
> :global(.block > .v-pane) {
flex-direction: column;
}
> :global(*), > :global(.form > *), .v-pane {
width: var(--size-full);
}
}
}
.handle {
cursor: grab;
z-index: 99999;
position: absolute;
right: 0;
top: 0;
width: 100%;
height: 100%;
}
.animation-wrapper {
position: relative;
flex-grow: 100;
}
.handle-widget:hover {
background-color: #add8e680;
}
.handle-container:hover {
background-color: #d8ade680;
}
.container.selected > :global(.block) {
background: var(--color-yellow-300);
}
</style>

View File

@@ -4,7 +4,7 @@
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { startDrag, stopDrag } from "$lib/utils"
import BlockContainer from "./BlockContainer.svelte"
import Container from "./Container.svelte"
import { type Writable } from "svelte/store"
import type { ComfyWidgetNode } from "$lib/nodes";
@@ -40,7 +40,8 @@
propsChanged = null;
}
$: showHandles = $uiState.uiEditMode === "widgets" // TODO
$: showHandles = $uiState.uiUnlocked
&& $uiState.uiEditMode === "widgets" // TODO
&& zIndex > 1
&& !$layoutState.isMenuOpen
@@ -58,15 +59,15 @@
{#if container}
{#key $attrsChanged}
<BlockContainer {container} {classes} {zIndex} {showHandles} />
<Container {container} {classes} {zIndex} {showHandles} />
{/key}
{:else if widget && widget.node}
{@const edit = $uiState.uiEditMode === "widgets" && zIndex > 1}
{@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets" && zIndex > 1}
{#key $attrsChanged}
{#key $propsChanged}
<div class="widget {widget.attrs.classes} {getWidgetClass()}"
class:edit={edit}
class:selected={$uiState.uiEditMode !== "disabled" && $layoutState.currentSelection.includes(widget.id)}
class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(widget.id)}
class:is-executing={$queueState.runningNodeId && $queueState.runningNodeId == widget.node.id}
class:hidden={widget.attrs.hidden}
>
@@ -83,8 +84,12 @@
{/if}
<style lang="scss">
.widget.selected {
background: var(--color-yellow-200);
.widget {
height: 100%;
&.selected {
background: var(--color-yellow-200);
}
}
.container.selected {
background: var(--color-yellow-400);

File diff suppressed because it is too large Load Diff

View File

@@ -4,14 +4,14 @@ import { Watch } from "@litegraph-ts/nodes-basic";
import type { SerializedPrompt } from "$lib/components/ComfyApp";
import { toast } from '@zerodevx/svelte-toast'
import type { GalleryOutput } from "./ComfyWidgetNodes";
import { get } from "svelte/store";
import queueState from "$lib/stores/queueState";
export interface ComfyQueueEventsProperties extends Record<any, any> {
prompt: SerializedPrompt | null
}
export class ComfyQueueEvents extends ComfyGraphNode {
override properties: ComfyQueueEventsProperties = {
prompt: null
}
static slotLayout: SlotLayout = {
@@ -22,29 +22,29 @@ export class ComfyQueueEvents extends ComfyGraphNode {
],
}
override onPropertyChanged(property: string, value: any, prevValue?: any) {
if (property === "value") {
this.setOutputData(2, this.properties.prompt)
private getActionParams(subgraph: string | null): any {
let queue = get(queueState)
let remaining = 0;
if (typeof queue.queueRemaining === "number")
remaining = queue.queueRemaining
return {
queueRemaining: remaining,
subgraph
}
}
override onExecute() {
this.setOutputData(2, this.properties.prompt)
override beforeQueued(subgraph: string | null) {
this.triggerSlot(0, this.getActionParams(subgraph))
}
override beforeQueued() {
this.setProperty("value", null)
this.triggerSlot(0, "bang")
}
override afterQueued(p: SerializedPrompt) {
this.setProperty("value", p)
this.triggerSlot(1, "bang")
override afterQueued(p: SerializedPrompt, subgraph: string | null) {
this.triggerSlot(1, this.getActionParams(subgraph))
}
override onSerialize(o: SerializedLGraphNode) {
super.onSerialize(o)
o.properties = { prompt: null }
}
}
@@ -112,7 +112,7 @@ export class ComfyCopyAction extends ComfyGraphNode {
{ name: "copy", type: BuiltInSlotType.ACTION }
],
outputs: [
{ name: "out", type: "*" }
{ name: "out", type: BuiltInSlotType.EVENT }
],
}
@@ -130,13 +130,15 @@ export class ComfyCopyAction extends ComfyGraphNode {
}
override onExecute() {
this.setProperty("value", this.getInputData(0))
if (this.getInputLink(0))
this.setProperty("value", this.getInputData(0))
}
override onAction(action: any, param: any) {
this.setProperty("value", this.getInputData(0))
this.setOutputData(0, this.properties.value)
console.log("setData", this.properties.value)
if (action === "copy") {
this.setProperty("value", this.getInputData(0))
this.triggerSlot(0, this.properties.value)
}
};
}

View File

@@ -20,8 +20,8 @@ export type DefaultWidgetLayout = {
export default class ComfyGraphNode extends LGraphNode {
isBackendNode?: boolean;
beforeQueued?(): void;
afterQueued?(prompt: SerializedPrompt): void;
beforeQueued?(subgraph: string | null): void;
afterQueued?(prompt: SerializedPrompt, subgraph: string | null): void;
onExecuted?(output: any): void;
defaultWidgets?: DefaultWidgetLayout

View File

@@ -1,4 +1,4 @@
import { BuiltInSlotType, LiteGraph, type ITextWidget, type SlotLayout, clamp } from "@litegraph-ts/core";
import { BuiltInSlotType, LiteGraph, type ITextWidget, type SlotLayout, clamp, type PropertyLayout, type IComboWidget } from "@litegraph-ts/core";
import ComfyGraphNode from "./ComfyGraphNode";
import type { GalleryOutput } from "./ComfyWidgetNodes";
@@ -6,7 +6,8 @@ export interface ComfyImageCacheNodeProperties extends Record<any, any> {
images: GalleryOutput | null,
index: number,
filenames: Record<number, { filename: string | null, status: ImageCacheState }>,
genNumber: number
genNumber: number,
updateMode: "replace" | "append"
}
type ImageCacheState = "none" | "uploading" | "failed" | "cached"
@@ -20,7 +21,8 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
images: null,
index: 0,
filenames: {},
genNumber: 0
genNumber: 0,
updateMode: "replace"
}
static slotLayout: SlotLayout = {
@@ -36,11 +38,15 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
]
}
static propertyLayout: PropertyLayout = [
{ name: "updateMode", defaultValue: "replace", type: "enum", options: { values: ["replace", "append"] } }
]
private _uploadPromise: Promise<void> | null = null;
private _state: ImageCacheState = "none"
stateWidget: ITextWidget;
filenameWidget: ITextWidget;
modeWidget: IComboWidget;
constructor(name?: string) {
super(name)
@@ -57,6 +63,14 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
""
);
this.filenameWidget.disabled = true;
this.modeWidget = this.addWidget<IComboWidget>(
"combo",
"Mode",
this.properties.updateMode,
null,
{ property: "updateMode", values: ["replace", "append"] }
);
}
override onPropertyChanged(property: string, value: any, prevValue?: any) {
@@ -66,13 +80,23 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
else
this.properties.index = 0
}
else if (property === "updateMode") {
this.modeWidget.value = value;
}
this.updateWidgets()
}
private updateWidgets() {
if (this.properties.filenames && this.properties.images) {
const fileCount = this.properties.images.images.length;
const cachedCount = Object.keys(this.properties.filenames).length
console.warn(cachedCount, this.properties.filenames)
this.filenameWidget.value = `${fileCount} files, ${cachedCount} cached`
}
else {
this.filenameWidget.value = `No files cached`
}
}
override onExecute() {
@@ -185,6 +209,7 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
this.setProperty("images", null)
this.setProperty("filenames", {})
this.setProperty("index", 0)
this.updateWidgets();
return
}
@@ -192,11 +217,24 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
if (link.data && "images" in link.data) {
this.setProperty("genNumber", this.properties.genNumber + 1)
this.setProperty("images", link.data as GalleryOutput)
this.setProperty("filenames", {})
console.debug("[ComfyImageCacheNode] Received output!", link.data)
const output = link.data as GalleryOutput;
if (this.properties.updateMode === "append" && this.properties.images != null) {
const newImages = this.properties.images.images.concat(output.images)
this.properties.images.images = newImages
this.setProperty("images", this.properties.images)
}
else {
this.setProperty("images", link.data as GalleryOutput)
this.setProperty("filenames", {})
}
console.debug("[ComfyImageCacheNode] Received output!", output, this.properties.updateMode, this.properties.images)
this.setIndex(0, true)
}
this.updateWidgets();
}
}

View File

@@ -1,10 +1,11 @@
import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout, LGraph, type INodeInputSlot, type ITextWidget, type INodeOutputSlot, type SerializedLGraphNode, BuiltInSlotType } from "@litegraph-ts/core";
import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout, LGraph, type INodeInputSlot, type ITextWidget, type INodeOutputSlot, type SerializedLGraphNode, BuiltInSlotType, type PropertyLayout, type IComboWidget } from "@litegraph-ts/core";
import ComfyGraphNode from "./ComfyGraphNode";
import ComboWidget from "$lib/widgets/ComboWidget.svelte";
import RangeWidget from "$lib/widgets/RangeWidget.svelte";
import TextWidget from "$lib/widgets/TextWidget.svelte";
import GalleryWidget from "$lib/widgets/GalleryWidget.svelte";
import ButtonWidget from "$lib/widgets/ButtonWidget.svelte";
import CheckboxWidget from "$lib/widgets/CheckboxWidget.svelte";
import type { SvelteComponentDev } from "svelte/internal";
import { Watch } from "@litegraph-ts/nodes-basic";
import type IComfyInputSlot from "$lib/IComfyInputSlot";
@@ -48,8 +49,11 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
override isBackendNode = false;
override serialize_widgets = true;
outputIndex: number | null = 0;
// input slots
inputIndex: number = 0;
// output slots
outputIndex: number | null = 0;
changedIndex: number | null = 1;
displayWidget: ITextWidget;
@@ -94,7 +98,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
if (this.outputIndex !== null && this.outputs.length >= this.outputIndex) {
this.setOutputData(this.outputIndex, get(this.value))
}
if (this.changedIndex !== null & this.outputs.length >= this.changedIndex) {
if (this.changedIndex !== null && this.outputs.length >= this.changedIndex) {
const changedOutput = this.outputs[this.changedIndex]
if (changedOutput.type === BuiltInSlotType.EVENT)
this.triggerSlot(this.changedIndex, "changed")
@@ -118,10 +122,8 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
if (this.copyFromInputLink) {
if (this.inputs.length >= this.inputIndex) {
const data = this.getInputData(this.inputIndex)
if (data) { // TODO can "null" be a legitimate value here?
if (data != null) { // TODO can "null" be a legitimate value here?
this.setValue(data)
const input = this.getInputLink(this.inputIndex)
input.data = null;
}
}
}
@@ -231,7 +233,8 @@ export class ComfySliderNode extends ComfyWidgetNode<number> {
static slotLayout: SlotLayout = {
inputs: [
{ name: "value", type: "number" }
{ name: "value", type: "number" },
{ name: "store", type: BuiltInSlotType.ACTION }
],
outputs: [
{ name: "value", type: "number" },
@@ -250,6 +253,11 @@ export class ComfySliderNode extends ComfyWidgetNode<number> {
super(name, 0)
}
override onAction(action: any, param: any) {
if (action === "store" && typeof param === "number")
this.setValue(param)
}
override setValue(value: any) {
if (typeof value !== "number")
return;
@@ -283,7 +291,8 @@ export class ComfyComboNode extends ComfyWidgetNode<string> {
static slotLayout: SlotLayout = {
inputs: [
{ name: "value", type: "string" }
{ name: "value", type: "string" },
{ name: "store", type: BuiltInSlotType.ACTION }
],
outputs: [
{ name: "value", type: "string" },
@@ -323,6 +332,11 @@ export class ComfyComboNode extends ComfyWidgetNode<string> {
return true;
}
override onAction(action: any, param: any) {
if (action === "store" && typeof param === "string")
this.setValue(param)
}
override setValue(value: any) {
if (typeof value !== "string" || this.properties.values.indexOf(value) === -1)
return;
@@ -358,7 +372,8 @@ export class ComfyTextNode extends ComfyWidgetNode<string> {
static slotLayout: SlotLayout = {
inputs: [
{ name: "value", type: "string" }
{ name: "value", type: "string" },
{ name: "store", type: BuiltInSlotType.ACTION }
],
outputs: [
{ name: "value", type: "string" },
@@ -372,6 +387,11 @@ export class ComfyTextNode extends ComfyWidgetNode<string> {
super(name, "")
}
override onAction(action: any, param: any) {
if (action === "store")
this.setValue(param)
}
override setValue(value: any) {
super.setValue(`${value}`)
}
@@ -397,13 +417,15 @@ export type GalleryOutputEntry = {
}
export interface ComfyGalleryProperties extends ComfyWidgetProperties {
index: number
index: number,
updateMode: "replace" | "append"
}
export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
override properties: ComfyGalleryProperties = {
defaultValue: [],
index: 0
index: 0,
updateMode: "replace"
}
static slotLayout: SlotLayout = {
@@ -417,20 +439,33 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
]
}
static propertyLayout: PropertyLayout = [
{ name: "updateMode", defaultValue: "replace", type: "enum", options: { values: ["replace", "append"] } }
]
override svelteComponentType = GalleryWidget
override copyFromInputLink = false;
override outputIndex = null;
override changedIndex = null;
modeWidget: IComboWidget;
constructor(name?: string) {
super(name, [])
this.modeWidget = this.addWidget("combo", "Mode", this.properties.updateMode, null, { property: "updateMode", values: ["replace", "append"] })
}
override onPropertyChanged(property: any, value: any) {
if (property === "updateMode") {
this.modeWidget.value = value;
}
}
override onExecute() {
this.setOutputData(0, this.properties.index)
}
override onAction(action: any) {
override onAction(action: any, param: any, options: { action_call?: string }) {
if (action === "clear") {
this.setValue([])
}
@@ -442,9 +477,13 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
const galleryItems: GradioFileData[] = this.convertItems(link.data)
// const currentValue = get(this.value)
// this.setValue(currentValue.concat(galleryItems))
this.setValue(galleryItems)
if (this.properties.updateMode === "append") {
const currentValue = get(this.value)
this.setValue(currentValue.concat(galleryItems))
}
else {
this.setValue(galleryItems)
}
}
this.setProperty("index", 0)
}
@@ -489,13 +528,13 @@ LiteGraph.registerNodeType({
})
export interface ComfyButtonProperties extends ComfyWidgetProperties {
message: string
param: string
}
export class ComfyButtonNode extends ComfyWidgetNode<boolean> {
override properties: ComfyButtonProperties = {
defaultValue: false,
message: "bang"
param: "bang"
}
static slotLayout: SlotLayout = {
@@ -514,8 +553,8 @@ export class ComfyButtonNode extends ComfyWidgetNode<boolean> {
onClick() {
this.setValue(true)
this.triggerSlot(0, this.properties.message);
this.setValue(false)
this.triggerSlot(0, this.properties.param);
this.setValue(false) // TODO onRelease
}
constructor(name?: string) {
@@ -529,3 +568,40 @@ LiteGraph.registerNodeType({
desc: "Button that triggers an event when clicked",
type: "ui/button"
})
export interface ComfyCheckboxProperties extends ComfyWidgetProperties {
}
export class ComfyCheckboxNode extends ComfyWidgetNode<boolean> {
override properties: ComfyCheckboxProperties = {
defaultValue: false,
}
static slotLayout: SlotLayout = {
outputs: [
{ name: "value", type: "boolean" },
{ name: "changed", type: BuiltInSlotType.EVENT },
]
}
override svelteComponentType = CheckboxWidget;
override setValue(value: any) {
value = Boolean(value)
const changed = value != get(this.value);
super.setValue(Boolean(value))
if (changed)
this.triggerSlot(1)
}
constructor(name?: string) {
super(name, false)
}
}
LiteGraph.registerNodeType({
class: ComfyCheckboxNode,
title: "UI.Checkbox",
desc: "Checkbox that stores a boolean value",
type: "ui/checkbox"
})

View File

@@ -6,52 +6,249 @@ import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
import type { ComfyWidgetNode } from '$lib/nodes';
type DragItemEntry = {
/*
* Drag item.
*/
dragItem: IDragItem,
/*
* Children of this drag item.
* Only applies if the drag item's type is "container"
*/
children: IDragItem[] | null,
/*
* Parent of this drag item.
*/
parent: IDragItem | null
}
/*
* Global workflow attributes
*/
export type LayoutAttributes = {
/*
* Default subgraph to run when the "Queue Prompt" button in the bottom bar
* is pressed.
*
* If it's an empty string, all backend nodes will be included in the prompt
* instead.
*/
defaultSubgraph: string
}
/*
* Keeps track of the tree of UI components - widgets and the containers that
* group them together.
*/
export type LayoutState = {
/*
* Root of the UI tree
*/
root: IDragItem | null,
/*
* All items indexed by their own ID
*/
allItems: Record<DragItemID, DragItemEntry>,
/*
* Items indexed by the litegraph node they're bound to
* Only contains drag items of type "widget"
*/
allItemsByNode: Record<number, DragItemEntry>,
/*
* Next ID to use for instantiating a new drag item
*/
currentId: number,
/*
* Selected drag items.
*/
currentSelection: DragItemID[],
/*
* Selected LGraphNodes inside the litegraph canvas.
*/
currentSelectionNodes: LGraphNode[],
/*
* If true, a saved workflow is being deserialized, so ignore any
* nodeAdded/nodeRemoved events.
*
* TODO: instead use LGraphAddNodeOptions.addedByDeserialize
*/
isConfiguring: boolean,
/*
* If true, the right-click context menu is open
*/
isMenuOpen: boolean,
/*
* Global workflow attributes
*/
attrs: LayoutAttributes
}
/**
* Attributes for both containers and nodes.
**/
export type Attributes = {
/*
* Flex direction for containers.
*/
direction: "horizontal" | "vertical",
/*
* Display name of this item.
*/
title: string,
showTitle: boolean,
/*
* List of classes to apply to the component.
*/
classes: string,
blockVariant?: "block" | "hidden",
/*
* Variant for containers. "hidden" hides margin/borders.
*/
containerVariant?: "block" | "hidden",
/*
* If true, don't show this component in the UI
*/
hidden?: boolean,
/*
* If true, grey out this component in the UI
*/
disabled?: boolean,
flexGrow?: number
/*
* CSS height
*/
height?: string,
/*
* CSS Flex grow
*/
flexGrow?: number,
/**
* Display variant for widgets/containers (e.g. number widget can act as slider/knob/dial)
* Valid values depend on the widget in question.
*/
variant?: string,
/*********************************************/
/* Special attributes for widgets/containers */
/*********************************************/
// Accordion
openOnStartup?: boolean
// Button
buttonVariant?: "primary" | "secondary",
buttonSize?: "large" | "small"
}
export type AttributesSpec = {
/*
* ID necessary for svelte's keyed each, autoset at the top level in this source file.
*/
id?: number,
/*
* Attribute name. Corresponds to the name of the instance variable in the
* hashmap/class instance, which depends on `location`.
*/
name: string,
type: string,
/*
* Type of this attribute.
* If you want to support a custom type, use "string" combined with
* `serialize` and `deserialize`.
*/
type: "string" | "enum" | "number" | "boolean",
/*
* Location of this attribute.
* - "widget": inside IDragNode.attrs
* - "nodeProps": inside LGraphNode.properties
* - "nodeVars": an instance variable directly on an LGraphNode
* - "workflow": inside $layoutState.attrs
*/
location: "widget" | "nodeProps" | "nodeVars" | "workflow"
/*
* Can this attribute be edited in the properties pane.
*/
editable: boolean,
/*
* Default value to supply to this attribute if it is null when the properties pane is opened.
* NOTE: This means that any attribute can't have a default null value!
*/
defaultValue: any,
/*
* If `type` is "enum", the valid values for the combo widget.
*/
values?: string[],
hidden?: boolean,
/*
* If `type` is "number", step for the slider
*/
step?: number,
/*
* If `type` is "number", min for the slider
*/
min?: number,
/*
* If `type` is "number", max for the slider
*/
max?: number,
/*
* Valid `LGraphNode.type`s this property applies to if it's located in a node.
* These are like "ui/button", "ui/slider".
*/
validNodeTypes?: string[],
/*
* Callback: if false, don't show the property in the pane.
* Useful if you need to show the property based on another property.
* Example: If the IDragItem is a container (not a widget), show its flex `direction`.
*/
canShow?: (arg: IDragItem | LGraphNode) => boolean,
/*
* If the type of this spec is "string", but the underlying type is something else,
* convert the value to a string here so it can be edited in the textbox.
*/
serialize?: (arg: any) => string,
/*
* If the type of this spec is "string", but the underlying type is something else,
* convert the textbox value into the underlying value.
*/
deserialize?: (arg: string) => any,
/*
* If true, when this property is changed the properties pane will be rebuilt.
* This should be used if there's a canShow dependent on this property so
* the pane can be updated with the new list of valid properties.
*/
refreshPanelOnChange?: boolean
}
/*
* A list of `AttributesSpec`s grouped under a category.
*/
export type AttributesCategorySpec = {
categoryName: string,
specs: AttributesSpec[]
@@ -59,6 +256,17 @@ export type AttributesCategorySpec = {
export type AttributesSpecList = AttributesCategorySpec[]
const serializeStringArray = (arg: string[]) => arg.join(",")
const deserializeStringArray = (arg: string) => {
if (arg === "")
return []
return arg.split(",").map(s => s.trim())
}
/*
* Attributes that will show up in the properties panel.
* Their order in the list is the order they'll appear in the panel.
*/
const ALL_ATTRIBUTES: AttributesSpecList = [
{
categoryName: "appearance",
@@ -67,18 +275,21 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
name: "title",
type: "string",
location: "widget",
defaultValue: "",
editable: true,
},
{
name: "hidden",
type: "boolean",
location: "widget",
defaultValue: false,
editable: true
},
{
name: "disabled",
type: "boolean",
location: "widget",
defaultValue: false,
editable: true
},
{
@@ -86,26 +297,74 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
type: "enum",
location: "widget",
editable: true,
values: ["horizontal", "vertical"]
values: ["horizontal", "vertical"],
defaultValue: "vertical",
canShow: (di: IDragItem) => di.type === "container"
},
{
name: "flexGrow",
type: "number",
location: "widget",
defaultValue: 100,
editable: true
},
{
name: "classes",
type: "string",
location: "widget",
defaultValue: "",
editable: true,
},
// Container variants
{
name: "blockVariant",
name: "variant",
type: "enum",
location: "widget",
editable: true,
values: ["block", "hidden"]
values: ["block", "accordion", "tabs"],
defaultValue: "block",
canShow: (di: IDragItem) => di.type === "container",
refreshPanelOnChange: true
},
{
name: "containerVariant",
type: "enum",
location: "widget",
editable: true,
values: ["block", "hidden"],
defaultValue: "block",
canShow: (di: IDragItem) => di.type === "container"
},
// Accordion
{
name: "openOnStartup",
type: "boolean",
location: "widget",
editable: true,
defaultValue: false,
canShow: (di: IDragItem) => di.type === "container" && di.attrs.variant === "accordion"
},
// Button
{
name: "buttonVariant",
type: "enum",
location: "widget",
editable: true,
validNodeTypes: ["ui/button"],
values: ["primary", "secondary"],
defaultValue: "primary"
},
{
name: "buttonSize",
type: "enum",
location: "widget",
editable: true,
validNodeTypes: ["ui/button"],
values: ["large", "small"],
defaultValue: "large"
},
]
},
@@ -118,12 +377,9 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
type: "string",
location: "nodeVars",
editable: true,
serialize: (arg: string[]) => arg.join(","),
deserialize: (arg: string) => {
if (arg === "")
return []
return arg.split(",").map(s => s.trim())
}
defaultValue: [],
serialize: serializeStringArray,
deserialize: deserializeStringArray
},
// Range
@@ -132,6 +388,9 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
type: "number",
location: "nodeProps",
editable: true,
defaultValue: 0,
min: -2 ^ 16,
max: 2 ^ 16,
validNodeTypes: ["ui/slider"],
},
{
@@ -139,6 +398,9 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
type: "number",
location: "nodeProps",
editable: true,
defaultValue: 10,
min: -2 ^ 16,
max: 2 ^ 16,
validNodeTypes: ["ui/slider"],
},
{
@@ -146,16 +408,31 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
type: "number",
location: "nodeProps",
editable: true,
defaultValue: 1,
min: -2 ^ 16,
max: 2 ^ 16,
validNodeTypes: ["ui/slider"],
},
// Button
{
name: "message",
name: "param",
type: "string",
location: "nodeProps",
editable: true,
validNodeTypes: ["ui/button"],
defaultValue: "bang"
},
// gallery
{
name: "updateMode",
type: "enum",
location: "nodeProps",
editable: true,
validNodeTypes: ["ui/gallery"],
values: ["replace", "append"],
defaultValue: "replace"
},
// Workflow
@@ -163,35 +440,80 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
name: "defaultSubgraph",
type: "string",
location: "workflow",
editable: true
editable: true,
defaultValue: ""
}
]
}
];
// This is needed so the specs can be iterated with svelte's keyed #each.
let i = 0;
for (const cat of Object.values(ALL_ATTRIBUTES)) {
for (const val of Object.values(cat.specs)) {
val.id = i;
i += 1;
}
}
export { ALL_ATTRIBUTES };
/*
* Something that can be dragged around in the frontend - a widget or a container.
*/
export interface IDragItem {
type: string,
/*
* Type of the item.
*/
type: "container" | "widget",
/*
* Unique ID of the item.
*/
id: DragItemID,
/*
* If true, the node associated with this drag item is executing.
* Used to show an indicator on the widget/container.
*/
isNodeExecuting?: boolean,
/*
* Attributes for this drag item.
*/
attrs: Attributes,
/*
* Hackish thing to indicate to Svelte that an attribute changed.
* TODO Use Writeable<Attributes> instead!
*/
attrsChanged: Writable<boolean>
}
/*
* A container (block, accordion, tabs). Has child drag items.
*/
export interface ContainerLayout extends IDragItem {
type: "container",
}
/*
* A widget (slider, dropdown, textbox...)
*/
export interface WidgetLayout extends IDragItem {
type: "widget",
/*
* litegraph node this widget is bound to.
*/
node: ComfyWidgetNode
}
type DragItemID = string;
type LayoutStateOps = {
addContainer: (parent: ContainerLayout | null, attrs: Partial<Attributes>, index: number) => ContainerLayout,
addWidget: (parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes>, index: number) => WidgetLayout,
addContainer: (parent: ContainerLayout | null, attrs: Partial<Attributes>, index?: number) => ContainerLayout,
addWidget: (parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes>, index?: number) => WidgetLayout,
findDefaultContainerForInsertion: () => ContainerLayout | null,
updateChildren: (parent: IDragItem, children: IDragItem[]) => IDragItem[],
nodeAdded: (node: LGraphNode) => void,
@@ -241,7 +563,7 @@ function findDefaultContainerForInsertion(): ContainerLayout | null {
return null
}
function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes> = {}, index: number = -1): ContainerLayout {
function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes> = {}, index?: number): ContainerLayout {
const state = get(store);
const dragItem: ContainerLayout = {
type: "container",
@@ -249,10 +571,9 @@ function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes>
attrsChanged: writable(false),
attrs: {
title: "Container",
showTitle: true,
direction: "vertical",
classes: "",
blockVariant: "block",
containerVariant: "block",
flexGrow: 100,
...attrs
}
@@ -260,14 +581,14 @@ function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes>
const entry: DragItemEntry = { dragItem, children: [], parent: null };
state.allItems[dragItem.id] = entry;
if (parent) {
moveItem(dragItem, parent)
moveItem(dragItem, parent, index)
}
console.debug("[layoutState] addContainer", state)
store.set(state)
return dragItem;
}
function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes> = {}, index: number = -1): WidgetLayout {
function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes> = {}, index?: number): WidgetLayout {
const state = get(store);
const widgetName = "Widget"
const dragItem: WidgetLayout = {
@@ -277,7 +598,6 @@ function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partia
attrsChanged: writable(false),
attrs: {
title: widgetName,
showTitle: true,
direction: "horizontal",
classes: "",
flexGrow: 100,
@@ -289,7 +609,7 @@ function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partia
state.allItems[dragItem.id] = entry;
state.allItemsByNode[node.id] = entry;
console.debug("[layoutState] addWidget", state)
moveItem(dragItem, parent)
moveItem(dragItem, parent, index)
return dragItem;
}
@@ -313,23 +633,10 @@ function nodeAdded(node: LGraphNode) {
const parent = findDefaultContainerForInsertion();
// Two cases where we want to add nodes:
// 1. User adds a new UI node, so we should instantiate its widget in the frontend.
// 2. User adds a node with inputs that can be filled by frontend widgets.
// Depending on config, this means we should instantiate default UI nodes connected to those inputs.
console.debug(node)
console.debug("[layoutState] nodeAdded", node)
if ("svelteComponentType" in node) {
addWidget(parent, node as ComfyWidgetNode);
}
// Add default node panel with all widgets autoinstantiated
// if (node.widgets && node.widgets.length > 0) {
// const container = addContainer(parent.id, { title: node.title, direction: "vertical", associatedNode: node.id });
// for (const widget of node.widgets) {
// addWidget(container.id, node, widget, { associatedNode: node.id });
// }
// }
}
function removeEntry(state: LayoutState, id: DragItemID) {
@@ -367,7 +674,7 @@ function nodeRemoved(node: LGraphNode) {
store.set(state)
}
function moveItem(target: IDragItem, to: ContainerLayout, index: number = -1) {
function moveItem(target: IDragItem, to: ContainerLayout, index?: number) {
const state = get(store)
const entry = state.allItems[target.id]
if (entry.parent && entry.parent.id === to.id)
@@ -375,9 +682,9 @@ function moveItem(target: IDragItem, to: ContainerLayout, index: number = -1) {
if (entry.parent) {
const parentEntry = state.allItems[entry.parent.id];
const index = parentEntry.children.findIndex(c => c.id === target.id)
if (index !== -1) {
parentEntry.children.splice(index, 1)
const parentIndex = parentEntry.children.findIndex(c => c.id === target.id)
if (parentIndex !== -1) {
parentEntry.children.splice(parentIndex, 1)
}
else {
console.error(parentEntry)
@@ -387,7 +694,7 @@ function moveItem(target: IDragItem, to: ContainerLayout, index: number = -1) {
}
const toEntry = state.allItems[to.id];
if (index !== -1)
if (index != null && index >= 0)
toEntry.children.splice(index, 0, target)
else
toEntry.children.push(target)

View File

@@ -2,13 +2,14 @@ import { writable } from 'svelte/store';
import type { Readable, Writable } from 'svelte/store';
import type ComfyApp from "$lib/components/ComfyApp"
export type UIEditMode = "disabled" | "widgets" | "containers" | "layout";
export type UIEditMode = "widgets" | "containers" | "layout";
export type UIState = {
app: ComfyApp,
nodesLocked: boolean,
graphLocked: boolean,
autoAddUI: boolean,
uiUnlocked: boolean,
uiEditMode: UIEditMode,
}
@@ -19,7 +20,8 @@ const store: WritableUIStateStore = writable(
graphLocked: false,
nodesLocked: false,
autoAddUI: true,
uiEditMode: "disabled",
uiUnlocked: false,
uiEditMode: "widgets"
})
const uiStateStore: WritableUIStateStore =

View File

@@ -3,11 +3,11 @@
import type { ComfySliderNode } from "$lib/nodes/index";
import { type WidgetLayout } from "$lib/stores/layoutState";
import { Button } from "@gradio/button";
import { get, type Writable } from "svelte/store";
import { get, type Writable, writable } from "svelte/store";
export let widget: WidgetLayout | null = null;
let node: ComfyButtonNode | null = null;
let nodeValue: Writable<boolean> | null = null;
let propsChanged: Writable<number> | null = null;
let attrsChanged: Writable<boolean> | null = null;
$: widget && setNodeValue(widget);
@@ -15,7 +15,7 @@
if (widget) {
node = widget.node as ComfyButtonNode
nodeValue = node.value;
propsChanged = node.propsChanged;
attrsChanged = widget.attrsChanged;
}
};
@@ -24,25 +24,33 @@
}
const style = {
full_width: "100%"
full_width: "100%",
}
</script>
<div class="wrapper gradio-button">
{#if node !== null}
<Button
disabled={widget.attrs.disabled}
on:click={onClick}
variant="primary"
{style}>
{widget.attrs.title}
</Button>
{/if}
{#key $attrsChanged}
{#if node !== null}
<Button
disabled={widget.attrs.disabled}
on:click={onClick}
variant={widget.attrs.buttonVariant || "primary"}
size={widget.attrs.buttonSize === "small" ? "sm" : "lg"}
{style}>
{widget.attrs.title}
</Button>
{/if}
{/key}
</div>
<style lang="scss">
.wrapper {
padding: 2px;
width: 100%;
height: 100%;
:global(> button) {
height: 100%;
}
}
</style>

View File

@@ -0,0 +1,55 @@
<script lang="ts">
import type { ComfyCheckboxNode } from "$lib/nodes/ComfyWidgetNodes";
import { type WidgetLayout } from "$lib/stores/layoutState";
import { Block } from "@gradio/atoms";
import { Checkbox } from "@gradio/form";
import { get, type Writable, writable } from "svelte/store";
export let widget: WidgetLayout | null = null;
let node: ComfyCheckboxNode | null = null;
let nodeValue: Writable<boolean> | null = null;
let attrsChanged: Writable<boolean> | null = null;
$: widget && setNodeValue(widget);
function setNodeValue(widget: WidgetLayout) {
if (widget) {
node = widget.node as ComfyCheckboxNode
nodeValue = node.value;
attrsChanged = widget.attrsChanged;
}
};
</script>
<div class="wrapper gradio-checkbox">
<div class="inner">
{#key $attrsChanged}
{#if node !== null}
<Block>
<Checkbox disabled={widget.attrs.disabled} label={widget.attrs.title} bind:value={$nodeValue} />
</Block>
{/if}
{/key}
</div>
</div>
<style lang="scss">
.wrapper {
display: flex;
flex-direction: row;
align-items: flex-end;
height: 100%;
> .inner {
padding: 2px;
width: 100%;
display: flex;
flex-direction: row;
height: min-content;
:global(> label) {
height: 100%;
}
}
}
</style>

View File

@@ -114,6 +114,11 @@
}
}
:global(.svelte-select) {
width: auto;
max-width: 30rem;
}
:global(.svelte-select-list) {
z-index: var(--layer-top) !important;
}

View File

@@ -73,10 +73,13 @@
{/if}
</div>
<style>
<style lang="scss">
.wrapper {
padding: 2px;
width: 100%;
:global(> .block) {
border-radius: 0px !important;
}
}
.padding {

View File

@@ -35,7 +35,7 @@
disabled={widget.attrs.disabled}
lines={node.properties.multiline ? 5 : 1}
max_lines={node.properties.multiline ? 5 : 1}
show_label={true}
show_label={widget.attrs.title !== ""}
on:change
on:submit
on:blur
@@ -49,4 +49,8 @@
padding: 2px;
width: 100%;
}
:global(span.hide) {
display: none;
}
</style>

View File

@@ -265,36 +265,66 @@ div.float {
}
}
.category-name {
.category-name {
background: var(--ae-panel-bg-color) !important;
border-color: var(--ae-panel-border-color) !important;
.title, .type {
color: var(--ae-label-color)
}
}
}
.props-entry {
border-width: 1px;
border-left: 1px var(--ae-panel-border-color) !important;
border-right: 1px var(--ae-panel-border-color) !important;
}
.props-entry {
border-width: 1px;
border-left: 1px var(--ae-panel-border-color) !important;
border-right: 1px var(--ae-panel-border-color) !important;
}
// .container > .block {
// box-shadow: none !important;
// border-color: var(--ae-panel-border-color) !important;
// border-radius: 0 !important;
// background: var(--ae-input-bg-color) !important;
/**************/
/* Accordions */
/**************/
// > .v-pane {
// box-shadow: none !important;
// border-color: var(--ae-panel-border-color) !important;
// border-radius: 0 !important;
// background: var(--ae-input-bg-color) !important;
// }
// }
.block.gradio-accordion {
background-color: var(--ae-main-bg-color) !important;
/*padding-bottom: 0 !important;*/
}
.block.gradio-accordion:hover .label-wrap {
.block.gradio-accordion:has(.label-wrap:hover) {
border-color: var(--ae-primary-color) !important;
}
.block.gradio-accordion .label-wrap {
margin: calc(-1px + var(--ae-inside-padding-size) * -1);
width: auto;
padding: var(--ae-accordion-vertical-padding) var(--ae-accordion-horizontal-padding);
border-radius: var(--ae-panel-border-radius);
line-height: var(--ae-accordion-line-height);
> span {
color: var(--ae-label-color) !important;
}
&:hover {
> span {
color: var(--ae-main-bg-color) !important;
}
}
/*pointer-events: none !important;*/
}
.block.gradio-accordion .hide + .open.label-wrap {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.block.gradio-accordion .label-wrap.open {
margin-bottom: calc(var(--ae-inside-padding-size) / 2);
}
.edit > .block.gradio-accordion .label-wrap.open {
margin-bottom: var(--ae-inside-padding-size);
}
.block.gradio-accordion > .gap.svelte-vt1mxs > div:first-child {
margin-top: calc(var(--ae-inside-padding-size) * 2) !important;
}
.block.gradio-accordion .label-wrap:hover {
color: var(--ae-main-bg-color) !important;
background-color: var(--ae-primary-color) !important;
}
@@ -308,7 +338,28 @@ div.float {
left: 0 !important;
top: 0 !important;
opacity: 0 !important;
}
.gradio-tabs.tabs {
> .tab-nav {
border-bottom: 1px solid var(--ae-subpanel-border-color);
> button {
border-radius: 0;
border-width: var(--ae-border-width);
color: var(--ae-text-color);
&.selected {
border-color: var(--ae-subpanel-border-color);
background: var(--ae-subpanel-bg-color);
color: var(--ae-primary-color);
}
}
}
> .tabitem {
border: 1px solid var(--ae-subpanel-border-color);
border-top: none;
border-radius: 0px !important;
}
}
.form>.gradio-row>.form{
@@ -368,6 +419,10 @@ div.gradio-row>.form{
box-shadow: none !important;
}
.gradio-checkbox > .inner > .block {
background-color: var(--ae-input-bg-color) !important;
}
.wrapper.gradio-textbox textarea {
overflow-y: scroll;
box-sizing: border-box;
@@ -545,12 +600,16 @@ button.primary {
border-radius: var(--ae-panel-border-radius) !important;
background: var(--ae-input-bg-color) !important;
color: var(--ae-input-color) !important;
}
button.secondary:hover,
button.primary:hover {
background: var(--ae-primary-color) !important;
color: var(--ae-input-bg-color) !important;
&:hover {
background: var(--ae-primary-color) !important;
color: var(--ae-input-bg-color) !important;
}
&:active {
background: var(--ae-input-bg-color) !important;
color: var(--ae-input-color) !important;
}
}
/**********************/
@@ -813,3 +872,29 @@ input[type=range]::-ms-fill-upper {
color: var(--ae-input-color) !important;
}
}
.comfy-toggle-button {
> .lg {
border-color: var(--ae-subpanel-border-color) !important;
}
&:hover svg {
color: var(--ae-main-bg-color) !important;
}
&:active svg {
color: var(--ae-placeholder-color) !important;
}
svg {
color: var(--ae-input-color) !important;
}
&.toggled {
svg {
color: var(--ae-icon-color) !important;
}
&:hover svg {
color: var(--ae-main-bg-color) !important;
}
&:active svg {
color: var(--ae-placeholder-color) !important;
}
}
}

View File

@@ -8,8 +8,13 @@ import { viteStaticCopy } from 'vite-plugin-static-copy'
export default defineConfig({
clearScreen: false,
plugins: [
FullReload(["src/**/*.{js,ts,scss,svelte}"]),
svelte(), ,
// FullReload([
// // "src/**/*.{js,ts,scss,svelte}"
// "src/**/*.{scss}",
// "src/lib/stores/*.*",
// "src/**/ComfyApp.{ts,svelte}"
// ]),
svelte(),
viteStaticCopy({
targets: [
{