File indexing completed on 2025-02-02 04:26:10

0001 /* SPDX-FileCopyrightText: 2022 Marco Martin <mart@kde.org>
0002  * SPDX-FileCopyrightText: 2024 Noah Davis <noahadvs@gmail.com>
0003  * SPDX-License-Identifier: LGPL-2.0-or-later
0004  */
0005 
0006 #include "Traits.h"
0007 #include "Geometry.h"
0008 #include <QLocale>
0009 #include <QUuid>
0010 
0011 using namespace Qt::StringLiterals;
0012 
0013 // Stroke
0014 
0015 QPen Traits::Stroke::defaultPen()
0016 {
0017     return {Qt::NoBrush, 1.0, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin};
0018 }
0019 
0020 int Traits::Text::textFlags() const
0021 {
0022     return (index() == Text::String ? Qt::AlignLeft | Qt::AlignTop : Qt::AlignCenter) //
0023         | Qt::TextDontClip | Qt::TextExpandTabs | Qt::TextIncludeTrailingSpaces;
0024 }
0025 
0026 QString Traits::Text::text() const
0027 {
0028     if (index() == String) {
0029         return std::get<String>(*this);
0030     } else if (index() == Number) {
0031         return QLocale::system().toString(std::get<Number>(*this));
0032     }
0033     return {};
0034 }
0035 
0036 // ImageEffects
0037 
0038 static const auto factorKey = u"factor"_s;
0039 QImage imageCopyHelper(const QImage &image, const QRectF &copyRect)
0040 {
0041     if (copyRect.size() != image.size()) {
0042         return image.copy(std::floor<int>(copyRect.x()),
0043                           std::floor<int>(copyRect.y()), //
0044                           std::ceil<int>(copyRect.width()),
0045                           std::ceil<int>(copyRect.height()));
0046     }
0047     return image;
0048 }
0049 
0050 Traits::ImageEffects::Blur::Blur(uint factor)
0051     : factor(factor)
0052 {
0053 }
0054 
0055 bool Traits::ImageEffects::Blur::isValid() const
0056 {
0057     return factor > 1;
0058 }
0059 
0060 QImage Traits::ImageEffects::Blur::image(std::function<QImage()> getImage, QRectF rect, qreal dpr) const
0061 {
0062     if (!isValid()) {
0063         return {};
0064     }
0065     if ((backingStoreCache.isNull() //
0066          || backingStoreCache.devicePixelRatio() != dpr //
0067          || backingStoreCache.text(factorKey).toFloat() != factor)
0068         && getImage) {
0069         backingStoreCache = getImage();
0070         // Scale the factor with the devicePixelRatio.
0071         // This way high DPI pictures aren't visually affected less than standard DPI pictures.
0072         const auto effectFactor = factor * dpr;
0073         auto scaleDown = QTransform::fromScale(1 / effectFactor, 1 / effectFactor);
0074         auto scaleUp = QTransform::fromScale(effectFactor, effectFactor);
0075         // A poor man's blur. It's fast, but not high quality.
0076         // It's somewhat blocky, but it's definitely blurry.
0077         backingStoreCache = backingStoreCache.transformed(scaleDown, Qt::SmoothTransformation);
0078         backingStoreCache = backingStoreCache.transformed(scaleUp, Qt::SmoothTransformation);
0079         backingStoreCache.setDevicePixelRatio(dpr);
0080         backingStoreCache.setText(factorKey, QString::number(factor));
0081     }
0082     rect = ::Geometry::rectScaled(rect, backingStoreCache.devicePixelRatio());
0083     return imageCopyHelper(backingStoreCache, rect);
0084 }
0085 
0086 Traits::ImageEffects::Pixelate::Pixelate(uint factor)
0087     : factor(factor)
0088 {
0089 }
0090 
0091 bool Traits::ImageEffects::Pixelate::isValid() const
0092 {
0093     return factor > 1;
0094 }
0095 
0096 QImage Traits::ImageEffects::Pixelate::image(std::function<QImage()> getImage, QRectF rect, qreal dpr) const
0097 {
0098     if (!isValid()) {
0099         return {};
0100     }
0101     if ((backingStoreCache.isNull() //
0102          || backingStoreCache.devicePixelRatio() != dpr //
0103          || backingStoreCache.text(factorKey).toFloat() != factor)
0104         && getImage) {
0105         backingStoreCache = getImage();
0106         // Scale the factor with the devicePixelRatio.
0107         // This way high DPI pictures aren't visually affected less than standard DPI pictures.
0108         const auto effectFactor = factor * dpr;
0109         auto scaleDown = QTransform::fromScale(1 / effectFactor, 1 / effectFactor);
0110         auto scaleUp = QTransform::fromScale(effectFactor, effectFactor);
0111         // Smooth when scaling down to average out the colors.
0112         backingStoreCache = backingStoreCache.transformed(scaleDown, Qt::SmoothTransformation);
0113         backingStoreCache = backingStoreCache.transformed(scaleUp, Qt::FastTransformation);
0114         backingStoreCache.setDevicePixelRatio(dpr);
0115         backingStoreCache.setText(factorKey, QString::number(factor));
0116     }
0117     rect = ::Geometry::rectScaled(rect, backingStoreCache.devicePixelRatio());
0118     return imageCopyHelper(backingStoreCache, rect);
0119 }
0120 
0121 // Functions
0122 
0123 Traits::Translation Traits::unTranslateScale(qreal sx, qreal sy, const QPointF &oldPoint)
0124 {
0125     return {-oldPoint.x() * sx + oldPoint.x(), -oldPoint.y() * sy + oldPoint.y()};
0126 }
0127 
0128 Traits::Scale Traits::scaleForSize(const QSizeF &oldSize, const QSizeF &newSize)
0129 {
0130     // We should never divide by zero and we don't need fractional sizes less than 1.
0131     auto absWidth = std::abs(oldSize.width());
0132     auto absHeight = std::abs(oldSize.height());
0133     auto wSign = std::copysign(1.0, oldSize.width());
0134     auto hSign = std::copysign(1.0, oldSize.height());
0135     // Don't allow an absolute size less than 1x1.
0136     const auto wDivisor = std::max(1.0, absWidth) * wSign;
0137     const auto hDivisor = std::max(1.0, absHeight) * hSign;
0138     return {newSize.width() / wDivisor, newSize.height() / hDivisor};
0139 }
0140 
0141 QPainterPath Traits::minPath(const QPainterPath &path)
0142 {
0143     if (path.isEmpty()) {
0144         auto start = path.elementCount() > 0 ? path.elementAt(0) : QPainterPath::Element{};
0145         QPainterPath dotPath(start);
0146         dotPath.lineTo(start.x + 0.0001, start.y);
0147         return dotPath;
0148     }
0149     return path;
0150 }
0151 
0152 QPainterPath Traits::arrowHead(const QLineF &mainLine, qreal strokeWidth)
0153 {
0154     const auto &end = mainLine.p2();
0155     // This should leave a decently sized gap between the arrow head and shaft
0156     // and a decently sized length for all stroke widths.
0157     // Arrow head length will grow with stroke width.
0158     const qreal length = qMax(8.0, strokeWidth * 3.0);
0159     const qreal angle = mainLine.angle() + 180;
0160     auto headLine1 = QLineF::fromPolar(length, angle + 30).translated(end);
0161     auto headLine2 = QLineF::fromPolar(length, angle - 30).translated(end);
0162     QPainterPath path(headLine1.p2());
0163     path.lineTo(end);
0164     path.lineTo(headLine2.p2());
0165     return path;
0166 }
0167 
0168 QPainterPath Traits::createTextPath(const OptTuple &traits)
0169 {
0170     auto &geometry = std::get<Geometry::Opt>(traits);
0171     auto &text = std::get<Text::Opt>(traits);
0172     if (!geometry) {
0173         return {};
0174     }
0175     if (!text) {
0176         return geometry->path;
0177     }
0178     const auto &start = geometry->path.elementCount() > 0 ? geometry->path.elementAt(0) : QPainterPath::Element{};
0179     QRectF rect{start, start};
0180     QFontMetricsF fm(text->font);
0181     QPainterPath path{start};
0182     if (text->index() == Text::String) {
0183         // Same as QPainter's default
0184         const auto tabStopDistance = qRound(fm.horizontalAdvance(u'x') * 8);
0185         auto size = fm.size(text->textFlags(), text->text(), tabStopDistance);
0186         size.rwidth() = std::max(size.width(), fm.height());
0187         size.rheight() = std::max(size.height(), fm.height());
0188         // TODO: RTL language reversal
0189         rect.adjust(0, -fm.height() / 2, size.width(), size.height() - fm.height() / 2);
0190         path.addRect(rect);
0191     } else if (text->index() == Text::Number) {
0192         auto margin = fm.capHeight() * 1.33;
0193         rect.adjust(-margin, -margin, margin, margin);
0194         path.addEllipse(rect);
0195     }
0196     return path;
0197 }
0198 
0199 QPainterPath Traits::createStrokePath(const OptTuple &traits)
0200 {
0201     auto &geometry = std::get<Geometry::Opt>(traits);
0202     auto &stroke = std::get<Stroke::Opt>(traits);
0203     if (!geometry && !stroke) {
0204         return {};
0205     }
0206     QPainterPathStroker stroker(stroke->pen);
0207     auto minPath = Traits::minPath(geometry->path); // Will always have at least 2 points.
0208     if (auto &arrow = std::get<Arrow::Opt>(traits)) {
0209         const int size = minPath.elementCount();
0210         const QLineF lastLine{minPath.elementAt(size - 2), minPath.elementAt(size - 1)};
0211         auto arrowHead = Traits::arrowHead(lastLine, stroke->pen.widthF());
0212         return stroker.createStroke(minPath) | stroker.createStroke(arrowHead);
0213     } else {
0214         return stroker.createStroke(minPath);
0215     }
0216 }
0217 
0218 QPainterPath Traits::createMousePath(const OptTuple &traits)
0219 {
0220     auto &geometry = std::get<Geometry::Opt>(traits);
0221     auto &stroke = std::get<Stroke::Opt>(traits);
0222     QPainterPath mousePath;
0223     if (geometry && !geometry->path.isEmpty()) {
0224         mousePath = geometry->path;
0225     }
0226     // Ensure you can click anywhere within the bounds.
0227     mousePath.setFillRule(Qt::WindingFill);
0228     if (stroke && !stroke->path.isEmpty()) {
0229         mousePath |= stroke->path;
0230     }
0231 
0232     return mousePath.simplified();
0233 }
0234 
0235 QRectF Traits::createVisualRect(const OptTuple &traits)
0236 {
0237     auto &geometry = std::get<Geometry::Opt>(traits);
0238     auto &stroke = std::get<Stroke::Opt>(traits);
0239     if (!geometry) {
0240         return {};
0241     }
0242     QRectF visualRect;
0243     if (stroke) {
0244         visualRect = stroke->path.boundingRect() | geometry->path.boundingRect();
0245     } else {
0246         visualRect = geometry->path.boundingRect();
0247     }
0248     // Add Shadow margins if not empty.
0249     auto &shadow = std::get<Shadow::Opt>(traits);
0250     if (shadow && shadow->enabled && !visualRect.isEmpty()) {
0251         visualRect += Shadow::margins;
0252     }
0253     return visualRect;
0254 }
0255 
0256 void Traits::fastInitOptTuple(OptTuple &traits)
0257 {
0258     auto &geometry = std::get<Geometry::Opt>(traits);
0259     if (geometry) {
0260         // Set Geometry::path from Font and Text/Number if empty.
0261         auto &text = std::get<Text::Opt>(traits);
0262         if (geometry->path.isEmpty() && text) {
0263             geometry->path = Traits::createTextPath(traits);
0264         }
0265         // Set Stroke::path from Geometry and Arrow if empty.
0266         auto &stroke = std::get<Stroke::Opt>(traits);
0267         if (stroke && stroke->path.isEmpty()) {
0268             stroke->path = createStrokePath(traits);
0269         }
0270         // Set Geometry::visualRect from Stroke and Geometry if empty.
0271         if (geometry->visualRect.isEmpty()) {
0272             geometry->visualRect = createVisualRect(traits);
0273         }
0274     }
0275 }
0276 
0277 void Traits::initOptTuple(OptTuple &traits)
0278 {
0279     fastInitOptTuple(traits);
0280     auto &geometry = std::get<Geometry::Opt>(traits);
0281     if (geometry) {
0282         // Set Geometry::mousePath from Stroke and Geometry if empty.
0283         if (geometry->mousePath.isEmpty()) {
0284             geometry->mousePath = createMousePath(traits);
0285         }
0286     }
0287 }
0288 
0289 template<typename T>
0290 void clearForInitHelper(Traits::OptTuple &traits)
0291 {
0292     auto &traitOpt = std::get<std::optional<T>>(traits);
0293     if (!traitOpt) {
0294         return;
0295     }
0296     auto &trait = traitOpt.value();
0297     if constexpr (std::same_as<T, Traits::Geometry>) {
0298         trait.mousePath.clear();
0299         trait.visualRect = {};
0300     } else if constexpr (std::same_as<T, Traits::Stroke>) {
0301         trait.path.clear();
0302     } else if constexpr (std::same_as<T, Traits::Text>) {
0303         auto &geometry = std::get<Traits::Geometry::Opt>(traits);
0304         if (!geometry) {
0305             return;
0306         }
0307         if (trait.index() == Traits::Text::String) {
0308             QFontMetricsF fm(trait.font);
0309             // TODO: RTL language reversal
0310             QPointF topLeft;
0311             if (geometry->path.elementCount() == 1) {
0312                 topLeft = geometry->path.elementAt(0);
0313             } else {
0314                 topLeft = geometry->path.boundingRect().topLeft();
0315             }
0316             geometry->path = QPainterPath{topLeft + QPointF{0, fm.height() / 2}};
0317         } else if (trait.index() == Traits::Text::Number) {
0318             QPointF point;
0319             if (geometry->path.elementCount() == 1) {
0320                 point = geometry->path.elementAt(0);
0321             } else {
0322                 point = geometry->path.boundingRect().center();
0323             }
0324             geometry->path = QPainterPath{point};
0325         }
0326     }
0327 }
0328 
0329 void Traits::clearForInit(OptTuple &traits)
0330 {
0331     clearForInitHelper<Geometry>(traits);
0332     clearForInitHelper<Stroke>(traits);
0333     clearForInitHelper<Text>(traits);
0334 }
0335 
0336 void Traits::reInitTraits(OptTuple &traits)
0337 {
0338     clearForInit(traits);
0339     initOptTuple(traits);
0340 }
0341 
0342 void Traits::transformTraits(const QTransform &transform, OptTuple &traits)
0343 {
0344     if (transform.isIdentity()) {
0345         return;
0346     }
0347     auto &geometry = std::get<Geometry::Opt>(traits);
0348     auto &text = std::get<Text::Opt>(traits);
0349     bool onlyTranslating = transform.type() == QTransform::TxTranslate || text;
0350     if (geometry && onlyTranslating) {
0351         geometry->path.translate(transform.dx(), transform.dy());
0352         geometry->mousePath.translate(transform.dx(), transform.dy());
0353         // This is dependent on other traits, but as long as all traits have,
0354         // the same transformations, transforming at this time should be fine.
0355         geometry->visualRect.translate(transform.dx(), transform.dy());
0356     } else if (geometry) {
0357         geometry->path = transform.map(geometry->path);
0358         geometry->mousePath = transform.map(geometry->mousePath);
0359         // This is dependent on other traits, but as long as all traits have,
0360         // the same transformations, transforming at this time should be fine.
0361         geometry->visualRect = transform.mapRect(geometry->visualRect);
0362     }
0363     auto &stroke = std::get<Stroke::Opt>(traits);
0364     if (stroke && onlyTranslating) {
0365         // If the stroke already has the arrow in it,
0366         // we shouldn't need to completely regenerate the stroke with QPainterPathStroker.
0367         stroke->path.translate(transform.dx(), transform.dy());
0368     } else if (stroke) {
0369         stroke->path = transform.map(stroke->path);
0370     }
0371 }
0372 
0373 // Whether the values of the traits without std::optional are considered valid.
0374 template<>
0375 bool Traits::isValidTrait<Traits::Geometry>(const Traits::Geometry &trait)
0376 {
0377     return !trait.visualRect.isEmpty() && !trait.path.isEmpty();
0378 }
0379 template<>
0380 bool Traits::isValidTrait<Traits::Stroke>(const Traits::Stroke &trait)
0381 {
0382     return !trait.path.isEmpty() && trait.pen.style() != Qt::NoPen;
0383 }
0384 template<>
0385 bool Traits::isValidTrait<Traits::Fill>(const Traits::Fill &trait)
0386 {
0387     switch (trait.index()) {
0388     case Fill::Brush:
0389         return std::get<Fill::Brush>(trait) != Qt::NoBrush;
0390     case Fill::Blur:
0391         return std::get<Fill::Blur>(trait).isValid();
0392     case Fill::Pixelate:
0393         return std::get<Fill::Pixelate>(trait).isValid();
0394     default:
0395         return false;
0396     }
0397 }
0398 template<>
0399 bool Traits::isValidTrait<Traits::Highlight>(const Traits::Highlight &)
0400 {
0401     return true;
0402 }
0403 template<>
0404 bool Traits::isValidTrait<Traits::Arrow>(const Traits::Arrow &)
0405 {
0406     return true;
0407 }
0408 template<>
0409 bool Traits::isValidTrait<Traits::Text>(const Traits::Text &trait)
0410 {
0411     return trait.brush != Qt::NoBrush //
0412         && (trait.index() == Traits::Text::Number || !trait.text().isEmpty());
0413 }
0414 template<>
0415 bool Traits::isValidTrait<Traits::Shadow>(const Traits::Shadow &)
0416 {
0417     return true;
0418 }
0419 
0420 // Whether the std::optionals are considered valid.
0421 template<typename T>
0422 bool Traits::isValidTraitOpt(const Traits::OptTuple &traits, bool isNullValid)
0423 {
0424     auto &traitOpt = std::get<std::optional<T>>(traits);
0425     if (!traitOpt) {
0426         return isNullValid;
0427     }
0428     auto &trait = traitOpt.value();
0429 
0430     if constexpr (std::same_as<T, Traits::Geometry>) {
0431         return Traits::isValidTrait(trait);
0432     }
0433 
0434     // Traits that depend on geometry
0435     auto &geometry = std::get<Traits::Geometry::Opt>(traits);
0436     const bool validGeometry = geometry && Traits::isValidTrait(geometry.value());
0437     if constexpr (std::same_as<T, Stroke>) {
0438         return validGeometry && Traits::isValidTrait(trait);
0439     }
0440     if constexpr (std::same_as<T, Fill>) {
0441         return validGeometry && Traits::isValidTrait(trait);
0442     }
0443     if constexpr (std::same_as<T, Text>) {
0444         return validGeometry && Traits::isValidTrait(trait);
0445     }
0446 
0447     // Traits that depend on vector graphic traits
0448     auto &stroke = std::get<Stroke::Opt>(traits);
0449     auto &fill = std::get<Fill::Opt>(traits);
0450     auto &text = std::get<Text::Opt>(traits);
0451     const bool validStroke = stroke && Traits::isValidTrait(stroke.value());
0452     const bool validFill = fill && Traits::isValidTrait(fill.value());
0453     const bool validText = text && Traits::isValidTrait(text.value());
0454     if constexpr (std::same_as<T, Highlight>) {
0455         return validGeometry && (validStroke || validFill || validText) //
0456             && Traits::isValidTrait(trait);
0457     }
0458     if constexpr (std::same_as<T, Arrow>) {
0459         return validGeometry && (validStroke || validFill || validText) //
0460             && Traits::isValidTrait(trait);
0461     }
0462     if constexpr (std::same_as<T, Shadow>) {
0463         return validGeometry && (validStroke || validFill || validText) //
0464             && Traits::isValidTrait(trait);
0465     }
0466     return false;
0467 }
0468 
0469 template<typename... Ts>
0470 bool isValidHelper(const Traits::OptTuple &traits)
0471 {
0472     return (Traits::isValidTraitOpt<Ts>(traits, true) && ...);
0473 }
0474 
0475 bool Traits::isValid(const OptTuple &traits)
0476 {
0477     return isValidHelper<Geometry, Stroke, Fill, Highlight, Arrow, Text, Shadow>(traits);
0478 }
0479 
0480 bool Traits::isVisible(const OptTuple &traits)
0481 {
0482     return Traits::isValidTraitOpt<Geometry>(traits, false) //
0483         && (Traits::isValidTraitOpt<Stroke>(traits, false) //
0484             || Traits::isValidTraitOpt<Fill>(traits, false) //
0485             || Traits::isValidTraitOpt<Text>(traits, false));
0486 }
0487 
0488 QPainterPath Traits::mousePath(const OptTuple &traits)
0489 {
0490     auto &geometry = std::get<Geometry::Opt>(traits);
0491     return geometry ? geometry->mousePath : QPainterPath{};
0492 }
0493 
0494 QRectF Traits::visualRect(const OptTuple &traits)
0495 {
0496     auto &geometry = std::get<Geometry::Opt>(traits);
0497     return geometry ? geometry->visualRect : QRectF{};
0498 }
0499 
0500 // QDebug operator<< declarations
0501 
0502 // Traits
0503 
0504 QDebug operator<<(QDebug debug, const Traits::Geometry &trait)
0505 {
0506     using namespace Traits;
0507     QDebugStateSaver stateSaver(debug);
0508     debug.nospace();
0509     debug << "Geometry" << '(';
0510     debug << (const void *)&trait;
0511     debug << ",\n    path=" << trait.path;
0512     debug << ",\n    mousePath=" << trait.mousePath;
0513     debug << ",\n    visualRect=" << trait.visualRect;
0514     debug << ')';
0515     return debug;
0516 }
0517 
0518 QDebug operator<<(QDebug debug, const Traits::Stroke &trait)
0519 {
0520     using namespace Traits;
0521     QDebugStateSaver stateSaver(debug);
0522     debug.nospace();
0523     debug << "Stroke" << '(';
0524     debug << (const void *)&trait;
0525     debug << ",\n    pen=" << trait.pen;
0526     debug << ",\n    path=" << trait.path;
0527     debug << ')';
0528     return debug;
0529 }
0530 
0531 QDebug operator<<(QDebug debug, const Traits::Fill &trait)
0532 {
0533     using namespace Traits;
0534     QDebugStateSaver stateSaver(debug);
0535     debug.nospace();
0536     debug << "Fill" << '(';
0537     debug << (const void *)&trait;
0538     debug << ", ";
0539     switch (trait.index()) {
0540     case Fill::Brush:
0541         debug << std::get<Fill::Brush>(trait);
0542         break;
0543     case Fill::Blur:
0544         debug << std::get<Fill::Blur>(trait);
0545         break;
0546     case Fill::Pixelate:
0547         debug << std::get<Fill::Pixelate>(trait);
0548         break;
0549     default:
0550         break;
0551     }
0552     debug << ')';
0553     return debug;
0554 }
0555 
0556 QDebug operator<<(QDebug debug, const Traits::Highlight &trait)
0557 {
0558     using namespace Traits;
0559     QDebugStateSaver stateSaver(debug);
0560     debug.nospace();
0561     debug << "Highlight" << '(';
0562     debug << (const void *)&trait;
0563     debug << ')';
0564     return debug;
0565 }
0566 
0567 QDebug operator<<(QDebug debug, const Traits::Arrow &trait)
0568 {
0569     using namespace Traits;
0570     QDebugStateSaver stateSaver(debug);
0571     debug.nospace();
0572     debug << "Arrow" << '(';
0573     debug << (const void *)&trait;
0574     debug << ')';
0575     return debug;
0576 }
0577 
0578 QDebug operator<<(QDebug debug, const Traits::Text &trait)
0579 {
0580     using namespace Traits;
0581     QDebugStateSaver stateSaver(debug);
0582     debug.nospace();
0583     debug << "Text" << '(';
0584     debug << (const void *)&trait;
0585     debug << ",\n    text=" << trait.text();
0586     debug << ",\n    brush=" << trait.brush;
0587     debug << ",\n    font=" << trait.font;
0588     debug << ')';
0589     return debug;
0590 }
0591 
0592 QDebug operator<<(QDebug debug, const Traits::Shadow &trait)
0593 {
0594     using namespace Traits;
0595     QDebugStateSaver stateSaver(debug);
0596     debug.nospace();
0597     debug << "Shadow" << '(';
0598     debug << (const void *)&trait;
0599     debug << ",\n    enabled=" << trait.enabled;
0600     debug << ')';
0601     return debug;
0602 }
0603 
0604 
0605 // ImageEffects
0606 
0607 QDebug operator<<(QDebug debug, const Traits::ImageEffects::Blur &ref)
0608 {
0609     using namespace Traits::ImageEffects;
0610     QDebugStateSaver stateSaver(debug);
0611     debug.nospace();
0612     debug << "Blur" << '(';
0613     debug << (const void *)&ref;
0614     debug << ", factor=" << ref.factor;
0615     debug << ')';
0616     return debug;
0617 }
0618 
0619 QDebug operator<<(QDebug debug, const Traits::ImageEffects::Pixelate &ref)
0620 {
0621     using namespace Traits::ImageEffects;
0622     QDebugStateSaver stateSaver(debug);
0623     debug.nospace();
0624     debug << "Pixelate" << '(';
0625     debug << (const void *)&ref;
0626     debug << ", factor=" << ref.factor;
0627     debug << ')';
0628     return debug;
0629 }
0630 
0631 // Optionals
0632 // clang-format off
0633 #define OPTIONAL_DEBUG_DEF(ClassName)\
0634 QDebug operator<<(QDebug debug, const Traits::ClassName::Opt &optional)\
0635 {\
0636     using namespace Traits;\
0637     QDebugStateSaver stateSaver(debug);\
0638     debug.nospace();\
0639     debug << "Opt" << '<';\
0640     if (optional.has_value()) {\
0641         debug << optional.value();\
0642     } else {\
0643         debug << #ClassName << "(0x0)";\
0644     }\
0645     debug << ">(" << &optional << ')';\
0646     return debug;\
0647 }
0648 // clang-format on
0649 OPTIONAL_DEBUG_DEF(Geometry)
0650 OPTIONAL_DEBUG_DEF(Stroke)
0651 OPTIONAL_DEBUG_DEF(Fill)
0652 OPTIONAL_DEBUG_DEF(Highlight)
0653 OPTIONAL_DEBUG_DEF(Arrow)
0654 OPTIONAL_DEBUG_DEF(Text)
0655 OPTIONAL_DEBUG_DEF(Shadow)
0656 
0657 #undef OPTIONAL_DEBUG_DEF
0658 
0659 QDebug operator<<(QDebug debug, const Traits::OptTuple &optTuple)
0660 {
0661     using namespace Traits;
0662     QDebugStateSaver stateSaver(debug);
0663     debug.nospace();
0664     debug << "OptTuple" << '(';
0665     debug << (const void *)&optTuple;
0666     debug << ",\n  " << std::get<Traits::Geometry::Opt>(optTuple);
0667     debug << ",\n  " << std::get<Traits::Stroke::Opt>(optTuple);
0668     debug << ",\n  " << std::get<Traits::Fill::Opt>(optTuple);
0669     debug << ",\n  " << std::get<Traits::Highlight::Opt>(optTuple);
0670     debug << ",\n  " << std::get<Traits::Arrow::Opt>(optTuple);
0671     debug << ",\n  " << std::get<Traits::Text::Opt>(optTuple);
0672     debug << ",\n  " << std::get<Traits::Shadow::Opt>(optTuple);
0673     debug << ')';
0674     return debug;
0675 }