blob: 5418079c55528a7f39324046280467bc39228d98 [file] [edit]
// 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.
@import "../theme";
.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;
}
}