From b02e4a072a3f67416b499aa37612605bf46172f6 Mon Sep 17 00:00:00 2001 From: Doome161 <38245052+Doome161@users.noreply.github.com> Date: Thu, 22 Oct 2026 21:40:43 +0200 Subject: [PATCH 1/9] render: add opt-in lightweight node shadows --- include/QtNodes/internal/NodeStyle.hpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/include/QtNodes/internal/NodeStyle.hpp b/include/QtNodes/internal/NodeStyle.hpp index 01ecd2ea1..cc13947ca 100644 --- a/include/QtNodes/internal/NodeStyle.hpp +++ b/include/QtNodes/internal/NodeStyle.hpp @@ -61,6 +61,9 @@ class NODE_EDITOR_PUBLIC NodeStyle : public Style QColor GradientColor3; QColor ShadowColor; bool ShadowEnabled; + // Draw an inexpensive shadow with DefaultNodePainter instead of a graphics effect. + // False by default; custom painters are responsible for their own shadow. + bool FastShadowEnabled{false}; QColor FontColor; QColor FontColorFaded; From 99a281a98fd33bc26f7e25d29201056bb12e8e02 Mon Sep 17 00:00:00 2001 From: Doome161 <38245052+Doome161@users.noreply.github.com> Date: Thu, 22 Oct 2026 21:41:02 +0200 Subject: [PATCH 2/9] render: add opt-in lightweight node shadows --- src/NodeStyle.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/NodeStyle.cpp b/src/NodeStyle.cpp index 0cfacf51b..f67ab3df7 100644 --- a/src/NodeStyle.cpp +++ b/src/NodeStyle.cpp @@ -120,6 +120,7 @@ void NodeStyle::loadJson(QJsonObject const &json) NODE_STYLE_READ_COLOR(obj, GradientColor3); NODE_STYLE_READ_COLOR(obj, ShadowColor); NODE_STYLE_READ_BOOL(obj, ShadowEnabled); + FastShadowEnabled = obj.value("FastShadowEnabled").toBool(false); NODE_STYLE_READ_COLOR(obj, FontColor); NODE_STYLE_READ_COLOR(obj, FontColorFaded); NODE_STYLE_READ_COLOR(obj, ConnectionPointColor); @@ -146,6 +147,7 @@ QJsonObject NodeStyle::toJson() const NODE_STYLE_WRITE_COLOR(obj, GradientColor3); NODE_STYLE_WRITE_COLOR(obj, ShadowColor); NODE_STYLE_WRITE_BOOL(obj, ShadowEnabled); + obj["FastShadowEnabled"] = FastShadowEnabled; NODE_STYLE_WRITE_COLOR(obj, FontColor); NODE_STYLE_WRITE_COLOR(obj, FontColorFaded); NODE_STYLE_WRITE_COLOR(obj, ConnectionPointColor); From f3a2e46e5ffc43530fed8f5222b09e001aa8bf83 Mon Sep 17 00:00:00 2001 From: Doome161 <38245052+Doome161@users.noreply.github.com> Date: Thu, 22 Oct 2026 21:41:05 +0200 Subject: [PATCH 3/9] render: add opt-in lightweight node shadows --- src/NodeGraphicsObject.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NodeGraphicsObject.cpp b/src/NodeGraphicsObject.cpp index c13b92086..c80502205 100644 --- a/src/NodeGraphicsObject.cpp +++ b/src/NodeGraphicsObject.cpp @@ -42,7 +42,8 @@ NodeGraphicsObject::NodeGraphicsObject(BasicGraphicsScene &scene, NodeId nodeId) NodeStyle nodeStyle(nodeStyleJson); - if (nodeStyle.ShadowEnabled) { + // The lightweight shadow is drawn by DefaultNodePainter. + if (nodeStyle.ShadowEnabled && !nodeStyle.FastShadowEnabled) { auto effect = new QGraphicsDropShadowEffect; effect->setOffset(4, 4); effect->setBlurRadius(20); From b639c1e6412ab8379c5677522769402f0a14e861 Mon Sep 17 00:00:00 2001 From: Doome161 <38245052+Doome161@users.noreply.github.com> Date: Thu, 22 Oct 2026 21:41:53 +0200 Subject: [PATCH 4/9] render: paint optional lightweight node shadows --- src/DefaultNodePainter.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/DefaultNodePainter.cpp b/src/DefaultNodePainter.cpp index 2d3dae9ba..439dc1820 100644 --- a/src/DefaultNodePainter.cpp +++ b/src/DefaultNodePainter.cpp @@ -104,6 +104,24 @@ void DefaultNodePainter::drawNodeRect(QPainter *painter, NodeGraphicsObject &ngo double const radius = 3.0; + if (nodeStyle.ShadowEnabled && nodeStyle.FastShadowEnabled) { + // Keep the approximation within the margins reserved by default geometries. + // Unlike a graphics effect, this does not blur embedded widgets. + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, true); + painter->setPen(Qt::NoPen); + + QRectF const shadowBody = boundary.translated(4.0, 4.0); + for (int spread = 10; spread >= 1; --spread) { + QColor shade = nodeStyle.ShadowColor; + shade.setAlphaF(shade.alphaF() * 0.01 * (11 - spread)); + painter->setBrush(shade); + painter->drawRoundedRect(shadowBody.adjusted(-spread, -spread, spread, spread), + radius + spread, radius + spread); + } + painter->restore(); + } + painter->drawRoundedRect(boundary, radius, radius); } From a66d0be6d3c0e233e68ede594bf549dc947e313c Mon Sep 17 00:00:00 2001 From: Doome161 <38245052+Doome161@users.noreply.github.com> Date: Thu, 22 Oct 2026 21:42:02 +0200 Subject: [PATCH 5/9] test: register lightweight shadow regression tests --- test/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e8f5bdc1f..5e63771e2 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -20,6 +20,7 @@ add_executable(test_nodes src/TestDataFlow.cpp src/TestNodeValidation.cpp src/TestCustomPainters.cpp + src/TestFastNodeShadows.cpp src/TestCopyPaste.cpp src/TestZoomFeatures.cpp src/TestLoopDetection.cpp From f80143ab72201778793ad4709e8343a23a28720e Mon Sep 17 00:00:00 2001 From: Doome161 <38245052+Doome161@users.noreply.github.com> Date: Thu, 22 Oct 2026 21:42:36 +0200 Subject: [PATCH 6/9] test: cover opt-in shadow behavior and geometry --- test/src/TestFastNodeShadows.cpp | 86 ++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 test/src/TestFastNodeShadows.cpp diff --git a/test/src/TestFastNodeShadows.cpp b/test/src/TestFastNodeShadows.cpp new file mode 100644 index 000000000..0b482e087 --- /dev/null +++ b/test/src/TestFastNodeShadows.cpp @@ -0,0 +1,86 @@ +#include "ApplicationSetup.hpp" +#include "TestGraphModel.hpp" + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +TEST_CASE("Lightweight shadows are opt-in and survive style serialization", "[graphics][shadows]") +{ + auto app = applicationSetup(); + + QtNodes::NodeStyle legacyStyle; + legacyStyle.ShadowEnabled = true; + CHECK_FALSE(legacyStyle.FastShadowEnabled); + + auto legacyJson = legacyStyle.toJson(); + QJsonObject legacyNodeJson = legacyJson.value("NodeStyle").toObject(); + legacyNodeJson.remove("FastShadowEnabled"); + legacyJson["NodeStyle"] = legacyNodeJson; + CHECK_FALSE(QtNodes::NodeStyle(legacyJson).FastShadowEnabled); + + QtNodes::NodeStyle fastStyle; + fastStyle.ShadowEnabled = true; + fastStyle.FastShadowEnabled = true; + CHECK(QtNodes::NodeStyle(fastStyle.toJson()).FastShadowEnabled); + + TestGraphModel model; + auto const legacyId = model.addNode("legacy"); + auto const fastId = model.addNode("fast"); + REQUIRE(model.setNodeData(legacyId, QtNodes::NodeRole::Style, legacyStyle.toJson())); + REQUIRE(model.setNodeData(fastId, QtNodes::NodeRole::Style, fastStyle.toJson())); + + QtNodes::BasicGraphicsScene scene(model); + auto *legacyNode = scene.nodeGraphicsObject(legacyId); + auto *fastNode = scene.nodeGraphicsObject(fastId); + REQUIRE(legacyNode != nullptr); + REQUIRE(fastNode != nullptr); + CHECK(legacyNode->graphicsEffect() != nullptr); + CHECK(fastNode->graphicsEffect() == nullptr); +} + +TEST_CASE("Lightweight shadow fits within default node margins", "[graphics][shadows]") +{ + auto app = applicationSetup(); + + TestGraphModel model; + auto const id = model.addNode("fast"); + + QtNodes::NodeStyle style; + style.ShadowEnabled = true; + style.FastShadowEnabled = true; + style.ShadowColor = QColor(0, 0, 0); + REQUIRE(model.setNodeData(id, QtNodes::NodeRole::Style, style.toJson())); + + QtNodes::BasicGraphicsScene scene(model); + auto *node = scene.nodeGraphicsObject(id); + REQUIRE(node != nullptr); + + QSize const size = scene.nodeGeometry().size(id); + REQUIRE(size.width() > 0); + REQUIRE(size.height() > 0); + + // The default horizontal and vertical geometries reserve 20 px per side. + QRectF const shadowFootprint(-6, -6, size.width() + 20, size.height() + 20); + CHECK(scene.nodeGeometry().boundingRect(id).contains(shadowFootprint)); + + QImage image(size.width() + 60, size.height() + 60, QImage::Format_ARGB32_Premultiplied); + image.fill(Qt::transparent); + QPainter painter(&image); + painter.translate(25, 25); + QtNodes::DefaultNodePainter nodePainter; + nodePainter.drawNodeRect(&painter, *node); + painter.end(); + + int const midX = 25 + size.width() / 2; + CHECK(qAlpha(image.pixel(midX, 25 + size.height() + 6)) > 0); + CHECK(qAlpha(image.pixel(midX, 25 + size.height() + 19)) == 0); +} From 4d30dc89458a47e0db80eae46fda2b03529c1d99 Mon Sep 17 00:00:00 2001 From: Doome161 <38245052+Doome161@users.noreply.github.com> Date: Thu, 22 Oct 2026 21:42:46 +0200 Subject: [PATCH 7/9] docs: document opt-in lightweight node shadows --- docs/fast-node-shadows.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 docs/fast-node-shadows.md diff --git a/docs/fast-node-shadows.md b/docs/fast-node-shadows.md new file mode 100644 index 000000000..235258595 --- /dev/null +++ b/docs/fast-node-shadows.md @@ -0,0 +1,28 @@ +# Optional lightweight node shadows + +`NodeStyle` supports `FastShadowEnabled` (default: `false`). Set it in +the `NodeStyle` JSON object alongside `ShadowEnabled` to enable a +cheaper shadow approximation when using QtNodes' `DefaultNodePainter`: + +```json +{ + "NodeStyle": { + "ShadowEnabled": true, + "FastShadowEnabled": true + } +} +``` + +Keep other existing `NodeStyle` JSON fields unchanged. The snippet above +shows only the shadow-related settings. + +When enabled, the default painter renders a soft shadow using translucent +rounded rectangles rather than installing a `QGraphicsDropShadowEffect` on +the entire node. Embedded widgets are no longer blurred as part of the +graphics effect. The approximation is not pixel-identical to Qt's blur. + +The original rendering is the default. Custom node painters must draw their +own shadows when this option is enabled. This change deliberately does not +modify `QPixmapCache` globally or change embedded-widget cache modes. +Benchmark different graph sizes, viewport zoom levels, and DPI settings +before enabling it application-wide. From 5577092b06eda6fcbec14e95bf2b161daebcdf8d Mon Sep 17 00:00:00 2001 From: Doome161 <38245052+Doome161@users.noreply.github.com> Date: Sun, 25 Oct 2026 20:29:29 +0100 Subject: [PATCH 8/9] docs: remove redundant lightweight shadows guide --- docs/fast-node-shadows.md | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 docs/fast-node-shadows.md diff --git a/docs/fast-node-shadows.md b/docs/fast-node-shadows.md deleted file mode 100644 index 235258595..000000000 --- a/docs/fast-node-shadows.md +++ /dev/null @@ -1,28 +0,0 @@ -# Optional lightweight node shadows - -`NodeStyle` supports `FastShadowEnabled` (default: `false`). Set it in -the `NodeStyle` JSON object alongside `ShadowEnabled` to enable a -cheaper shadow approximation when using QtNodes' `DefaultNodePainter`: - -```json -{ - "NodeStyle": { - "ShadowEnabled": true, - "FastShadowEnabled": true - } -} -``` - -Keep other existing `NodeStyle` JSON fields unchanged. The snippet above -shows only the shadow-related settings. - -When enabled, the default painter renders a soft shadow using translucent -rounded rectangles rather than installing a `QGraphicsDropShadowEffect` on -the entire node. Embedded widgets are no longer blurred as part of the -graphics effect. The approximation is not pixel-identical to Qt's blur. - -The original rendering is the default. Custom node painters must draw their -own shadows when this option is enabled. This change deliberately does not -modify `QPixmapCache` globally or change embedded-widget cache modes. -Benchmark different graph sizes, viewport zoom levels, and DPI settings -before enabling it application-wide. From 94ac91372aace5037c0de0f601413913b956c9fd Mon Sep 17 00:00:00 2001 From: Doome161 <38245052+Doome161@users.noreply.github.com> Date: Sun, 25 Oct 2026 20:45:23 +0100 Subject: [PATCH 9/9] Fix lightweight shadow regression tests and document integration --- docs/guide/styling.rst | 33 +++++++++++++++ include/QtNodes/internal/NodeStyle.hpp | 4 +- src/NodeStyle.cpp | 4 +- test/src/TestFastNodeShadows.cpp | 58 ++++++++++++++++++++------ 4 files changed, 82 insertions(+), 17 deletions(-) diff --git a/docs/guide/styling.rst b/docs/guide/styling.rst index 344cb25d0..351c2266a 100644 --- a/docs/guide/styling.rst +++ b/docs/guide/styling.rst @@ -119,6 +119,8 @@ Controls node appearance: - Background gradient stops * - ``ShadowEnabled`` - Drop shadow on/off + * - ``FastShadowEnabled`` + - Approximate shadow drawn by the default painter; defaults to false * - ``ConnectionPointColor`` - Empty port circles * - ``FilledConnectionPointColor`` @@ -128,6 +130,37 @@ Controls node appearance: * - ``WarningColor`` - Validation warning indicator +Lightweight Node Shadows +~~~~~~~~~~~~~~~~~~~~~~~~ + +Set both ``ShadowEnabled`` and ``FastShadowEnabled`` to true to replace +``QGraphicsDropShadowEffect`` with translucent rounded rectangles drawn by +``DefaultNodePainter``. Configure the style before creating nodes: + +.. code-block:: cpp + + NodeStyle style = StyleCollection::nodeStyle(); + style.ShadowEnabled = true; + style.FastShadowEnabled = true; + StyleCollection::setNodeStyle(style); + +For a custom graph model, return ``style.toJson().toVariantMap()`` for +``NodeRole::Style``. The same setting is available as ``FastShadowEnabled`` +inside the ``NodeStyle`` JSON object. Older JSON without this key keeps the +original graphics effect. ``ShadowEnabled = false`` disables both modes. + +The approximation is not pixel-identical to Qt's blur and does not apply a +graphics effect to embedded widgets. Custom painters must draw their own +shadow when this option is enabled. Custom node geometries must include the +shadow in their ``boundingRect()``: it extends 6 logical pixels above and to +the left of the node body, and 14 below and to the right. Both default +geometries provide sufficient margins. + +This option avoids the separate graphics effect, but a performance gain is +not guaranteed. Measure your own graphs, zoom levels and display scaling +before choosing a mode. It does not change global pixmap caching or resolve +the underlying Qt graphics-effect caching issue. + ConnectionStyle --------------- diff --git a/include/QtNodes/internal/NodeStyle.hpp b/include/QtNodes/internal/NodeStyle.hpp index cc13947ca..865b3ef75 100644 --- a/include/QtNodes/internal/NodeStyle.hpp +++ b/include/QtNodes/internal/NodeStyle.hpp @@ -61,8 +61,8 @@ class NODE_EDITOR_PUBLIC NodeStyle : public Style QColor GradientColor3; QColor ShadowColor; bool ShadowEnabled; - // Draw an inexpensive shadow with DefaultNodePainter instead of a graphics effect. - // False by default; custom painters are responsible for their own shadow. + /// Draw an approximate shadow with DefaultNodePainter instead of a graphics effect. + /// Requires ShadowEnabled; custom painters are responsible for their own shadow. bool FastShadowEnabled{false}; QColor FontColor; QColor FontColorFaded; diff --git a/src/NodeStyle.cpp b/src/NodeStyle.cpp index f67ab3df7..26c112632 100644 --- a/src/NodeStyle.cpp +++ b/src/NodeStyle.cpp @@ -120,7 +120,7 @@ void NodeStyle::loadJson(QJsonObject const &json) NODE_STYLE_READ_COLOR(obj, GradientColor3); NODE_STYLE_READ_COLOR(obj, ShadowColor); NODE_STYLE_READ_BOOL(obj, ShadowEnabled); - FastShadowEnabled = obj.value("FastShadowEnabled").toBool(false); + NODE_STYLE_READ_BOOL(obj, FastShadowEnabled); NODE_STYLE_READ_COLOR(obj, FontColor); NODE_STYLE_READ_COLOR(obj, FontColorFaded); NODE_STYLE_READ_COLOR(obj, ConnectionPointColor); @@ -147,7 +147,7 @@ QJsonObject NodeStyle::toJson() const NODE_STYLE_WRITE_COLOR(obj, GradientColor3); NODE_STYLE_WRITE_COLOR(obj, ShadowColor); NODE_STYLE_WRITE_BOOL(obj, ShadowEnabled); - obj["FastShadowEnabled"] = FastShadowEnabled; + NODE_STYLE_WRITE_BOOL(obj, FastShadowEnabled); NODE_STYLE_WRITE_COLOR(obj, FontColor); NODE_STYLE_WRITE_COLOR(obj, FontColorFaded); NODE_STYLE_WRITE_COLOR(obj, ConnectionPointColor); diff --git a/test/src/TestFastNodeShadows.cpp b/test/src/TestFastNodeShadows.cpp index 0b482e087..d3c3fd89c 100644 --- a/test/src/TestFastNodeShadows.cpp +++ b/test/src/TestFastNodeShadows.cpp @@ -32,35 +32,50 @@ TEST_CASE("Lightweight shadows are opt-in and survive style serialization", "[gr fastStyle.FastShadowEnabled = true; CHECK(QtNodes::NodeStyle(fastStyle.toJson()).FastShadowEnabled); + fastStyle.loadJson(legacyJson); + CHECK_FALSE(fastStyle.FastShadowEnabled); +} + +TEST_CASE("Shadow flags select the graphics effect", "[graphics][shadows]") +{ + bool const shadowEnabled = GENERATE(false, true); + bool const fastShadowEnabled = GENERATE(false, true); + CAPTURE(shadowEnabled, fastShadowEnabled); + auto app = applicationSetup(); + + QtNodes::NodeStyle style; + style.ShadowEnabled = shadowEnabled; + style.FastShadowEnabled = fastShadowEnabled; + TestGraphModel model; - auto const legacyId = model.addNode("legacy"); - auto const fastId = model.addNode("fast"); - REQUIRE(model.setNodeData(legacyId, QtNodes::NodeRole::Style, legacyStyle.toJson())); - REQUIRE(model.setNodeData(fastId, QtNodes::NodeRole::Style, fastStyle.toJson())); + auto const id = model.addNode("shadow"); + // Match DataFlowGraphModel and the documented NodeRole::Style representation. + REQUIRE(model.setNodeData(id, QtNodes::NodeRole::Style, style.toJson().toVariantMap())); QtNodes::BasicGraphicsScene scene(model); - auto *legacyNode = scene.nodeGraphicsObject(legacyId); - auto *fastNode = scene.nodeGraphicsObject(fastId); - REQUIRE(legacyNode != nullptr); - REQUIRE(fastNode != nullptr); - CHECK(legacyNode->graphicsEffect() != nullptr); - CHECK(fastNode->graphicsEffect() == nullptr); + auto *node = scene.nodeGraphicsObject(id); + REQUIRE(node != nullptr); + CHECK((node->graphicsEffect() != nullptr) == (shadowEnabled && !fastShadowEnabled)); } TEST_CASE("Lightweight shadow fits within default node margins", "[graphics][shadows]") { + bool const vertical = GENERATE(false, true); + bool const shadowEnabled = GENERATE(false, true); + CAPTURE(vertical, shadowEnabled); auto app = applicationSetup(); TestGraphModel model; auto const id = model.addNode("fast"); QtNodes::NodeStyle style; - style.ShadowEnabled = true; + style.ShadowEnabled = shadowEnabled; style.FastShadowEnabled = true; style.ShadowColor = QColor(0, 0, 0); - REQUIRE(model.setNodeData(id, QtNodes::NodeRole::Style, style.toJson())); + REQUIRE(model.setNodeData(id, QtNodes::NodeRole::Style, style.toJson().toVariantMap())); QtNodes::BasicGraphicsScene scene(model); + scene.setOrientation(vertical ? Qt::Vertical : Qt::Horizontal); auto *node = scene.nodeGraphicsObject(id); REQUIRE(node != nullptr); @@ -76,11 +91,28 @@ TEST_CASE("Lightweight shadow fits within default node margins", "[graphics][sha image.fill(Qt::transparent); QPainter painter(&image); painter.translate(25, 25); + QTransform const transform = painter.transform(); + auto const renderHints = painter.renderHints(); QtNodes::DefaultNodePainter nodePainter; nodePainter.drawNodeRect(&painter, *node); + CHECK(painter.transform() == transform); + CHECK(painter.renderHints() == renderHints); + CHECK(painter.pen().style() != Qt::NoPen); + CHECK(painter.brush().style() == Qt::LinearGradientPattern); painter.end(); int const midX = 25 + size.width() / 2; - CHECK(qAlpha(image.pixel(midX, 25 + size.height() + 6)) > 0); + CHECK((qAlpha(image.pixel(midX, 25 + size.height() + 6)) > 0) == shadowEnabled); CHECK(qAlpha(image.pixel(midX, 25 + size.height() + 19)) == 0); + + QRectF const bounds = scene.nodeGeometry().boundingRect(id).translated(25, 25); + unsigned int pixelsOutsideBounds = 0; + for (int y = 0; y < image.height(); ++y) { + for (int x = 0; x < image.width(); ++x) { + if (qAlpha(image.pixel(x, y)) != 0 && !bounds.contains(QPointF(x, y))) { + ++pixelsOutsideBounds; + } + } + } + CHECK(pixelsOutsideBounds == 0); }