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", "type": "module",
"dependencies": { "dependencies": {
"@gradio/accordion": "workspace:*",
"@gradio/atoms": "workspace:*", "@gradio/atoms": "workspace:*",
"@gradio/button": "workspace:*", "@gradio/button": "workspace:*",
"@gradio/client": "workspace:*", "@gradio/client": "workspace:*",
"@gradio/form": "workspace:*", "@gradio/form": "workspace:*",
"@gradio/gallery": "workspace:*", "@gradio/gallery": "workspace:*",
"@gradio/icons": "workspace:*", "@gradio/icons": "workspace:*",
"@gradio/tabs": "workspace:*",
"@gradio/theme": "workspace:*", "@gradio/theme": "workspace:*",
"@gradio/upload": "workspace:*", "@gradio/upload": "workspace:*",
"@gradio/utils": "workspace:*", "@gradio/utils": "workspace:*",

6
pnpm-lock.yaml generated
View File

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

View File

@@ -50,6 +50,7 @@ export class ImageViewer {
showModal(event: Event) { showModal(event: Event) {
const source = (event.target || event.srcElement) as HTMLImageElement; const source = (event.target || event.srcElement) as HTMLImageElement;
const galleryElem = source.closest<HTMLDivElement>("div.block") const galleryElem = source.closest<HTMLDivElement>("div.block")
console.debug("[ImageViewer] showModal", event, source, galleryElem);
if (!galleryElem || ImageViewer.all_gallery_buttons(galleryElem).length === 0) { if (!galleryElem || ImageViewer.all_gallery_buttons(galleryElem).length === 0) {
console.error("No buttons found on gallery element!", galleryElem) console.error("No buttons found on gallery element!", galleryElem)
return; 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 { flip } from 'svelte/animate';
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState"; import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { startDrag, stopDrag } from "$lib/utils" import { startDrag, stopDrag } from "$lib/utils"
import type { Writable } from "svelte/store";
export let container: ContainerLayout | null = null; export let container: ContainerLayout | null = null;
export let zIndex: number = 0; export let zIndex: number = 0;
export let classes: string[] = []; export let classes: string[] = [];
export let showHandles: boolean = false; export let showHandles: boolean = false;
export let edit: boolean = false;
export let dragDisabled: boolean = false;
let attrsChanged: Writable<boolean> | null = null; let attrsChanged: Writable<boolean> | null = null;
let children: IDragItem[] | null = null; let children: IDragItem[] | null = null;
const flipDurationMs = 100; const flipDurationMs = 100;
@@ -38,20 +42,20 @@
children = layoutState.updateChildren(container, evt.detail.items) children = layoutState.updateChildren(container, evt.detail.items)
// Ensure dragging is stopped on drag finish // Ensure dragging is stopped on drag finish
}; };
const tt = "asd\nasdlkj"
</script> </script>
{#if container && children} {#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}" <div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')} z-index{zIndex}"
class:hide-block={container.attrs.blockVariant === "hidden"} class:hide-block={container.attrs.containerVariant === "hidden"}
class:selected={$uiState.uiEditMode !== "disabled" && $layoutState.currentSelection.includes(container.id)} class:selected={$uiState.uiUnlocked && $layoutState.currentSelection.includes(container.id)}
class:root-container={zIndex === 0} class:root-container={zIndex === 0}
class:is-executing={container.isNodeExecuting} class:is-executing={container.isNodeExecuting}
class:edit={edit}> class:edit={edit}>
<Block> <Block>
{#if container.attrs.title !== ""} {#if container.attrs.title && container.attrs.title !== ""}
<label for={String(container.id)} class={$uiState.uiEditMode === "widgets" ? "edit-title-label" : ""}> <label for={String(container.id)} class={($uiState.uiUnlocked && $uiState.uiEditMode === "widgets") ? "edit-title-label" : ""}>
<BlockTitle>{container.attrs.title}</BlockTitle> <BlockTitle>{container.attrs.title}</BlockTitle>
</label> </label>
{/if} {/if}
@@ -64,7 +68,7 @@
centreDraggedOnCursor: true, centreDraggedOnCursor: true,
morphDisabled: true, morphDisabled: true,
dropFromOthersDisabled: zIndex === 0, dropFromOthersDisabled: zIndex === 0,
dragDisabled: zIndex === 0 || $layoutState.currentSelection.length > 2 || $uiState.uiEditMode === "disabled" dragDisabled
}}" }}"
on:consider="{handleConsider}" on:consider="{handleConsider}"
on:finalize="{handleFinalize}" on:finalize="{handleFinalize}"
@@ -91,7 +95,6 @@
{/if} {/if}
</Block> </Block>
</div> </div>
{/key}
{/if} {/if}
<style lang="scss"> <style lang="scss">
@@ -158,7 +161,7 @@
} }
} }
:global(.block) { > :global(.block) {
height: fit-content; height: fit-content;
} }
@@ -170,7 +173,7 @@
padding: 1.4em; padding: 1.4em;
} }
:global(.hide-block > .block) { > :global(.hide-block > .block) {
padding: 0.5em 0.25em; padding: 0.5em 0.25em;
box-shadow: unset; box-shadow: unset;
border-width: 0; border-width: 0;
@@ -289,12 +292,6 @@
color: var(--input-placeholder-color); 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) { .root-container > :global(.block) {
padding: 0px; padding: 0px;
} }

View File

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

View File

@@ -106,6 +106,8 @@ export default class ComfyApp {
LiteGraph.release_link_on_empty_shows_menu = true; LiteGraph.release_link_on_empty_shows_menu = true;
LiteGraph.alt_drag_do_clone_nodes = true; LiteGraph.alt_drag_do_clone_nodes = true;
(window as any).LiteGraph = LiteGraph;
// await this.#invokeExtensionsAsync("init"); // await this.#invokeExtensionsAsync("init");
await this.registerNodes(); await this.registerNodes();
@@ -230,17 +232,17 @@ export default class ComfyApp {
} }
private addDropHandler() { private addDropHandler() {
this.dropZone = document.getElementById("dropzone"); // this.dropZone = document.getElementById("dropzone");
if (this.dropZone) { // if (this.dropZone) {
window.addEventListener('dragenter', this.allowDrag.bind(this)); // window.addEventListener('dragenter', this.allowDrag.bind(this));
this.dropZone.addEventListener('dragover', this.allowDrag.bind(this)); // this.dropZone.addEventListener('dragover', this.allowDrag.bind(this));
this.dropZone.addEventListener('dragleave', this.hideDropZone.bind(this)); // this.dropZone.addEventListener('dragleave', this.hideDropZone.bind(this));
this.dropZone.addEventListener('drop', this.handleDrop.bind(this)); // this.dropZone.addEventListener('drop', this.handleDrop.bind(this));
} // }
else { // else {
console.warn("No dropzone detected (probably on mobile).") // 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); const n = workflow.nodes.find((n) => n.id === node_.id);
if (!node_.isBackendNode) { if (!node_.isBackendNode) {
console.debug("Not serializing node: ", node_.type) // console.debug("Not serializing node: ", node_.type)
continue; continue;
} }
@@ -562,8 +564,8 @@ export default class ComfyApp {
} }
} }
console.warn({ workflow, output }) // console.debug({ workflow, output })
console.warn(promptToGraphVis({ workflow, output })) // console.debug(promptToGraphVis({ workflow, output }))
return { workflow, output }; return { workflow, output };
} }
@@ -588,7 +590,7 @@ export default class ComfyApp {
for (let i = 0; i < batchCount; i++) { for (let i = 0; i < batchCount; i++) {
for (const node of this.lGraph._nodes_in_order) { for (const node of this.lGraph._nodes_in_order) {
if ("beforeQueued" in node) { 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) { for (const n of p.workflow.nodes) {
const node = this.lGraph.getNodeById(n.id); const node = this.lGraph.getNodeById(n.id);
if ("afterQueued" in node) { 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 value: string = "";
export let values: string[] = [""]; export let values: string[] = [""];
export let name: string = ""; export let name: string = "";
export let disabled: boolean = false;
let value_: string = "" let value_: string = ""
$: value; $: value;
@@ -27,7 +28,7 @@
<label class="select-wrapper"> <label class="select-wrapper">
<BlockTitle>{name}</BlockTitle> <BlockTitle>{name}</BlockTitle>
<div class="select"> <div class="select">
<select on:blur bind:value> <select on:blur bind:value {disabled}>
{#each values as value} {#each values as value}
<option {value}> <option {value}>
{value} {value}
@@ -53,4 +54,8 @@
.select-title { .select-title {
padding: 0.2rem; padding: 0.2rem;
} }
input:disabled {
cursor: not-allowed;
}
</style> </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"; import { createEventDispatcher } from "svelte";
export let value: number = 0; export let value: number = 0;
export let min: number = -1024
export let max: number = 1024
export let step: number = 1; export let step: number = 1;
export let name: string = ""; export let name: string = "";
export let disabled: boolean = false;
let value_: number = 0; let value_: number = 0;
$: value; $: value;
@@ -26,7 +29,7 @@
<label class="number-wrapper"> <label class="number-wrapper">
<BlockTitle>{name}</BlockTitle> <BlockTitle>{name}</BlockTitle>
<div class="number"> <div class="number">
<input type="number" bind:value {step}> <input type="number" bind:value {min} {max} {step} {disabled}>
</div> </div>
</label> </label>
@@ -42,4 +45,7 @@
} }
} }
} }
input[disabled] {
cursor: not-allowed;
}
</style> </style>

View File

@@ -3,17 +3,21 @@
import { TextBox, Checkbox } from "@gradio/form"; import { TextBox, Checkbox } from "@gradio/form";
import { LGraphNode } from "@litegraph-ts/core" import { LGraphNode } from "@litegraph-ts/core"
import layoutState, { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec } from "$lib/stores/layoutState" import layoutState, { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec } from "$lib/stores/layoutState"
import { get } from "svelte/store" import uiState from "$lib/stores/uiState"
import { get, type Writable, writable } from "svelte/store"
import type { ComfyWidgetNode } from "$lib/nodes"; import type { ComfyWidgetNode } from "$lib/nodes";
import ComfyNumberProperty from "./ComfyNumberProperty.svelte"; import ComfyNumberProperty from "./ComfyNumberProperty.svelte";
import ComfyComboProperty from "./ComfyComboProperty.svelte"; import ComfyComboProperty from "./ComfyComboProperty.svelte";
let target: IDragItem | null = null; let target: IDragItem | null = null;
let node: LGraphNode | null = null; let node: LGraphNode | null = null;
let attrsChanged: Writable<boolean> | null = null;
let refreshPanel: Writable<number> = writable(0);
$: if ($layoutState.currentSelection.length > 0) { $: if ($layoutState.currentSelection.length > 0) {
const targetId = $layoutState.currentSelection.slice(-1)[0] const targetId = $layoutState.currentSelection.slice(-1)[0]
target = $layoutState.allItems[targetId].dragItem target = $layoutState.allItems[targetId].dragItem
attrsChanged = target.attrsChanged;
if (target.type === "widget") { if (target.type === "widget") {
node = (target as WidgetLayout).node node = (target as WidgetLayout).node
} }
@@ -24,10 +28,32 @@
else if ($layoutState.currentSelectionNodes.length > 0) { else if ($layoutState.currentSelectionNodes.length > 0) {
target = null; target = null;
node = $layoutState.currentSelectionNodes[0] node = $layoutState.currentSelectionNodes[0]
attrsChanged = null;
} }
else { else {
target = null target = null
node = 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 = "???" let targetType: string = "???"
@@ -41,17 +67,79 @@
targetType = "" targetType = ""
} }
function validNodeProperty(spec: AttributesSpec, node: LGraphNode): boolean { 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) { if (spec.validNodeTypes) {
return spec.validNodeTypes.indexOf(node.type) !== -1; return spec.validNodeTypes.indexOf(node.type) !== -1;
} }
return spec.name in node.properties return spec.name in node.properties
} }
function updateAttribute(entry: AttributesSpec, target: IDragItem, value: any) { function validNodeVar(spec: AttributesSpec, node: LGraphNode | null): boolean {
if (target) { if (node == null || spec.location !== "nodeVars")
const name = entry.name return false;
console.warn("updateAttribute", name, value)
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.attrs[name] = value
target.attrsChanged.set(!get(target.attrsChanged)) target.attrsChanged.set(!get(target.attrsChanged))
@@ -60,12 +148,19 @@
const comfyNode = node as ComfyWidgetNode const comfyNode = node as ComfyWidgetNode
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1) comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
} }
console.warn(spec)
if (spec.refreshPanelOnChange) {
console.error("A! refresh")
$refreshPanel += 1;
} }
} }
function updateProperty(entry: AttributesSpec, value: any) { function updateProperty(spec: AttributesSpec, value: any) {
if (node) { if (node == null || !spec.editable)
const name = entry.name return
const name = spec.name
console.warn("updateProperty", name, value) console.warn("updateProperty", name, value)
node.properties[name] = value; node.properties[name] = value;
@@ -74,37 +169,47 @@
const comfyNode = node as ComfyWidgetNode const comfyNode = node as ComfyWidgetNode
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1) comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
} }
}
if (spec.refreshPanelOnChange)
$refreshPanel += 1;
} }
function getVar(node: LGraphNode, entry: AttributesSpec) { function getVar(node: LGraphNode, spec: AttributesSpec) {
let value = node[entry.name] let value = node[spec.name]
if (entry.serialize) if (value == null)
value = entry.serialize(value) value = spec.defaultValue
console.debug("[ComfyProperties] getVar", entry, value, node) else if (spec.serialize)
value = spec.serialize(value)
console.debug("[ComfyProperties] getVar", spec, value, node)
return value return value
} }
function updateVar(entry: any, value: any) { function updateVar(spec: AttributesSpec, value: any) {
if (node) { if (node == null || !spec.editable)
const name = entry.name return;
console.warn("updateProperty", name, value)
if (entry.deserialize) const name = spec.name
value = entry.deserialize(value)
console.debug("[ComfyProperties] updateVar", spec, value, name, node)
if (spec.deserialize)
value = spec.deserialize(value)
console.debug("[ComfyProperties] updateVar", entry, value, name, node)
node[name] = value; node[name] = value;
if ("propsChanged" in node) { if ("propsChanged" in node) {
const comfyNode = node as ComfyWidgetNode const comfyNode = node as ComfyWidgetNode
comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1) comfyNode.propsChanged.set(get(comfyNode.propsChanged) + 1)
} }
}
if (spec.refreshPanelOnChange)
$refreshPanel += 1;
} }
function updateWorkflowAttribute(entry: AttributesSpec, value: any) { function updateWorkflowAttribute(spec: AttributesSpec, value: any) {
const name = entry.name if (!spec.editable)
return;
const name = spec.name
console.warn("updateWorkflowAttribute", name, value) console.warn("updateWorkflowAttribute", name, value)
$layoutState.attrs[name] = value $layoutState.attrs[name] = value
@@ -121,82 +226,96 @@
<span class="type">({targetType})</span> <span class="type">({targetType})</span>
{/if} {/if}
</span> </span>
</span>
</div> </div>
</div> </div>
<div class="props-entries"> <div class="props-entries">
{#key $refreshPanel}
{#each ALL_ATTRIBUTES as category(category.categoryName)} {#each ALL_ATTRIBUTES as category(category.categoryName)}
<div class="category-name"> <div class="category-name">
<span> <span>
<span class="title">{category.categoryName}</span> <span class="title">{category.categoryName}</span>
</span> </span>
</div> </div>
{#each category.specs as spec(spec.name)} {#each category.specs as spec(spec.id)}
{#if spec.location === "widget" && target && spec.name in target.attrs} {#if validWidgetAttribute(spec, target)}
<div class="props-entry"> <div class="props-entry">
{#if spec.type === "string"} {#if spec.type === "string"}
<TextBox <TextBox
value={target.attrs[spec.name]} value={getAttribute(target, spec)}
on:change={(e) => updateAttribute(spec, target, e.detail)} on:change={(e) => updateAttribute(spec, target, e.detail)}
on:input={(e) => updateAttribute(spec, target, e.detail)} on:input={(e) => updateAttribute(spec, target, e.detail)}
disabled={!$uiState.uiUnlocked || !spec.editable}
label={spec.name} label={spec.name}
max_lines={1} max_lines={1}
/> />
{:else if spec.type === "boolean"} {:else if spec.type === "boolean"}
<Checkbox <Checkbox
value={target.attrs[spec.name]} value={getAttribute(target, spec)}
on:change={(e) => updateAttribute(spec, target, e.detail)} on:change={(e) => updateAttribute(spec, target, e.detail)}
disabled={!$uiState.uiUnlocked || !spec.editable}
label={spec.name} label={spec.name}
/> />
{:else if spec.type === "number"} {:else if spec.type === "number"}
<ComfyNumberProperty <ComfyNumberProperty
name={spec.name} name={spec.name}
value={target.attrs[spec.name]} value={getAttribute(target, spec)}
step={1} 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)} on:change={(e) => updateAttribute(spec, target, e.detail)}
/> />
{:else if spec.type === "enum"} {:else if spec.type === "enum"}
<ComfyComboProperty <ComfyComboProperty
name={spec.name} name={spec.name}
value={target.attrs[spec.name]} value={getAttribute(target, spec)}
values={spec.values} values={spec.values}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateAttribute(spec, target, e.detail)} on:change={(e) => updateAttribute(spec, target, e.detail)}
/> />
{/if} {/if}
</div> </div>
{:else if node} {:else if node}
{#if spec.location === "nodeProps" && validNodeProperty(spec, node)} {#if validNodeProperty(spec, node)}
<div class="props-entry"> <div class="props-entry">
{#if spec.type === "string"} {#if spec.type === "string"}
<TextBox <TextBox
value={node.properties[spec.name]} value={node.properties[spec.name] || spec.defaultValue}
on:change={(e) => updateProperty(spec, e.detail)} on:change={(e) => updateProperty(spec, e.detail)}
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}
max_lines={1} max_lines={1}
/> />
{:else if spec.type === "boolean"} {:else if spec.type === "boolean"}
<Checkbox <Checkbox
value={node.properties[spec.name]} value={node.properties[spec.name] || spec.defaultValue}
label={spec.name} label={spec.name}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateProperty(spec, e.detail)} on:change={(e) => updateProperty(spec, e.detail)}
/> />
{:else if spec.type === "number"} {:else if spec.type === "number"}
<ComfyNumberProperty <ComfyNumberProperty
name={spec.name} name={spec.name}
value={node.properties[spec.name]} value={node.properties[spec.name] || spec.defaultValue}
step={1} step={spec.step || 1}
min={spec.min || -1024}
max={spec.max || 1024}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateProperty(spec, e.detail)} on:change={(e) => updateProperty(spec, e.detail)}
/> />
{:else if spec.type === "enum"} {:else if spec.type === "enum"}
<ComfyComboProperty <ComfyComboProperty
name={spec.name} name={spec.name}
value={node.properties[spec.name]} value={node.properties[spec.name] || spec.defaultValue}
values={spec.values} values={spec.values}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateProperty(spec, e.detail)} on:change={(e) => updateProperty(spec, e.detail)}
/> />
{/if} {/if}
</div> </div>
{:else if spec.location === "nodeVars" && spec.name in node} {:else if validNodeVar(spec, node)}
<div class="props-entry"> <div class="props-entry">
{#if spec.type === "string"} {#if spec.type === "string"}
<TextBox <TextBox
@@ -204,19 +323,24 @@
on:change={(e) => updateVar(spec, e.detail)} on:change={(e) => updateVar(spec, e.detail)}
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}
max_lines={1} max_lines={1}
/> />
{:else if spec.type === "boolean"} {:else if spec.type === "boolean"}
<Checkbox <Checkbox
value={getVar(node, spec)} value={getVar(node, spec)}
on:change={(e) => updateVar(spec, e.detail)} on:change={(e) => updateVar(spec, e.detail)}
disabled={!$uiState.uiUnlocked || !spec.editable}
label={spec.name} label={spec.name}
/> />
{:else if spec.type === "number"} {:else if spec.type === "number"}
<ComfyNumberProperty <ComfyNumberProperty
name={spec.name} name={spec.name}
value={getVar(node, spec)} value={getVar(node, spec)}
step={1} step={spec.step || 1}
min={spec.min || -1024}
max={spec.max || 1024}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateVar(spec, e.detail)} on:change={(e) => updateVar(spec, e.detail)}
/> />
{:else if spec.type === "enum"} {:else if spec.type === "enum"}
@@ -224,39 +348,46 @@
name={spec.name} name={spec.name}
value={getVar(node, spec)} value={getVar(node, spec)}
values={spec.values} values={spec.values}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateVar(spec, e.detail)} on:change={(e) => updateVar(spec, e.detail)}
/> />
{/if} {/if}
</div> </div>
{/if} {/if}
{:else if spec.location === "workflow" && spec.name in $layoutState.attrs} {:else if !node && !target && validWorkflowAttribute(spec)}
<div class="props-entry"> <div class="props-entry">
{#if spec.type === "string"} {#if spec.type === "string"}
<TextBox <TextBox
value={$layoutState.attrs[spec.name]} value={$layoutState.attrs[spec.name] || spec.defaultValue}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)} on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
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}
max_lines={1} max_lines={1}
/> />
{:else if spec.type === "boolean"} {:else if spec.type === "boolean"}
<Checkbox <Checkbox
value={$layoutState.attrs[spec.name]} value={$layoutState.attrs[spec.name] || spec.defaultValue}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)} on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
disabled={!$uiState.uiUnlocked || !spec.editable}
label={spec.name} label={spec.name}
/> />
{:else if spec.type === "number"} {:else if spec.type === "number"}
<ComfyNumberProperty <ComfyNumberProperty
name={spec.name} name={spec.name}
value={$layoutState.attrs[spec.name]} value={$layoutState.attrs[spec.name] || spec.defaultValue}
step={1} step={spec.step || 1}
min={spec.min || -1024}
max={spec.max || 1024}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)} on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
/> />
{:else if spec.type === "enum"} {:else if spec.type === "enum"}
<ComfyComboProperty <ComfyComboProperty
name={spec.name} name={spec.name}
value={$layoutState.attrs[spec.name]} value={$layoutState.attrs[spec.name] || spec.defaultValue}
values={spec.values} values={spec.values}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)} on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
/> />
@@ -264,6 +395,7 @@
</div> </div>
{/if} {/if}
{/each} {/each}
{/each}
{/key} {/key}
</div> </div>
</div> </div>
@@ -283,6 +415,11 @@
padding: 0.8rem 1.0rem; padding: 0.8rem 1.0rem;
.title { .title {
font-weight: bold;
.type {
padding-left: 0.25rem;
font-weight: normal;
} }
} }
} }

View File

@@ -50,7 +50,7 @@
function ungroup() { function ungroup() {
const item = layoutState.getCurrentSelection()[0] const item = layoutState.getCurrentSelection()[0]
if (item.type !== "container") if (!item || item.type !== "container")
return; return;
$layoutState.currentSelection = [] $layoutState.currentSelection = []
@@ -69,7 +69,7 @@
} }
async function onRightClick(e) { async function onRightClick(e) {
if ($uiState.uiEditMode === "disabled") if (!$uiState.uiUnlocked)
return; return;
e.preventDefault(); 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 layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { startDrag, stopDrag } from "$lib/utils" import { startDrag, stopDrag } from "$lib/utils"
import BlockContainer from "./BlockContainer.svelte" import Container from "./Container.svelte"
import { type Writable } from "svelte/store" import { type Writable } from "svelte/store"
import type { ComfyWidgetNode } from "$lib/nodes"; import type { ComfyWidgetNode } from "$lib/nodes";
@@ -40,7 +40,8 @@
propsChanged = null; propsChanged = null;
} }
$: showHandles = $uiState.uiEditMode === "widgets" // TODO $: showHandles = $uiState.uiUnlocked
&& $uiState.uiEditMode === "widgets" // TODO
&& zIndex > 1 && zIndex > 1
&& !$layoutState.isMenuOpen && !$layoutState.isMenuOpen
@@ -58,15 +59,15 @@
{#if container} {#if container}
{#key $attrsChanged} {#key $attrsChanged}
<BlockContainer {container} {classes} {zIndex} {showHandles} /> <Container {container} {classes} {zIndex} {showHandles} />
{/key} {/key}
{:else if widget && widget.node} {:else if widget && widget.node}
{@const edit = $uiState.uiEditMode === "widgets" && zIndex > 1} {@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets" && zIndex > 1}
{#key $attrsChanged} {#key $attrsChanged}
{#key $propsChanged} {#key $propsChanged}
<div class="widget {widget.attrs.classes} {getWidgetClass()}" <div class="widget {widget.attrs.classes} {getWidgetClass()}"
class:edit={edit} 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:is-executing={$queueState.runningNodeId && $queueState.runningNodeId == widget.node.id}
class:hidden={widget.attrs.hidden} class:hidden={widget.attrs.hidden}
> >
@@ -83,9 +84,13 @@
{/if} {/if}
<style lang="scss"> <style lang="scss">
.widget.selected { .widget {
height: 100%;
&.selected {
background: var(--color-yellow-200); background: var(--color-yellow-200);
} }
}
.container.selected { .container.selected {
background: var(--color-yellow-400); 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 type { SerializedPrompt } from "$lib/components/ComfyApp";
import { toast } from '@zerodevx/svelte-toast' import { toast } from '@zerodevx/svelte-toast'
import type { GalleryOutput } from "./ComfyWidgetNodes"; import type { GalleryOutput } from "./ComfyWidgetNodes";
import { get } from "svelte/store";
import queueState from "$lib/stores/queueState";
export interface ComfyQueueEventsProperties extends Record<any, any> { export interface ComfyQueueEventsProperties extends Record<any, any> {
prompt: SerializedPrompt | null
} }
export class ComfyQueueEvents extends ComfyGraphNode { export class ComfyQueueEvents extends ComfyGraphNode {
override properties: ComfyQueueEventsProperties = { override properties: ComfyQueueEventsProperties = {
prompt: null
} }
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
@@ -22,29 +22,29 @@ export class ComfyQueueEvents extends ComfyGraphNode {
], ],
} }
override onPropertyChanged(property: string, value: any, prevValue?: any) { private getActionParams(subgraph: string | null): any {
if (property === "value") { let queue = get(queueState)
this.setOutputData(2, this.properties.prompt) let remaining = 0;
if (typeof queue.queueRemaining === "number")
remaining = queue.queueRemaining
return {
queueRemaining: remaining,
subgraph
} }
} }
override onExecute() { override beforeQueued(subgraph: string | null) {
this.setOutputData(2, this.properties.prompt) this.triggerSlot(0, this.getActionParams(subgraph))
} }
override beforeQueued() { override afterQueued(p: SerializedPrompt, subgraph: string | null) {
this.setProperty("value", null) this.triggerSlot(1, this.getActionParams(subgraph))
this.triggerSlot(0, "bang")
}
override afterQueued(p: SerializedPrompt) {
this.setProperty("value", p)
this.triggerSlot(1, "bang")
} }
override onSerialize(o: SerializedLGraphNode) { override onSerialize(o: SerializedLGraphNode) {
super.onSerialize(o) super.onSerialize(o)
o.properties = { prompt: null }
} }
} }
@@ -112,7 +112,7 @@ export class ComfyCopyAction extends ComfyGraphNode {
{ name: "copy", type: BuiltInSlotType.ACTION } { name: "copy", type: BuiltInSlotType.ACTION }
], ],
outputs: [ outputs: [
{ name: "out", type: "*" } { name: "out", type: BuiltInSlotType.EVENT }
], ],
} }
@@ -130,13 +130,15 @@ export class ComfyCopyAction extends ComfyGraphNode {
} }
override onExecute() { override onExecute() {
if (this.getInputLink(0))
this.setProperty("value", this.getInputData(0)) this.setProperty("value", this.getInputData(0))
} }
override onAction(action: any, param: any) { override onAction(action: any, param: any) {
if (action === "copy") {
this.setProperty("value", this.getInputData(0)) this.setProperty("value", this.getInputData(0))
this.setOutputData(0, this.properties.value) this.triggerSlot(0, this.properties.value)
console.log("setData", this.properties.value) }
}; };
} }

View File

@@ -20,8 +20,8 @@ export type DefaultWidgetLayout = {
export default class ComfyGraphNode extends LGraphNode { export default class ComfyGraphNode extends LGraphNode {
isBackendNode?: boolean; isBackendNode?: boolean;
beforeQueued?(): void; beforeQueued?(subgraph: string | null): void;
afterQueued?(prompt: SerializedPrompt): void; afterQueued?(prompt: SerializedPrompt, subgraph: string | null): void;
onExecuted?(output: any): void; onExecuted?(output: any): void;
defaultWidgets?: DefaultWidgetLayout 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 ComfyGraphNode from "./ComfyGraphNode";
import type { GalleryOutput } from "./ComfyWidgetNodes"; import type { GalleryOutput } from "./ComfyWidgetNodes";
@@ -6,7 +6,8 @@ export interface ComfyImageCacheNodeProperties extends Record<any, any> {
images: GalleryOutput | null, images: GalleryOutput | null,
index: number, index: number,
filenames: Record<number, { filename: string | null, status: ImageCacheState }>, filenames: Record<number, { filename: string | null, status: ImageCacheState }>,
genNumber: number genNumber: number,
updateMode: "replace" | "append"
} }
type ImageCacheState = "none" | "uploading" | "failed" | "cached" type ImageCacheState = "none" | "uploading" | "failed" | "cached"
@@ -20,7 +21,8 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
images: null, images: null,
index: 0, index: 0,
filenames: {}, filenames: {},
genNumber: 0 genNumber: 0,
updateMode: "replace"
} }
static slotLayout: SlotLayout = { 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 _uploadPromise: Promise<void> | null = null;
private _state: ImageCacheState = "none"
stateWidget: ITextWidget; stateWidget: ITextWidget;
filenameWidget: ITextWidget; filenameWidget: ITextWidget;
modeWidget: IComboWidget;
constructor(name?: string) { constructor(name?: string) {
super(name) super(name)
@@ -57,6 +63,14 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
"" ""
); );
this.filenameWidget.disabled = true; 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) { override onPropertyChanged(property: string, value: any, prevValue?: any) {
@@ -66,13 +80,23 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
else else
this.properties.index = 0 this.properties.index = 0
} }
else if (property === "updateMode") {
this.modeWidget.value = value;
}
this.updateWidgets()
}
private updateWidgets() {
if (this.properties.filenames && this.properties.images) { if (this.properties.filenames && this.properties.images) {
const fileCount = this.properties.images.images.length; const fileCount = this.properties.images.images.length;
const cachedCount = Object.keys(this.properties.filenames).length const cachedCount = Object.keys(this.properties.filenames).length
console.warn(cachedCount, this.properties.filenames) console.warn(cachedCount, this.properties.filenames)
this.filenameWidget.value = `${fileCount} files, ${cachedCount} cached` this.filenameWidget.value = `${fileCount} files, ${cachedCount} cached`
} }
else {
this.filenameWidget.value = `No files cached`
}
} }
override onExecute() { override onExecute() {
@@ -185,6 +209,7 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
this.setProperty("images", null) this.setProperty("images", null)
this.setProperty("filenames", {}) this.setProperty("filenames", {})
this.setProperty("index", 0) this.setProperty("index", 0)
this.updateWidgets();
return return
} }
@@ -192,11 +217,24 @@ export default class ComfyImageCacheNode extends ComfyGraphNode {
if (link.data && "images" in link.data) { if (link.data && "images" in link.data) {
this.setProperty("genNumber", this.properties.genNumber + 1) this.setProperty("genNumber", this.properties.genNumber + 1)
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("images", link.data as GalleryOutput)
this.setProperty("filenames", {}) this.setProperty("filenames", {})
console.debug("[ComfyImageCacheNode] Received output!", link.data) }
console.debug("[ComfyImageCacheNode] Received output!", output, this.properties.updateMode, this.properties.images)
this.setIndex(0, true) 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 ComfyGraphNode from "./ComfyGraphNode";
import ComboWidget from "$lib/widgets/ComboWidget.svelte"; import ComboWidget from "$lib/widgets/ComboWidget.svelte";
import RangeWidget from "$lib/widgets/RangeWidget.svelte"; import RangeWidget from "$lib/widgets/RangeWidget.svelte";
import TextWidget from "$lib/widgets/TextWidget.svelte"; import TextWidget from "$lib/widgets/TextWidget.svelte";
import GalleryWidget from "$lib/widgets/GalleryWidget.svelte"; import GalleryWidget from "$lib/widgets/GalleryWidget.svelte";
import ButtonWidget from "$lib/widgets/ButtonWidget.svelte"; import ButtonWidget from "$lib/widgets/ButtonWidget.svelte";
import CheckboxWidget from "$lib/widgets/CheckboxWidget.svelte";
import type { SvelteComponentDev } from "svelte/internal"; import type { SvelteComponentDev } from "svelte/internal";
import { Watch } from "@litegraph-ts/nodes-basic"; import { Watch } from "@litegraph-ts/nodes-basic";
import type IComfyInputSlot from "$lib/IComfyInputSlot"; import type IComfyInputSlot from "$lib/IComfyInputSlot";
@@ -48,8 +49,11 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
override isBackendNode = false; override isBackendNode = false;
override serialize_widgets = true; override serialize_widgets = true;
outputIndex: number | null = 0; // input slots
inputIndex: number = 0; inputIndex: number = 0;
// output slots
outputIndex: number | null = 0;
changedIndex: number | null = 1; changedIndex: number | null = 1;
displayWidget: ITextWidget; displayWidget: ITextWidget;
@@ -94,7 +98,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
if (this.outputIndex !== null && this.outputs.length >= this.outputIndex) { if (this.outputIndex !== null && this.outputs.length >= this.outputIndex) {
this.setOutputData(this.outputIndex, get(this.value)) 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] const changedOutput = this.outputs[this.changedIndex]
if (changedOutput.type === BuiltInSlotType.EVENT) if (changedOutput.type === BuiltInSlotType.EVENT)
this.triggerSlot(this.changedIndex, "changed") this.triggerSlot(this.changedIndex, "changed")
@@ -118,10 +122,8 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
if (this.copyFromInputLink) { if (this.copyFromInputLink) {
if (this.inputs.length >= this.inputIndex) { if (this.inputs.length >= this.inputIndex) {
const data = this.getInputData(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) 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 = { static slotLayout: SlotLayout = {
inputs: [ inputs: [
{ name: "value", type: "number" } { name: "value", type: "number" },
{ name: "store", type: BuiltInSlotType.ACTION }
], ],
outputs: [ outputs: [
{ name: "value", type: "number" }, { name: "value", type: "number" },
@@ -250,6 +253,11 @@ export class ComfySliderNode extends ComfyWidgetNode<number> {
super(name, 0) super(name, 0)
} }
override onAction(action: any, param: any) {
if (action === "store" && typeof param === "number")
this.setValue(param)
}
override setValue(value: any) { override setValue(value: any) {
if (typeof value !== "number") if (typeof value !== "number")
return; return;
@@ -283,7 +291,8 @@ export class ComfyComboNode extends ComfyWidgetNode<string> {
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
inputs: [ inputs: [
{ name: "value", type: "string" } { name: "value", type: "string" },
{ name: "store", type: BuiltInSlotType.ACTION }
], ],
outputs: [ outputs: [
{ name: "value", type: "string" }, { name: "value", type: "string" },
@@ -323,6 +332,11 @@ export class ComfyComboNode extends ComfyWidgetNode<string> {
return true; return true;
} }
override onAction(action: any, param: any) {
if (action === "store" && typeof param === "string")
this.setValue(param)
}
override setValue(value: any) { override setValue(value: any) {
if (typeof value !== "string" || this.properties.values.indexOf(value) === -1) if (typeof value !== "string" || this.properties.values.indexOf(value) === -1)
return; return;
@@ -358,7 +372,8 @@ export class ComfyTextNode extends ComfyWidgetNode<string> {
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
inputs: [ inputs: [
{ name: "value", type: "string" } { name: "value", type: "string" },
{ name: "store", type: BuiltInSlotType.ACTION }
], ],
outputs: [ outputs: [
{ name: "value", type: "string" }, { name: "value", type: "string" },
@@ -372,6 +387,11 @@ export class ComfyTextNode extends ComfyWidgetNode<string> {
super(name, "") super(name, "")
} }
override onAction(action: any, param: any) {
if (action === "store")
this.setValue(param)
}
override setValue(value: any) { override setValue(value: any) {
super.setValue(`${value}`) super.setValue(`${value}`)
} }
@@ -397,13 +417,15 @@ export type GalleryOutputEntry = {
} }
export interface ComfyGalleryProperties extends ComfyWidgetProperties { export interface ComfyGalleryProperties extends ComfyWidgetProperties {
index: number index: number,
updateMode: "replace" | "append"
} }
export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> { export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
override properties: ComfyGalleryProperties = { override properties: ComfyGalleryProperties = {
defaultValue: [], defaultValue: [],
index: 0 index: 0,
updateMode: "replace"
} }
static slotLayout: SlotLayout = { 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 svelteComponentType = GalleryWidget
override copyFromInputLink = false; override copyFromInputLink = false;
override outputIndex = null; override outputIndex = null;
override changedIndex = null; override changedIndex = null;
modeWidget: IComboWidget;
constructor(name?: string) { constructor(name?: string) {
super(name, []) 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() { override onExecute() {
this.setOutputData(0, this.properties.index) this.setOutputData(0, this.properties.index)
} }
override onAction(action: any) { override onAction(action: any, param: any, options: { action_call?: string }) {
if (action === "clear") { if (action === "clear") {
this.setValue([]) this.setValue([])
} }
@@ -442,10 +477,14 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
const galleryItems: GradioFileData[] = this.convertItems(link.data) const galleryItems: GradioFileData[] = this.convertItems(link.data)
// const currentValue = get(this.value) if (this.properties.updateMode === "append") {
// this.setValue(currentValue.concat(galleryItems)) const currentValue = get(this.value)
this.setValue(currentValue.concat(galleryItems))
}
else {
this.setValue(galleryItems) this.setValue(galleryItems)
} }
}
this.setProperty("index", 0) this.setProperty("index", 0)
} }
} }
@@ -489,13 +528,13 @@ LiteGraph.registerNodeType({
}) })
export interface ComfyButtonProperties extends ComfyWidgetProperties { export interface ComfyButtonProperties extends ComfyWidgetProperties {
message: string param: string
} }
export class ComfyButtonNode extends ComfyWidgetNode<boolean> { export class ComfyButtonNode extends ComfyWidgetNode<boolean> {
override properties: ComfyButtonProperties = { override properties: ComfyButtonProperties = {
defaultValue: false, defaultValue: false,
message: "bang" param: "bang"
} }
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
@@ -514,8 +553,8 @@ export class ComfyButtonNode extends ComfyWidgetNode<boolean> {
onClick() { onClick() {
this.setValue(true) this.setValue(true)
this.triggerSlot(0, this.properties.message); this.triggerSlot(0, this.properties.param);
this.setValue(false) this.setValue(false) // TODO onRelease
} }
constructor(name?: string) { constructor(name?: string) {
@@ -529,3 +568,40 @@ LiteGraph.registerNodeType({
desc: "Button that triggers an event when clicked", desc: "Button that triggers an event when clicked",
type: "ui/button" 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'; import type { ComfyWidgetNode } from '$lib/nodes';
type DragItemEntry = { type DragItemEntry = {
/*
* Drag item.
*/
dragItem: IDragItem, dragItem: IDragItem,
/*
* Children of this drag item.
* Only applies if the drag item's type is "container"
*/
children: IDragItem[] | null, children: IDragItem[] | null,
/*
* Parent of this drag item.
*/
parent: IDragItem | null parent: IDragItem | null
} }
/*
* Global workflow attributes
*/
export type LayoutAttributes = { 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 defaultSubgraph: string
} }
/*
* Keeps track of the tree of UI components - widgets and the containers that
* group them together.
*/
export type LayoutState = { export type LayoutState = {
/*
* Root of the UI tree
*/
root: IDragItem | null, root: IDragItem | null,
/*
* All items indexed by their own ID
*/
allItems: Record<DragItemID, DragItemEntry>, 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>, allItemsByNode: Record<number, DragItemEntry>,
/*
* Next ID to use for instantiating a new drag item
*/
currentId: number, currentId: number,
/*
* Selected drag items.
*/
currentSelection: DragItemID[], currentSelection: DragItemID[],
/*
* Selected LGraphNodes inside the litegraph canvas.
*/
currentSelectionNodes: LGraphNode[], currentSelectionNodes: LGraphNode[],
/*
* If true, a saved workflow is being deserialized, so ignore any
* nodeAdded/nodeRemoved events.
*
* TODO: instead use LGraphAddNodeOptions.addedByDeserialize
*/
isConfiguring: boolean, isConfiguring: boolean,
/*
* If true, the right-click context menu is open
*/
isMenuOpen: boolean, isMenuOpen: boolean,
/*
* Global workflow attributes
*/
attrs: LayoutAttributes attrs: LayoutAttributes
} }
/**
* Attributes for both containers and nodes.
**/
export type Attributes = { export type Attributes = {
/*
* Flex direction for containers.
*/
direction: "horizontal" | "vertical", direction: "horizontal" | "vertical",
/*
* Display name of this item.
*/
title: string, title: string,
showTitle: boolean,
/*
* List of classes to apply to the component.
*/
classes: string, 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, hidden?: boolean,
/*
* If true, grey out this component in the UI
*/
disabled?: boolean, 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 = { 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, 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" location: "widget" | "nodeProps" | "nodeVars" | "workflow"
/*
* Can this attribute be edited in the properties pane.
*/
editable: boolean, 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[], 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[], 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, 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, 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 = { export type AttributesCategorySpec = {
categoryName: string, categoryName: string,
specs: AttributesSpec[] specs: AttributesSpec[]
@@ -59,6 +256,17 @@ export type AttributesCategorySpec = {
export type AttributesSpecList = 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 = [ const ALL_ATTRIBUTES: AttributesSpecList = [
{ {
categoryName: "appearance", categoryName: "appearance",
@@ -67,18 +275,21 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
name: "title", name: "title",
type: "string", type: "string",
location: "widget", location: "widget",
defaultValue: "",
editable: true, editable: true,
}, },
{ {
name: "hidden", name: "hidden",
type: "boolean", type: "boolean",
location: "widget", location: "widget",
defaultValue: false,
editable: true editable: true
}, },
{ {
name: "disabled", name: "disabled",
type: "boolean", type: "boolean",
location: "widget", location: "widget",
defaultValue: false,
editable: true editable: true
}, },
{ {
@@ -86,26 +297,74 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
type: "enum", type: "enum",
location: "widget", location: "widget",
editable: true, editable: true,
values: ["horizontal", "vertical"] values: ["horizontal", "vertical"],
defaultValue: "vertical",
canShow: (di: IDragItem) => di.type === "container"
}, },
{ {
name: "flexGrow", name: "flexGrow",
type: "number", type: "number",
location: "widget", location: "widget",
defaultValue: 100,
editable: true editable: true
}, },
{ {
name: "classes", name: "classes",
type: "string", type: "string",
location: "widget", location: "widget",
defaultValue: "",
editable: true, editable: true,
}, },
// Container variants
{ {
name: "blockVariant", name: "variant",
type: "enum", type: "enum",
location: "widget", location: "widget",
editable: true, 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", type: "string",
location: "nodeVars", location: "nodeVars",
editable: true, editable: true,
serialize: (arg: string[]) => arg.join(","), defaultValue: [],
deserialize: (arg: string) => { serialize: serializeStringArray,
if (arg === "") deserialize: deserializeStringArray
return []
return arg.split(",").map(s => s.trim())
}
}, },
// Range // Range
@@ -132,6 +388,9 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
type: "number", type: "number",
location: "nodeProps", location: "nodeProps",
editable: true, editable: true,
defaultValue: 0,
min: -2 ^ 16,
max: 2 ^ 16,
validNodeTypes: ["ui/slider"], validNodeTypes: ["ui/slider"],
}, },
{ {
@@ -139,6 +398,9 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
type: "number", type: "number",
location: "nodeProps", location: "nodeProps",
editable: true, editable: true,
defaultValue: 10,
min: -2 ^ 16,
max: 2 ^ 16,
validNodeTypes: ["ui/slider"], validNodeTypes: ["ui/slider"],
}, },
{ {
@@ -146,16 +408,31 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
type: "number", type: "number",
location: "nodeProps", location: "nodeProps",
editable: true, editable: true,
defaultValue: 1,
min: -2 ^ 16,
max: 2 ^ 16,
validNodeTypes: ["ui/slider"], validNodeTypes: ["ui/slider"],
}, },
// Button // Button
{ {
name: "message", name: "param",
type: "string", type: "string",
location: "nodeProps", location: "nodeProps",
editable: true, editable: true,
validNodeTypes: ["ui/button"], validNodeTypes: ["ui/button"],
defaultValue: "bang"
},
// gallery
{
name: "updateMode",
type: "enum",
location: "nodeProps",
editable: true,
validNodeTypes: ["ui/gallery"],
values: ["replace", "append"],
defaultValue: "replace"
}, },
// Workflow // Workflow
@@ -163,35 +440,80 @@ const ALL_ATTRIBUTES: AttributesSpecList = [
name: "defaultSubgraph", name: "defaultSubgraph",
type: "string", type: "string",
location: "workflow", 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 }; export { ALL_ATTRIBUTES };
/*
* Something that can be dragged around in the frontend - a widget or a container.
*/
export interface IDragItem { export interface IDragItem {
type: string, /*
* Type of the item.
*/
type: "container" | "widget",
/*
* Unique ID of the item.
*/
id: DragItemID, id: DragItemID,
/*
* If true, the node associated with this drag item is executing.
* Used to show an indicator on the widget/container.
*/
isNodeExecuting?: boolean, isNodeExecuting?: boolean,
/*
* Attributes for this drag item.
*/
attrs: Attributes, attrs: Attributes,
/*
* Hackish thing to indicate to Svelte that an attribute changed.
* TODO Use Writeable<Attributes> instead!
*/
attrsChanged: Writable<boolean> attrsChanged: Writable<boolean>
} }
/*
* A container (block, accordion, tabs). Has child drag items.
*/
export interface ContainerLayout extends IDragItem { export interface ContainerLayout extends IDragItem {
type: "container", type: "container",
} }
/*
* A widget (slider, dropdown, textbox...)
*/
export interface WidgetLayout extends IDragItem { export interface WidgetLayout extends IDragItem {
type: "widget", type: "widget",
/*
* litegraph node this widget is bound to.
*/
node: ComfyWidgetNode node: ComfyWidgetNode
} }
type DragItemID = string; type DragItemID = string;
type LayoutStateOps = { type LayoutStateOps = {
addContainer: (parent: ContainerLayout | null, attrs: Partial<Attributes>, index: number) => ContainerLayout, addContainer: (parent: ContainerLayout | null, attrs: Partial<Attributes>, index?: number) => ContainerLayout,
addWidget: (parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes>, index: number) => WidgetLayout, addWidget: (parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes>, index?: number) => WidgetLayout,
findDefaultContainerForInsertion: () => ContainerLayout | null, findDefaultContainerForInsertion: () => ContainerLayout | null,
updateChildren: (parent: IDragItem, children: IDragItem[]) => IDragItem[], updateChildren: (parent: IDragItem, children: IDragItem[]) => IDragItem[],
nodeAdded: (node: LGraphNode) => void, nodeAdded: (node: LGraphNode) => void,
@@ -241,7 +563,7 @@ function findDefaultContainerForInsertion(): ContainerLayout | null {
return 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 state = get(store);
const dragItem: ContainerLayout = { const dragItem: ContainerLayout = {
type: "container", type: "container",
@@ -249,10 +571,9 @@ function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes>
attrsChanged: writable(false), attrsChanged: writable(false),
attrs: { attrs: {
title: "Container", title: "Container",
showTitle: true,
direction: "vertical", direction: "vertical",
classes: "", classes: "",
blockVariant: "block", containerVariant: "block",
flexGrow: 100, flexGrow: 100,
...attrs ...attrs
} }
@@ -260,14 +581,14 @@ function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes>
const entry: DragItemEntry = { dragItem, children: [], parent: null }; const entry: DragItemEntry = { dragItem, children: [], parent: null };
state.allItems[dragItem.id] = entry; state.allItems[dragItem.id] = entry;
if (parent) { if (parent) {
moveItem(dragItem, parent) moveItem(dragItem, parent, index)
} }
console.debug("[layoutState] addContainer", state) console.debug("[layoutState] addContainer", state)
store.set(state) store.set(state)
return dragItem; 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 state = get(store);
const widgetName = "Widget" const widgetName = "Widget"
const dragItem: WidgetLayout = { const dragItem: WidgetLayout = {
@@ -277,7 +598,6 @@ function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partia
attrsChanged: writable(false), attrsChanged: writable(false),
attrs: { attrs: {
title: widgetName, title: widgetName,
showTitle: true,
direction: "horizontal", direction: "horizontal",
classes: "", classes: "",
flexGrow: 100, flexGrow: 100,
@@ -289,7 +609,7 @@ function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partia
state.allItems[dragItem.id] = entry; state.allItems[dragItem.id] = entry;
state.allItemsByNode[node.id] = entry; state.allItemsByNode[node.id] = entry;
console.debug("[layoutState] addWidget", state) console.debug("[layoutState] addWidget", state)
moveItem(dragItem, parent) moveItem(dragItem, parent, index)
return dragItem; return dragItem;
} }
@@ -313,23 +633,10 @@ function nodeAdded(node: LGraphNode) {
const parent = findDefaultContainerForInsertion(); const parent = findDefaultContainerForInsertion();
// Two cases where we want to add nodes: console.debug("[layoutState] nodeAdded", node)
// 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)
if ("svelteComponentType" in node) { if ("svelteComponentType" in node) {
addWidget(parent, node as ComfyWidgetNode); 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) { function removeEntry(state: LayoutState, id: DragItemID) {
@@ -367,7 +674,7 @@ function nodeRemoved(node: LGraphNode) {
store.set(state) 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 state = get(store)
const entry = state.allItems[target.id] const entry = state.allItems[target.id]
if (entry.parent && entry.parent.id === to.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) { if (entry.parent) {
const parentEntry = state.allItems[entry.parent.id]; const parentEntry = state.allItems[entry.parent.id];
const index = parentEntry.children.findIndex(c => c.id === target.id) const parentIndex = parentEntry.children.findIndex(c => c.id === target.id)
if (index !== -1) { if (parentIndex !== -1) {
parentEntry.children.splice(index, 1) parentEntry.children.splice(parentIndex, 1)
} }
else { else {
console.error(parentEntry) console.error(parentEntry)
@@ -387,7 +694,7 @@ function moveItem(target: IDragItem, to: ContainerLayout, index: number = -1) {
} }
const toEntry = state.allItems[to.id]; const toEntry = state.allItems[to.id];
if (index !== -1) if (index != null && index >= 0)
toEntry.children.splice(index, 0, target) toEntry.children.splice(index, 0, target)
else else
toEntry.children.push(target) toEntry.children.push(target)

View File

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

View File

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

View File

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

View File

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

View File

@@ -280,21 +280,51 @@ div.float {
border-right: 1px var(--ae-panel-border-color) !important; border-right: 1px var(--ae-panel-border-color) !important;
} }
// .container > .block { /**************/
// box-shadow: none !important; /* Accordions */
// border-color: var(--ae-panel-border-color) !important; /**************/
// border-radius: 0 !important;
// background: var(--ae-input-bg-color) !important;
// > .v-pane { .block.gradio-accordion {
// box-shadow: none !important; background-color: var(--ae-main-bg-color) !important;
// border-color: var(--ae-panel-border-color) !important; /*padding-bottom: 0 !important;*/
// border-radius: 0 !important; }
// background: var(--ae-input-bg-color) !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; color: var(--ae-main-bg-color) !important;
background-color: var(--ae-primary-color) !important; background-color: var(--ae-primary-color) !important;
} }
@@ -308,7 +338,28 @@ div.float {
left: 0 !important; left: 0 !important;
top: 0 !important; top: 0 !important;
opacity: 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{ .form>.gradio-row>.form{
@@ -368,6 +419,10 @@ div.gradio-row>.form{
box-shadow: none !important; box-shadow: none !important;
} }
.gradio-checkbox > .inner > .block {
background-color: var(--ae-input-bg-color) !important;
}
.wrapper.gradio-textbox textarea { .wrapper.gradio-textbox textarea {
overflow-y: scroll; overflow-y: scroll;
box-sizing: border-box; box-sizing: border-box;
@@ -545,14 +600,18 @@ button.primary {
border-radius: var(--ae-panel-border-radius) !important; border-radius: var(--ae-panel-border-radius) !important;
background: var(--ae-input-bg-color) !important; background: var(--ae-input-bg-color) !important;
color: var(--ae-input-color) !important; color: var(--ae-input-color) !important;
}
button.secondary:hover, &:hover {
button.primary:hover {
background: var(--ae-primary-color) !important; background: var(--ae-primary-color) !important;
color: var(--ae-input-bg-color) !important; color: var(--ae-input-bg-color) !important;
} }
&:active {
background: var(--ae-input-bg-color) !important;
color: var(--ae-input-color) !important;
}
}
/**********************/ /**********************/
/* Sliders Scrollbars */ /* Sliders Scrollbars */
/**********************/ /**********************/
@@ -813,3 +872,29 @@ input[type=range]::-ms-fill-upper {
color: var(--ae-input-color) !important; 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({ export default defineConfig({
clearScreen: false, clearScreen: false,
plugins: [ plugins: [
FullReload(["src/**/*.{js,ts,scss,svelte}"]), // FullReload([
svelte(), , // // "src/**/*.{js,ts,scss,svelte}"
// "src/**/*.{scss}",
// "src/lib/stores/*.*",
// "src/**/ComfyApp.{ts,svelte}"
// ]),
svelte(),
viteStaticCopy({ viteStaticCopy({
targets: [ targets: [
{ {