diff --git a/app/gui/integration-test/project-view/edgeRendering.spec.ts b/app/gui/integration-test/project-view/edgeRendering.spec.ts
index f1de01f3dc58..dfcd8ada7ac6 100644
--- a/app/gui/integration-test/project-view/edgeRendering.spec.ts
+++ b/app/gui/integration-test/project-view/edgeRendering.spec.ts
@@ -65,8 +65,8 @@ test('Hover behaviour of edges', async ({ page }) => {
// Expect the top edge part to be dimmed
const topEdge = page.locator('svg.behindNodes g:nth-child(2) path:nth-child(1)')
- await expect(topEdge).toHaveClass('edge visible dimmed')
+ await expect(topEdge).toHaveClass('edge define-node-colors visible dimmed pending')
// Expect the bottom edge part not to be dimmed
const bottomEdge = page.locator('svg.behindNodes g:nth-child(2) path:nth-child(3)')
- await expect(bottomEdge).toHaveClass('edge visible')
+ await expect(bottomEdge).toHaveClass('edge define-node-colors visible pending')
})
diff --git a/app/gui/src/project-view/assets/base.css b/app/gui/src/project-view/assets/base.css
index 135bc3587525..3154fc2209d7 100644
--- a/app/gui/src/project-view/assets/base.css
+++ b/app/gui/src/project-view/assets/base.css
@@ -88,9 +88,15 @@
#aaa 40%
);
--color-node-text-missing-value: white;
+ --color-edge-from-node: color-mix(
+ in oklab,
+ var(--node-group-color) 85%,
+ white 15%
+ );
&.pending {
- --color-node-primary: var(--color-node-pending);
+ --color-node-background: var(--color-node-pending);
+ --color-edge-from-node: var(--color-node-pending);
}
&.selected {
@@ -109,6 +115,11 @@
var(--color-node-primary) 70%,
black 30%
);
+ --color-edge-from-node: color-mix(
+ in oklab,
+ var(--color-node-primary) 40%,
+ white 60%
+ );
}
}
diff --git a/app/gui/src/project-view/components/GraphEditor/GraphEdge.vue b/app/gui/src/project-view/components/GraphEditor/GraphEdge.vue
index 4818af27cf3c..eb5659f8767b 100644
--- a/app/gui/src/project-view/components/GraphEditor/GraphEdge.vue
+++ b/app/gui/src/project-view/components/GraphEditor/GraphEdge.vue
@@ -1,5 +1,6 @@
@@ -335,8 +335,8 @@ const baseClass = computed(() => {
{
{
v-if="arrowTransform"
:transform="arrowTransform"
:d="arrowPath"
- class="arrow visible"
- :class="{ dimmed: targetEndIsDimmed }"
+ class="arrow define-node-colors visible"
+ :class="{ ...colorClasses, dimmed: targetEndIsDimmed }"
:style="baseStyle"
/>
{
diff --git a/app/gui/src/project-view/components/GraphEditor/GraphNode.vue b/app/gui/src/project-view/components/GraphEditor/GraphNode.vue
index 5e7715298d24..a958c9e06ddc 100644
--- a/app/gui/src/project-view/components/GraphEditor/GraphNode.vue
+++ b/app/gui/src/project-view/components/GraphEditor/GraphNode.vue
@@ -18,6 +18,7 @@ import GraphVisualization from '@/components/GraphEditor/GraphVisualization.vue'
import type { NodeCreationOptions } from '@/components/GraphEditor/nodeCreation'
import PointFloatingMenu from '@/components/PointFloatingMenu.vue'
import SvgIcon from '@/components/SvgIcon.vue'
+import { useComponentColors } from '@/composables/componentColors'
import { useDoubleClick } from '@/composables/doubleClick'
import { usePointer, useResizeObserver } from '@/composables/events'
import { provideComponentButtons } from '@/providers/componentButtons'
@@ -159,8 +160,6 @@ const visibleMessage = computed(
const nodeHovered = ref(false)
-const selected = computed(() => nodeSelection?.isSelected(nodeId.value) ?? false)
-
const isOnlyOneSelected = computed(
() =>
nodeSelection?.committedSelection.size === 1 &&
@@ -307,9 +306,9 @@ const isRecordingOverridden = computed({
},
})
-const expressionInfo = computed(() => graph.db.getExpressionInfo(props.node.innerExpr.externalId))
-const executionState = computed(() => expressionInfo.value?.payload.type ?? 'Unknown')
-const color = computed(() => graph.db.getNodeColorStyle(nodeId.value))
+const typename = computed(
+ () => graph.db.getExpressionInfo(props.node.innerExpr.externalId)?.rawTypename,
+)
const nodeEditHandler = nodeEditBindings.handler({
cancel(e) {
@@ -362,16 +361,6 @@ const dataSource = computed(
() => ({ type: 'node', nodeId: props.node.rootExpr.externalId }) as const,
)
-const pending = computed(() => {
- switch (executionState.value) {
- case 'Unknown':
- case 'Pending':
- return true
- default:
- return false
- }
-})
-
// === Recompute node expression ===
function useRecomputation() {
@@ -394,7 +383,7 @@ const nodeStyle = computed(() => {
return {
transform: transform.value,
minWidth: isVisualizationEnabled.value ? `${visualizationWidth.value ?? 200}px` : undefined,
- '--node-group-color': color.value,
+ '--node-group-color': baseColor.value,
...(props.node.zIndex ? { 'z-index': props.node.zIndex } : {}),
'--viz-below-node': `${graphSelectionSize.value.y - nodeSize.value.y}px`,
'--node-size-x': `${nodeSize.value.x}px`,
@@ -402,6 +391,8 @@ const nodeStyle = computed(() => {
}
})
+const { baseColor, selected, pending } = useComponentColors(graph.db, nodeSelection, nodeId)
+
const nodeClass = computed(() => {
return {
selected: selected.value,
@@ -500,7 +491,7 @@ const showMenuAt = ref<{ x: number; y: number }>()
:isComponentMenuVisible="menuVisible"
:currentType="props.node.vis?.identifier"
:dataSource="dataSource"
- :typename="expressionInfo?.rawTypename"
+ :typename="typename"
:width="visualizationWidth"
:height="visualizationHeight"
:isFocused="isOnlyOneSelected"
diff --git a/app/gui/src/project-view/composables/componentColors.ts b/app/gui/src/project-view/composables/componentColors.ts
new file mode 100644
index 000000000000..aa3fb5a9ca70
--- /dev/null
+++ b/app/gui/src/project-view/composables/componentColors.ts
@@ -0,0 +1,27 @@
+import { type NodeId } from '@/stores/graph'
+import { type GraphDb } from '@/stores/graph/graphDatabase'
+import { type ToValue } from '@/util/reactivity'
+import { computed, toValue } from 'vue'
+
+/** Returns the component's base color, and information about state that may modify it. */
+export function useComponentColors(
+ graphDb: GraphDb,
+ nodeSelection: { isSelected: (nodeId: NodeId) => boolean } | undefined,
+ nodeId: ToValue,
+) {
+ const nodeIdValue = computed(() => toValue(nodeId))
+ const node = computed(() => nodeIdValue.value && graphDb.nodeIdToNode.get(nodeIdValue.value))
+ const expressionInfo = computed(
+ () => node.value && graphDb.getExpressionInfo(node.value.innerExpr.externalId),
+ )
+ const executionState = computed(() => expressionInfo.value?.payload.type ?? 'Unknown')
+ return {
+ baseColor: computed(() => nodeIdValue.value && graphDb.getNodeColorStyle(nodeIdValue.value)),
+ selected: computed(
+ () => (nodeIdValue.value && nodeSelection?.isSelected(nodeIdValue.value)) ?? false,
+ ),
+ pending: computed(
+ () => executionState.value === 'Unknown' || executionState.value === 'Pending',
+ ),
+ }
+}
diff --git a/app/gui/src/project-view/stores/graph/unconnectedEdges.ts b/app/gui/src/project-view/stores/graph/unconnectedEdges.ts
index 54b575e20d44..de87cc8e9bbb 100644
--- a/app/gui/src/project-view/stores/graph/unconnectedEdges.ts
+++ b/app/gui/src/project-view/stores/graph/unconnectedEdges.ts
@@ -14,8 +14,6 @@ interface AnyUnconnectedEdge {
disconnectedEdgeTarget?: PortId
/** Identifies what the disconnected end should be attached to. */
anchor: UnconnectedEdgeAnchor
- /** CSS value; if provided, overrides any color calculation. */
- color?: string
}
export interface UnconnectedSource extends AnyUnconnectedEdge {
source: undefined