Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/guide/styling.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``
Expand All @@ -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
---------------

Expand Down
3 changes: 3 additions & 0 deletions include/QtNodes/internal/NodeStyle.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ class NODE_EDITOR_PUBLIC NodeStyle : public Style
QColor GradientColor3;
QColor ShadowColor;
bool ShadowEnabled;
/// 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;

Expand Down
18 changes: 18 additions & 0 deletions src/DefaultNodePainter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
3 changes: 2 additions & 1 deletion src/NodeGraphicsObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/NodeStyle.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
NODE_STYLE_READ_BOOL(obj, FastShadowEnabled);
NODE_STYLE_READ_COLOR(obj, FontColor);
NODE_STYLE_READ_COLOR(obj, FontColorFaded);
NODE_STYLE_READ_COLOR(obj, ConnectionPointColor);
Expand All @@ -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);
NODE_STYLE_WRITE_BOOL(obj, FastShadowEnabled);
NODE_STYLE_WRITE_COLOR(obj, FontColor);
NODE_STYLE_WRITE_COLOR(obj, FontColorFaded);
NODE_STYLE_WRITE_COLOR(obj, ConnectionPointColor);
Expand Down
1 change: 1 addition & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
118 changes: 118 additions & 0 deletions test/src/TestFastNodeShadows.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#include "ApplicationSetup.hpp"
#include "TestGraphModel.hpp"

#include <catch2/catch.hpp>

#include <QtNodes/internal/AbstractNodeGeometry.hpp>
#include <QtNodes/internal/BasicGraphicsScene.hpp>
#include <QtNodes/internal/DefaultNodePainter.hpp>
#include <QtNodes/internal/NodeGraphicsObject.hpp>
#include <QtNodes/internal/NodeStyle.hpp>

#include <QImage>
#include <QJsonObject>
#include <QPainter>

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);

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 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 *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 = shadowEnabled;
style.FastShadowEnabled = true;
style.ShadowColor = QColor(0, 0, 0);
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);

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);
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) == 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);
}