| // Copyright (C) 2025 The Android Open Source Project | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| @use "../assets/theme" as *; | |
| .pf-canvas { | |
| isolation: isolate; // Don't allow z-index to escape container. | |
| overflow: hidden; // Clip overflowing content. | |
| position: relative; // Place absolute children relative to this container. | |
| height: 450px; // Give the canvas some intrinsic height by default. | |
| touch-action: none; // Disable default touch actions for panning. | |
| user-select: none; // Disable text selection during dragging. | |
| font-family: var(--pf-font-compact); // Consistent font. | |
| cursor: grab; // Indicate panning by default. | |
| // Dot pattern background. | |
| background-color: color-mix( | |
| in srgb, | |
| var(--pf-color-background) 90%, | |
| var(--pf-color-border-secondary) 10% | |
| ); | |
| background-image: radial-gradient( | |
| circle, | |
| color-mix(in srgb, var(--pf-color-border-secondary) 85%, black 15%) 1px, | |
| transparent 0px | |
| ); | |
| background-size: 20px 20px; | |
| &--fill-height { | |
| height: 100%; // Fill parent height when modifier class applied. | |
| } | |
| } | |
| .pf-canvas-content { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| overflow: visible; | |
| svg { | |
| isolation: isolate; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| overflow: visible; | |
| } | |
| } | |
| .pf-nodegraph-controls { | |
| position: absolute; | |
| top: 16px; | |
| right: 16px; | |
| z-index: 3; | |
| pointer-events: auto; | |
| display: flex; | |
| gap: 8px; | |
| } | |
| .pf-node { | |
| position: relative; | |
| background: var(--pf-color-background-secondary); | |
| border: 2px solid var(--pf-color-border); | |
| border-radius: 8px; | |
| min-width: 180px; | |
| cursor: move; | |
| pointer-events: auto; | |
| width: 100%; | |
| // Hide elements with this class, show on node hover | |
| .pf-show-on-hover { | |
| visibility: hidden; | |
| } | |
| &:hover .pf-show-on-hover { | |
| visibility: visible; | |
| } | |
| } | |
| .pf-node--has-accent-bar::before { | |
| content: ""; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 10px; | |
| height: 100%; | |
| background-color: var( | |
| --pf-color-border | |
| ); /* Fallback color when hue not set */ | |
| border-top-left-radius: 6px; | |
| border-bottom-left-radius: 6px; | |
| } | |
| /* Override with hue-based color when --pf-node-hue is set via inline styles */ | |
| .pf-node[style*="--pf-node-hue"].pf-node--has-accent-bar::before { | |
| background: color-mix( | |
| in srgb, | |
| hsl(var(--pf-node-hue), 60%, 50%) 50%, | |
| var(--pf-color-background) | |
| ); | |
| } | |
| .pf-node-body { | |
| padding-block: 12px; | |
| } | |
| // Wrapper for dock chains - uses flexbox to make children same width | |
| .pf-node-wrapper { | |
| isolation: isolate; // Don't allow z-index to escape container. | |
| position: absolute; | |
| display: flex; | |
| flex-direction: column; | |
| pointer-events: auto; | |
| box-shadow: 0 4px 12px var(--pf-color-box-shadow); | |
| border-radius: 8px; | |
| width: max-content; | |
| transition: | |
| left 0.1s ease-out, | |
| top 0.1s ease-out; | |
| &--dragging { | |
| z-index: 1; // Ensure dragging entire chain appears above other nodes. | |
| // Don't animate transitions while dragging to avoid laggy movement | |
| transition: none; | |
| // TODO Might want to add some visual indication of dragging entire chain | |
| // using box shadow, and maybe make slightly transparent. | |
| // opacity: 0.8; | |
| // box-shadow: 6px 6px 24px var(--pf-color-box-shadow); | |
| } | |
| } | |
| .pf-node.pf-selected { | |
| border-color: var(--pf-color-accent); | |
| box-shadow: 0 0 0 3px | |
| color-mix(in srgb, var(--pf-color-accent) 50%, transparent); | |
| z-index: 1; // Make sure box shadow appears above other nodes | |
| } | |
| // Docked child: flush top edge with no rounded corners or border | |
| .pf-node.pf-docked-child { | |
| border-top-left-radius: 0; | |
| border-top-right-radius: 0; | |
| margin-top: -2px; // Overlap border with parent | |
| } | |
| // Parent with docked child: flush bottom edge with no rounded corners | |
| .pf-node.pf-has-docked-child { | |
| border-bottom-left-radius: 0; | |
| border-bottom-right-radius: 0; | |
| } | |
| // Dock target highlight with pulsing animation | |
| .pf-node.pf-dock-target { | |
| box-shadow: 0 4px 0 3px var(--pf-color-accent); | |
| animation: pulse-dock 0.6s ease-in-out infinite; | |
| } | |
| @keyframes pulse-dock { | |
| 0%, | |
| 100% { | |
| box-shadow: 0 4px 0 3px var(--pf-color-accent); | |
| } | |
| 50% { | |
| box-shadow: 0 6px 0 5px var(--pf-color-accent); | |
| opacity: 0.95; | |
| } | |
| } | |
| .pf-node-header { | |
| /* Default background when hue is not set */ | |
| background: var(--pf-color-background); | |
| padding: 6px 6px; | |
| border-radius: 6px 6px 0 0; | |
| display: flex; | |
| border-bottom: 1px solid var(--pf-color-border); | |
| justify-content: space-between; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| /* Override with hue-based color when --pf-node-hue is set via inline styles */ | |
| .pf-node[style*="--pf-node-hue"] .pf-node-header { | |
| background: color-mix( | |
| in srgb, | |
| hsl(var(--pf-node-hue), 60%, 50%) 50%, | |
| var(--pf-color-background) | |
| ); | |
| } | |
| .pf-node--has-accent-bar .pf-node-header { | |
| padding-left: 22px; | |
| } | |
| .pf-docked-child .pf-node-header { | |
| border-top-left-radius: 0; | |
| border-top-right-radius: 0; | |
| } | |
| .pf-has-docked-child .pf-node-header { | |
| border-bottom-left-radius: 0; | |
| } | |
| .pf-docked-child::before { | |
| border-top-left-radius: 0; | |
| } | |
| .pf-has-docked-child::before { | |
| border-bottom-left-radius: 0; | |
| } | |
| .pf-node-title { | |
| flex: 1; | |
| } | |
| .pf-node-title-icon { | |
| align-self: center; | |
| } | |
| .pf-node-context-menu { | |
| position: absolute; | |
| top: 6px; | |
| right: 6px; | |
| } | |
| .pf-node-content { | |
| padding-inline: 12px; | |
| } | |
| .pf-node--has-accent-bar .pf-node-content { | |
| padding-left: 22px; | |
| } | |
| .pf-port-row { | |
| position: relative; | |
| margin: 8px 0; | |
| color: var(--pf-color-text-muted); | |
| font-size: 13px; | |
| padding: 4px 16px; | |
| .pf-port { | |
| transform: translateY(-50%); | |
| } | |
| } | |
| .pf-node--has-accent-bar .pf-port-row { | |
| padding-left: 22px; | |
| } | |
| .pf-port-input { | |
| text-align: left; | |
| } | |
| .pf-port-output { | |
| text-align: right; | |
| } | |
| .pf-port { | |
| width: 16px; | |
| height: 16px; | |
| border-radius: 50%; | |
| background: var(--pf-color-background-secondary); | |
| border: 2px solid var(--pf-color-border); | |
| cursor: crosshair; | |
| position: absolute; | |
| top: 50%; | |
| &--with-context-menu { | |
| cursor: pointer; | |
| &::after { | |
| // Render a little plus button to indicate context menu availability | |
| @include material-icon("add"); | |
| position: absolute; | |
| font-size: 15px; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| } | |
| } | |
| &.pf-connected { | |
| background: var(--pf-color-accent); | |
| border-color: var(--pf-color-accent); | |
| color: var(--pf-color-text-on-accent); | |
| } | |
| } | |
| .pf-output { | |
| background: var(--pf-color-border); | |
| z-index: 2; // Make outputs appear above inputs when docked. | |
| } | |
| .pf-output:hover { | |
| background: var(--pf-color-accent); | |
| border-color: var(--pf-color-accent); | |
| color: var(--pf-color-text-on-accent); | |
| } | |
| .pf-port.pf-input { | |
| left: -9px; | |
| } | |
| .pf-port.pf-port-top { | |
| top: -1px; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| } | |
| .pf-port.pf-port-bottom { | |
| top: unset; | |
| bottom: -1px; | |
| left: 50%; | |
| transform: translate(-50%, 50%); | |
| } | |
| .pf-port.pf-output { | |
| right: -9px; | |
| } | |
| .pf-connection { | |
| stroke: var(--pf-color-accent); | |
| color: var(--pf-color-accent); | |
| stroke-width: 2; | |
| fill: none; | |
| transition: | |
| stroke-width 0.2s, | |
| stroke 0.2s; | |
| pointer-events: auto; | |
| cursor: pointer; | |
| } | |
| .pf-connection-group:hover .pf-connection { | |
| stroke: var(--pf-color-danger); | |
| filter: drop-shadow( | |
| 0 0 6px color-mix(in srgb, var(--pf-color-danger) 80%, transparent) | |
| ); | |
| } | |
| .pf-temp-connection { | |
| stroke: var(--pf-color-text-muted); | |
| stroke-width: 2; | |
| fill: none; | |
| stroke-dasharray: 5, 5; | |
| } | |
| .pf-connecting { | |
| cursor: crosshair; | |
| .pf-input:hover { | |
| background: var(--pf-color-accent); | |
| border-color: var(--pf-color-accent); | |
| cursor: copy; | |
| } | |
| .pf-output:hover { | |
| background: var(--pf-color-border); | |
| border-color: var(--pf-color-border-secondary); | |
| cursor: not-allowed; | |
| } | |
| } | |
| .pf-output.pf-active, | |
| .pf-output.pf-active:hover { | |
| background: var(--pf-color-accent); | |
| border-color: var(--pf-color-accent); | |
| color: var(--pf-color-text-on-accent); | |
| cursor: crosshair; | |
| } | |
| .pf-panning { | |
| cursor: grabbing; | |
| } | |
| .pf-selection-rect { | |
| position: absolute; | |
| border: 2px solid var(--pf-color-accent); | |
| background: color-mix(in srgb, var(--pf-color-accent) 15%, transparent); | |
| pointer-events: none; | |
| z-index: 2; // Render selection rectangle above nodes. | |
| } | |
| .pf-node.pf-invalid { | |
| border-color: var(--pf-color-danger); | |
| background: color-mix( | |
| in srgb, | |
| var(--pf-color-danger) 8%, | |
| var(--pf-color-background-secondary) | |
| ); | |
| .pf-node-header { | |
| background: color-mix( | |
| in srgb, | |
| var(--pf-color-danger) 15%, | |
| var(--pf-color-background) | |
| ); | |
| border-bottom-color: var(--pf-color-danger); | |
| } | |
| } | |
| // Invalid nodes override hue-based styling | |
| .pf-node.pf-invalid[style*="--pf-node-hue"] .pf-node-header { | |
| background: color-mix( | |
| in srgb, | |
| var(--pf-color-danger) 15%, | |
| var(--pf-color-background) | |
| ); | |
| } | |
| .pf-node.pf-invalid.pf-node--has-accent-bar::before { | |
| background-color: var(--pf-color-danger); | |
| } | |
| // Label styles | |
| .pf-label { | |
| position: absolute; | |
| background: color-mix( | |
| in srgb, | |
| var(--pf-color-background-secondary) 85%, | |
| var(--pf-color-accent) 15% | |
| ); | |
| border: 2px solid var(--pf-color-border); | |
| cursor: move; | |
| pointer-events: auto; | |
| box-sizing: border-box; | |
| &.pf-dragging { | |
| opacity: 0.7; | |
| cursor: grabbing; | |
| } | |
| &.pf-selected { | |
| border-color: var(--pf-color-accent); | |
| box-shadow: 0 0 0 3px | |
| color-mix(in srgb, var(--pf-color-accent) 50%, transparent); | |
| } | |
| .pf-label-resize-handle { | |
| position: absolute; | |
| right: -5px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| width: 10px; | |
| height: 20px; | |
| background: var(--pf-color-accent); | |
| border-radius: 3px; | |
| cursor: ew-resize; | |
| opacity: 0; | |
| transition: opacity 0.2s; | |
| &:hover { | |
| opacity: 1; | |
| } | |
| } | |
| &:hover .pf-label-resize-handle { | |
| opacity: 0.6; | |
| } | |
| &.pf-selected .pf-label-resize-handle { | |
| opacity: 1; | |
| } | |
| .pf-label-delete-button { | |
| position: absolute; | |
| top: -10px; | |
| right: -10px; | |
| width: 20px; | |
| height: 20px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background: var(--pf-color-danger); | |
| color: white; | |
| border-radius: 3px; | |
| cursor: pointer; | |
| opacity: 0; | |
| transition: opacity 0.2s; | |
| &:hover { | |
| opacity: 1; | |
| background: color-mix(in srgb, var(--pf-color-danger) 85%, black 15%); | |
| } | |
| .pf-icon { | |
| font-size: var(--pf-font-size-s); | |
| } | |
| } | |
| &:hover .pf-label-delete-button, | |
| &.pf-selected .pf-label-delete-button { | |
| opacity: 1; | |
| } | |
| } | |
| .pf-label-content { | |
| width: 100%; | |
| max-height: 400px; | |
| overflow-y: auto; | |
| box-sizing: border-box; | |
| > .pf-simple-label-text { | |
| padding: 12px; | |
| font-family: var(--pf-font-compact); | |
| font-size: var(--pf-font-size-m); | |
| line-height: 1.4; | |
| color: var(--pf-color-text); | |
| } | |
| > .pf-simple-label-button { | |
| padding: 8px; | |
| } | |
| > .pf-label-placeholder { | |
| padding: 8px; | |
| color: var(--pf-color-text-secondary); | |
| font-style: italic; | |
| } | |
| } |