File indexing completed on 2024-12-01 04:21:11

0001 // SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
0002 // SPDX-License-Identifier: BSD-2-Clause OR MIT
0003 
0004 // First included header is the public header of the class we are testing;
0005 // this forces the header to be self-contained.
0006 #include "chromalightnessdiagram.h"
0007 // Second, the private implementation.
0008 #include "chromalightnessdiagram_p.h" // IWYU pragma: keep
0009 
0010 #include "constpropagatinguniquepointer.h"
0011 #include "helper.h"
0012 #include "helpermath.h"
0013 #include "lchdouble.h"
0014 #include "rgbcolorspacefactory.h"
0015 #include <cmath>
0016 #include <limits>
0017 #include <qglobal.h>
0018 #include <qlist.h>
0019 #include <qnamespace.h>
0020 #include <qobject.h>
0021 #include <qpoint.h>
0022 #include <qrect.h>
0023 #include <qsharedpointer.h>
0024 #include <qsignalspy.h>
0025 #include <qsize.h>
0026 #include <qtest.h>
0027 #include <qtestcase.h>
0028 #include <qtestkeyboard.h>
0029 #include <qtestmouse.h>
0030 
0031 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
0032 #include <qtmetamacros.h>
0033 #else
0034 #include <qobjectdefs.h>
0035 #include <qstring.h>
0036 #endif
0037 
0038 namespace PerceptualColor
0039 {
0040 class RgbColorSpace;
0041 
0042 class TestChromaLightnessDiagram : public QObject
0043 {
0044     Q_OBJECT
0045 
0046 public:
0047     explicit TestChromaLightnessDiagram(QObject *parent = nullptr)
0048         : QObject(parent)
0049     {
0050     }
0051 
0052 private:
0053     QSharedPointer<PerceptualColor::RgbColorSpace> m_rgbColorSpace = RgbColorSpaceFactory::createSrgb();
0054 
0055 private Q_SLOTS:
0056     void initTestCase()
0057     {
0058         // Called before the first test function is executed
0059     }
0060 
0061     void cleanupTestCase()
0062     {
0063         // Called after the last test function was executed
0064     }
0065 
0066     void init()
0067     {
0068         // Called before each test function is executed
0069     }
0070 
0071     void cleanup()
0072     {
0073         // Called after every test function
0074     }
0075 
0076     void testConstructorDestructor()
0077     {
0078         ChromaLightnessDiagram test(m_rgbColorSpace);
0079     }
0080 
0081     void testVerySmallWidgetSizes()
0082     {
0083         // Also very small widget sizes should not crash the widget.
0084         // This might happen because of divisions by 0, even when the widget
0085         // is bigger than 0 because of borders or offsets. We test this
0086         // here with various small sizes, always forcing in immediate
0087         // re-paint.
0088         ChromaLightnessDiagram myWidget{m_rgbColorSpace};
0089         myWidget.show();
0090         myWidget.resize(QSize());
0091         myWidget.repaint();
0092         myWidget.resize(QSize(-1, -1));
0093         myWidget.repaint();
0094         myWidget.resize(QSize(-1, 0));
0095         myWidget.repaint();
0096         myWidget.resize(QSize(0, -1));
0097         myWidget.repaint();
0098         myWidget.resize(QSize(0, 1));
0099         myWidget.repaint();
0100         myWidget.resize(QSize(1, 0));
0101         myWidget.repaint();
0102         myWidget.resize(QSize(1, 1));
0103         myWidget.repaint();
0104         myWidget.resize(QSize(2, 2));
0105         myWidget.repaint();
0106         myWidget.resize(QSize(3, 3));
0107         myWidget.repaint();
0108         myWidget.resize(QSize(4, 4));
0109         myWidget.repaint();
0110         myWidget.resize(QSize(5, 5));
0111         myWidget.repaint();
0112         myWidget.resize(QSize(6, 6));
0113         myWidget.repaint();
0114         myWidget.resize(QSize(7, 7));
0115         myWidget.repaint();
0116         myWidget.resize(QSize(8, 8));
0117         myWidget.repaint();
0118         myWidget.resize(QSize(9, 9));
0119         myWidget.repaint();
0120         myWidget.resize(QSize(10, 10));
0121         myWidget.repaint();
0122         myWidget.resize(QSize(11, 11));
0123         myWidget.repaint();
0124         myWidget.resize(QSize(12, 12));
0125         myWidget.repaint();
0126         myWidget.resize(QSize(13, 13));
0127         myWidget.repaint();
0128         myWidget.resize(QSize(14, 14));
0129         myWidget.repaint();
0130     }
0131 
0132     void testSetCurrentColorFromWidgetPixelPosition1()
0133     {
0134         // Also very small widget sizes should not crash the widget.
0135         // This might happen because if the widget is too small, there
0136         // is no place for a diagram, and some value conversions are
0137         // diagram-based..
0138         ChromaLightnessDiagram myWidget{m_rgbColorSpace};
0139         const QPoint positive(10, 20);
0140         const QPoint negative(-10, -20);
0141         myWidget.resize(QSize(1, 1));
0142         // Executing the following lines should not crash!
0143         myWidget.d_pointer->setCurrentColorFromWidgetPixelPosition(positive);
0144         myWidget.d_pointer->setCurrentColorFromWidgetPixelPosition(negative);
0145     }
0146 
0147     void testSetCurrentColorFromWidgetPixelPosition2()
0148     {
0149         // Test this function for out-of-gamut positions
0150         ChromaLightnessDiagram myWidget{m_rgbColorSpace};
0151         myWidget.show();
0152         constexpr int size = 100;
0153         myWidget.resize(size, size);
0154         LchDouble color;
0155 
0156         // Test for top-left corner
0157         myWidget.d_pointer->setCurrentColorFromWidgetPixelPosition(
0158             // Same x and y spacing from top-left corner
0159             QPoint(size * (-1), size * (-1)));
0160         delayedEventProcessing();
0161         color = myWidget.currentColor();
0162         QCOMPARE(color.l, 100);
0163         QCOMPARE(color.c, 0);
0164 
0165         // Test for bottom-left corner
0166         myWidget.d_pointer->setCurrentColorFromWidgetPixelPosition(
0167             // Same x and y spacing from bottom-left corner
0168             QPoint(size * (-1), size * 2));
0169         delayedEventProcessing();
0170         color = myWidget.currentColor();
0171         QCOMPARE(color.l, 0);
0172         QCOMPARE(color.c, 0);
0173 
0174         // Test for middle-right position
0175         myWidget.d_pointer->setCurrentColorFromWidgetPixelPosition(
0176             // x position far from diagram boundaries, y position in the middle
0177             QPoint(size * 10, size * 50 / 100));
0178         delayedEventProcessing();
0179         color = myWidget.currentColor();
0180         // Lightness should be somewhere in the middle.
0181         QVERIFY(color.l > 10);
0182         QVERIFY(color.l < 90);
0183         // At least 25 should be possible on all hues.
0184         QVERIFY(color.c > 25);
0185     }
0186 
0187     void testDefaultBorderPhysical()
0188     {
0189         ChromaLightnessDiagram myWidget{m_rgbColorSpace};
0190         QVERIFY(myWidget.d_pointer->defaultBorderPhysical() >= 0);
0191     }
0192 
0193     void testLeftBorderPhysical()
0194     {
0195         ChromaLightnessDiagram myWidget{m_rgbColorSpace};
0196         QVERIFY(myWidget.d_pointer->defaultBorderPhysical() >= 0);
0197         QVERIFY(myWidget.d_pointer->defaultBorderPhysical() >= myWidget.d_pointer->defaultBorderPhysical());
0198     }
0199 
0200     void testCalculateImageSizePhysical()
0201     {
0202         // Also very small widget sizes should not crash the widget.
0203         // This might happen because of divisions by 0, even when the widget
0204         // is bigger than 0 because of borders or offsets. We test this
0205         // here with various small sizes.
0206         ChromaLightnessDiagram myWidget{m_rgbColorSpace};
0207         myWidget.resize(QSize());
0208         Q_UNUSED(myWidget.d_pointer->calculateImageSizePhysical()); // Should not crash
0209         myWidget.resize(QSize(-1, -1));
0210         Q_UNUSED(myWidget.d_pointer->calculateImageSizePhysical()); // Should not crash
0211         myWidget.resize(QSize(-1, 0));
0212         Q_UNUSED(myWidget.d_pointer->calculateImageSizePhysical()); // Should not crash
0213         myWidget.resize(QSize(0, -1));
0214         Q_UNUSED(myWidget.d_pointer->calculateImageSizePhysical()); // Should not crash
0215         myWidget.resize(QSize(0, 1));
0216         Q_UNUSED(myWidget.d_pointer->calculateImageSizePhysical()); // Should not crash
0217         myWidget.resize(QSize(1, 0));
0218         Q_UNUSED(myWidget.d_pointer->calculateImageSizePhysical()); // Should not crash
0219         myWidget.resize(QSize(1, 1));
0220         Q_UNUSED(myWidget.d_pointer->calculateImageSizePhysical()); // Should not crash
0221     }
0222 
0223     void testFromWidgetPixelPositionToColor()
0224     {
0225         // Also very small widget sizes should not crash the widget.
0226         // This might happen because of divisions by 0, even when the widget
0227         // is bigger than 0 because of borders or offsets. We test this
0228         // here with various small sizes.
0229         ChromaLightnessDiagram myWidget{m_rgbColorSpace};
0230         const QPoint positive(10, 20);
0231         const QPoint negative(-10, -20);
0232         myWidget.resize(QSize());
0233         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(positive));
0234         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(negative));
0235         myWidget.resize(QSize(-1, -1));
0236         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(positive));
0237         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(negative));
0238         myWidget.resize(QSize(-1, 0));
0239         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(positive));
0240         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(negative));
0241         myWidget.resize(QSize(0, -1));
0242         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(positive));
0243         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(negative));
0244         myWidget.resize(QSize(0, 1));
0245         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(positive));
0246         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(negative));
0247         myWidget.resize(QSize(1, 0));
0248         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(positive));
0249         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(negative));
0250         myWidget.resize(QSize(1, 1));
0251         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(positive));
0252         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(negative));
0253         myWidget.resize(QSize(2, 2));
0254         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(positive));
0255         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(negative));
0256         myWidget.resize(QSize(3, 3));
0257         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(positive));
0258         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(negative));
0259         myWidget.resize(QSize(4, 4));
0260         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(positive));
0261         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(negative));
0262         myWidget.resize(QSize(5, 5));
0263         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(positive));
0264         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(negative));
0265         myWidget.resize(QSize(6, 6));
0266         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(positive));
0267         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(negative));
0268         myWidget.resize(QSize(7, 7));
0269         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(positive));
0270         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(negative));
0271         myWidget.resize(QSize(8, 8));
0272         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(positive));
0273         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(negative));
0274         myWidget.resize(QSize(9, 9));
0275         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(positive));
0276         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(negative));
0277         myWidget.resize(QSize(10, 10));
0278         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(positive));
0279         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(negative));
0280         myWidget.resize(QSize(11, 11));
0281         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(positive));
0282         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(negative));
0283         myWidget.resize(QSize(12, 12));
0284         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(positive));
0285         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(negative));
0286         myWidget.resize(QSize(13, 13));
0287         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(positive));
0288         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(negative));
0289         myWidget.resize(QSize(14, 14));
0290         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(positive));
0291         Q_UNUSED(myWidget.d_pointer->fromWidgetPixelPositionToColor(negative));
0292     }
0293 
0294     void testMouseSupport1()
0295     {
0296         ChromaLightnessDiagram myWidget{m_rgbColorSpace};
0297         myWidget.show();
0298         myWidget.resize(2, 2);
0299         // Mouse movements should not crash when the size of the widget is
0300         // too small to show a diagram:
0301         QTest::mousePress(&myWidget, //
0302                           Qt::MouseButton::LeftButton, //
0303                           Qt::KeyboardModifier::NoModifier, //
0304                           QPoint(0, 0));
0305         // Alternative: Maybe this catches more bugs?…:
0306         // QTest::mouseMove(&myWidget, QPoint(1, 1));
0307         QTest::mouseRelease(&myWidget, //
0308                             Qt::MouseButton::LeftButton, //
0309                             Qt::KeyboardModifier::NoModifier, //
0310                             QPoint(1, 1));
0311     }
0312 
0313     void testMouseSupport2()
0314     {
0315         // Test reactions to mouse events when moving out-of-gamut
0316         ChromaLightnessDiagram myWidget{m_rgbColorSpace};
0317         myWidget.show();
0318         constexpr int size = 100;
0319         myWidget.resize(size, size);
0320         LchDouble color;
0321 
0322         // Test for top-left corner
0323         QTest::mousePress(&myWidget,
0324                           Qt::MouseButton::LeftButton,
0325                           Qt::KeyboardModifier::NoModifier,
0326                           // Press the mouse at a point with some chroma
0327                           // (10%) and a medium lightness (50%). This makes
0328                           // sure to get a point within the gamut.
0329                           QPoint(size * 10 / 100, size * 50 / 100));
0330         QTest::mouseRelease(&myWidget,
0331                             Qt::MouseButton::LeftButton,
0332                             Qt::KeyboardModifier::NoModifier,
0333                             // Press the mouse at a point with some chroma
0334                             // (10%) and a medium lightness (50%). This makes
0335                             // sure to get a point within the gamut.
0336                             QPoint(size * (-1), size * (-1)));
0337         // Test if the widget value is really the nearest in-gamut color
0338         delayedEventProcessing();
0339         color = myWidget.currentColor();
0340         QCOMPARE(color.l, 100);
0341         QCOMPARE(color.c, 0);
0342 
0343         // Test for bottom-left corner
0344         QTest::mousePress(&myWidget,
0345                           Qt::MouseButton::LeftButton,
0346                           Qt::KeyboardModifier::NoModifier,
0347                           // Press the mouse at a point with some chroma
0348                           // (10%) and a medium lightness (50%). This makes
0349                           // sure to get a point within the gamut.
0350                           QPoint(size * 10 / 100, size * 50 / 100));
0351         QTest::mouseRelease(&myWidget,
0352                             Qt::MouseButton::LeftButton,
0353                             Qt::KeyboardModifier::NoModifier,
0354                             // Press the mouse at a point with some chroma
0355                             // (10%) and a medium lightness (50%). This makes
0356                             // sure to get a point within the gamut.
0357                             QPoint(size * (-1), size * 2));
0358         // Test if the widget value is really the nearest in-gamut color
0359         color = myWidget.currentColor();
0360         QCOMPARE(color.l, 0);
0361         QCOMPARE(color.c, 0);
0362 
0363         // Test for middle-right position
0364         QTest::mousePress(&myWidget,
0365                           Qt::MouseButton::LeftButton,
0366                           Qt::KeyboardModifier::NoModifier,
0367                           // Press the mouse at a point with some chroma
0368                           // (10%) and a medium lightness (50%). This makes
0369                           // sure to get a point within the gamut.
0370                           QPoint(size * 10 / 100, size * 50 / 100));
0371         QTest::mouseRelease(&myWidget,
0372                             Qt::MouseButton::LeftButton,
0373                             Qt::KeyboardModifier::NoModifier,
0374                             // Press the mouse at a point with some chroma
0375                             // (10%) and a medium lightness (50%). This makes
0376                             // sure to get a point within the gamut.
0377                             QPoint(size * 10, size * 50 / 100));
0378         // Test if the widget value is really the nearest in-gamut color
0379         color = myWidget.currentColor();
0380         // Lightness should be somewhere in the middle.
0381         QVERIFY(color.l > 10);
0382         QVERIFY(color.l < 90);
0383         // At least 25 should be possible on all hues.
0384         QVERIFY(color.c > 25);
0385     }
0386 
0387     void testPaintEventNormalSize()
0388     {
0389         ChromaLightnessDiagram myWidget{m_rgbColorSpace};
0390         myWidget.show();
0391         // Test normal size
0392         myWidget.resize(100, 100);
0393         myWidget.repaint();
0394     }
0395 
0396     void testPaintEventTooSmallSize()
0397     {
0398         ChromaLightnessDiagram myWidget{m_rgbColorSpace};
0399         myWidget.show();
0400         // Test small size (too small to show a diagram)
0401         myWidget.resize(2, 2);
0402         myWidget.repaint();
0403     }
0404 
0405     void testPaintEventEmptySize()
0406     {
0407         ChromaLightnessDiagram myWidget{m_rgbColorSpace};
0408         myWidget.show();
0409         // Test empty size
0410         myWidget.resize(0, 0);
0411         myWidget.repaint();
0412     }
0413 
0414     void testKeyPressEvent()
0415     {
0416         ChromaLightnessDiagram myDiagram(m_rgbColorSpace);
0417         LchDouble referenceColorLch;
0418         referenceColorLch.l = 50;
0419         referenceColorLch.c = 20;
0420         referenceColorLch.h = 180;
0421         myDiagram.setCurrentColor(referenceColorLch);
0422 
0423         // Assert pre-conditions:
0424 
0425         QCOMPARE(myDiagram.currentColor().l, 50);
0426         QCOMPARE(myDiagram.currentColor().c, 20);
0427         QCOMPARE(myDiagram.currentColor().h, 180);
0428 
0429         // Actual test:
0430 
0431         myDiagram.setCurrentColor(referenceColorLch);
0432         QTest::keyClick(&myDiagram, Qt::Key_Left);
0433         QVERIFY(myDiagram.currentColor().l == referenceColorLch.l);
0434         QVERIFY(myDiagram.currentColor().c < referenceColorLch.c);
0435         QVERIFY(myDiagram.currentColor().h == referenceColorLch.h);
0436 
0437         myDiagram.setCurrentColor(referenceColorLch);
0438         QTest::keyClick(&myDiagram, Qt::Key_Right);
0439         QVERIFY(myDiagram.currentColor().l == referenceColorLch.l);
0440         QVERIFY(myDiagram.currentColor().c > referenceColorLch.c);
0441         QVERIFY(myDiagram.currentColor().h == referenceColorLch.h);
0442 
0443         myDiagram.setCurrentColor(referenceColorLch);
0444         QTest::keyClick(&myDiagram, Qt::Key_Up);
0445         QVERIFY(myDiagram.currentColor().l > referenceColorLch.l);
0446         QVERIFY(myDiagram.currentColor().c == referenceColorLch.c);
0447         QVERIFY(myDiagram.currentColor().h == referenceColorLch.h);
0448 
0449         myDiagram.setCurrentColor(referenceColorLch);
0450         QTest::keyClick(&myDiagram, Qt::Key_Down);
0451         QVERIFY(myDiagram.currentColor().l < referenceColorLch.l);
0452         QVERIFY(myDiagram.currentColor().c == referenceColorLch.c);
0453         QVERIFY(myDiagram.currentColor().h == referenceColorLch.h);
0454 
0455         myDiagram.setCurrentColor(referenceColorLch);
0456         QTest::keyClick(&myDiagram, Qt::Key_Home);
0457         QVERIFY(myDiagram.currentColor().l == referenceColorLch.l);
0458         QVERIFY(myDiagram.currentColor().c > referenceColorLch.c);
0459         QVERIFY(myDiagram.currentColor().h == referenceColorLch.h);
0460 
0461         myDiagram.setCurrentColor(referenceColorLch);
0462         QTest::keyClick(&myDiagram, Qt::Key_End);
0463         QVERIFY(myDiagram.currentColor().l == referenceColorLch.l);
0464         QVERIFY(myDiagram.currentColor().c < referenceColorLch.c);
0465         QVERIFY(myDiagram.currentColor().h == referenceColorLch.h);
0466 
0467         myDiagram.setCurrentColor(referenceColorLch);
0468         QTest::keyClick(&myDiagram, Qt::Key_PageUp);
0469         QVERIFY(myDiagram.currentColor().l > referenceColorLch.l);
0470         QVERIFY(myDiagram.currentColor().c == referenceColorLch.c);
0471         QVERIFY(myDiagram.currentColor().h == referenceColorLch.h);
0472 
0473         myDiagram.setCurrentColor(referenceColorLch);
0474         QTest::keyClick(&myDiagram, Qt::Key_PageDown);
0475         QVERIFY(myDiagram.currentColor().l < referenceColorLch.l);
0476         QVERIFY(myDiagram.currentColor().c == referenceColorLch.c);
0477         QVERIFY(myDiagram.currentColor().h == referenceColorLch.h);
0478 
0479         referenceColorLch.c = 0;
0480 
0481         // Chroma should never become negative
0482 
0483         myDiagram.setCurrentColor(referenceColorLch);
0484         QTest::keyClick(&myDiagram, Qt::Key_Left);
0485         QCOMPARE(myDiagram.currentColor().l, referenceColorLch.l);
0486         QCOMPARE(myDiagram.currentColor().c, referenceColorLch.c);
0487         QCOMPARE(myDiagram.currentColor().h, referenceColorLch.h);
0488 
0489         myDiagram.setCurrentColor(referenceColorLch);
0490         QTest::keyClick(&myDiagram, Qt::Key_End);
0491         QCOMPARE(myDiagram.currentColor().l, referenceColorLch.l);
0492         QCOMPARE(myDiagram.currentColor().c, referenceColorLch.c);
0493         QCOMPARE(myDiagram.currentColor().h, referenceColorLch.h);
0494 
0495         referenceColorLch.l = 0;
0496 
0497         // Lightness should never be smaller than 0.
0498 
0499         myDiagram.setCurrentColor(referenceColorLch);
0500         QTest::keyClick(&myDiagram, Qt::Key_Down);
0501         QVERIFY(myDiagram.currentColor().l >= 0);
0502         QVERIFY(myDiagram.currentColor().c == referenceColorLch.c);
0503         QVERIFY(myDiagram.currentColor().h == referenceColorLch.h);
0504 
0505         myDiagram.setCurrentColor(referenceColorLch);
0506         QTest::keyClick(&myDiagram, Qt::Key_PageDown);
0507         QVERIFY(myDiagram.currentColor().l >= 0);
0508         QVERIFY(myDiagram.currentColor().c == referenceColorLch.c);
0509         QVERIFY(myDiagram.currentColor().h == referenceColorLch.h);
0510 
0511         referenceColorLch.l = 100;
0512 
0513         // Lightness should never be bigger than 100.
0514 
0515         myDiagram.setCurrentColor(referenceColorLch);
0516         QTest::keyClick(&myDiagram, Qt::Key_Up);
0517         QVERIFY(myDiagram.currentColor().l <= 100);
0518         QVERIFY(myDiagram.currentColor().c == referenceColorLch.c);
0519         QVERIFY(myDiagram.currentColor().h == referenceColorLch.h);
0520 
0521         myDiagram.setCurrentColor(referenceColorLch);
0522         QTest::keyClick(&myDiagram, Qt::Key_PageUp);
0523         QVERIFY(myDiagram.currentColor().l <= 100);
0524         QVERIFY(myDiagram.currentColor().c == referenceColorLch.c);
0525         QVERIFY(myDiagram.currentColor().h == referenceColorLch.h);
0526     }
0527 
0528     void testIsWidgetPixelPositionInGamut()
0529     {
0530         ChromaLightnessDiagram myDiagram(m_rgbColorSpace);
0531         myDiagram.show();
0532         myDiagram.resize(QSize(2, 2));
0533         // On very small widget sizes, no diagram is visible. Therefore,
0534         // no pixel should be in-gamut.
0535         QVERIFY(!myDiagram.d_pointer->isWidgetPixelPositionInGamut(QPoint(0, 0)));
0536         QVERIFY(!myDiagram.d_pointer->isWidgetPixelPositionInGamut(QPoint(0, 1)));
0537         QVERIFY(!myDiagram.d_pointer->isWidgetPixelPositionInGamut(QPoint(0, 2)));
0538         QVERIFY(!myDiagram.d_pointer->isWidgetPixelPositionInGamut(QPoint(1, 0)));
0539         QVERIFY(!myDiagram.d_pointer->isWidgetPixelPositionInGamut(QPoint(1, 1)));
0540         QVERIFY(!myDiagram.d_pointer->isWidgetPixelPositionInGamut(QPoint(1, 2)));
0541         QVERIFY(!myDiagram.d_pointer->isWidgetPixelPositionInGamut(QPoint(2, 0)));
0542         QVERIFY(!myDiagram.d_pointer->isWidgetPixelPositionInGamut(QPoint(2, 1)));
0543         QVERIFY(!myDiagram.d_pointer->isWidgetPixelPositionInGamut(QPoint(2, 2)));
0544     }
0545 
0546 #ifndef MSVC_DLL
0547     // The automatic export of otherwise private symbols on MSVC
0548     // shared libraries via CMake's WINDOWS_EXPORT_ALL_SYMBOLS property
0549     // does not work well for Qt meta objects, resulting in non-functional
0550     // signals. Since the following unit tests require signals, it cannot be
0551     // built for MSVC shared libraries.
0552 
0553     void testCurrentColorProperty()
0554     {
0555         ChromaLightnessDiagram test{m_rgbColorSpace};
0556         LchDouble color;
0557         color.l = 50;
0558         color.c = 20;
0559         color.h = 10;
0560         test.setCurrentColor(color);
0561         QVERIFY(test.currentColor().hasSameCoordinates(color));
0562         QSignalSpy spy(&test, &ChromaLightnessDiagram::currentColorChanged);
0563         QCOMPARE(spy.count(), 0);
0564 
0565         // Change hue only:
0566         color.h += 1;
0567         test.setCurrentColor(color);
0568         QCOMPARE(spy.count(), 1);
0569         QVERIFY(test.currentColor().hasSameCoordinates(color));
0570 
0571         // Change chroma only:
0572         color.c += 1;
0573         test.setCurrentColor(color);
0574         QCOMPARE(spy.count(), 2);
0575         QVERIFY(test.currentColor().hasSameCoordinates(color));
0576 
0577         // Change lightness only:
0578         color.l += 1;
0579         test.setCurrentColor(color);
0580         QCOMPARE(spy.count(), 3);
0581         QVERIFY(test.currentColor().hasSameCoordinates(color));
0582 
0583         // Not changing the color should not trigger the signal
0584         test.setCurrentColor(color);
0585         QCOMPARE(spy.count(), 3);
0586         QVERIFY(test.currentColor().hasSameCoordinates(color));
0587     }
0588 
0589 #endif
0590 
0591     void testResizeEvent()
0592     {
0593         ChromaLightnessDiagram test{m_rgbColorSpace};
0594         test.show();
0595         // Resize events should not crash!
0596         test.resize(QSize(100, 100)); // normal size
0597         test.resize(QSize(2, 2)); // very small size
0598         test.resize(QSize(0, 0)); // empty size
0599         test.resize(QSize(-1, -1)); // invalid size
0600     }
0601 
0602     void testSizeHintAndMinimumSizeHint()
0603     {
0604         ChromaLightnessDiagram test{m_rgbColorSpace};
0605         test.show();
0606         QVERIFY(test.minimumSizeHint().width() >= 0);
0607         QVERIFY(test.minimumSizeHint().height() >= 0);
0608         QVERIFY(test.sizeHint().width() >= test.minimumSizeHint().width());
0609         QVERIFY(test.sizeHint().height() >= test.minimumSizeHint().height());
0610     }
0611 
0612     void testOutOfGamutColors()
0613     {
0614         ChromaLightnessDiagram myWidget{m_rgbColorSpace};
0615         myWidget.show();
0616         myWidget.resize(QSize(400, 400));
0617 
0618         // Test that setting out-of-gamut colors works
0619 
0620         const LchDouble myFirstColor{100, 150, 0};
0621         myWidget.setCurrentColor(myFirstColor);
0622         QVERIFY(myFirstColor.hasSameCoordinates(myWidget.currentColor()));
0623         QVERIFY( //
0624             myFirstColor.hasSameCoordinates(myWidget.d_pointer->m_currentColor));
0625 
0626         const LchDouble mySecondColor{0, 150, 0};
0627         myWidget.setCurrentColor(mySecondColor);
0628         QVERIFY(mySecondColor.hasSameCoordinates(myWidget.currentColor()));
0629         QVERIFY( //
0630             mySecondColor.hasSameCoordinates(myWidget.d_pointer->m_currentColor));
0631     }
0632 
0633     void testOutOfRange()
0634     {
0635         ChromaLightnessDiagram myWidget{m_rgbColorSpace};
0636         myWidget.show();
0637         myWidget.resize(QSize(400, 400));
0638 
0639         // Test that setting colors, that are not only out-of-gamut colors
0640         // but also out of a reasonable range, works.
0641 
0642         const LchDouble myFirstColor{300, 550, -10};
0643         myWidget.setCurrentColor(myFirstColor);
0644         QVERIFY( //
0645             myFirstColor.hasSameCoordinates(myWidget.currentColor()));
0646         QVERIFY( //
0647             myFirstColor.hasSameCoordinates(myWidget.d_pointer->m_currentColor));
0648 
0649         const LchDouble mySecondColor{-100, -150, 890};
0650         myWidget.setCurrentColor(mySecondColor);
0651         QVERIFY(mySecondColor.hasSameCoordinates(myWidget.currentColor()));
0652         QVERIFY(mySecondColor.hasSameCoordinates(myWidget.d_pointer->m_currentColor));
0653     }
0654 
0655     void testNearestInGamutColorByAdjustingChromaLightness()
0656     {
0657         ChromaLightnessDiagram myWidget{m_rgbColorSpace};
0658 
0659         // Variables
0660         LchDouble color;
0661         LchDouble nearestInGamutColor;
0662 
0663         // In-gamut colors should not be changed.
0664         color.l = 50;
0665         color.c = 20;
0666         color.h = 10;
0667         myWidget.setCurrentColor(color);
0668         nearestInGamutColor = //
0669             myWidget.d_pointer->nearestInGamutColorByAdjustingChromaLightness(color.c, color.l);
0670         QVERIFY(nearestInGamutColor.hasSameCoordinates(color));
0671 
0672         // A negative chroma value should not be normalized (this would
0673         // mean to change the hue), but just put to 0.
0674         color.l = 50;
0675         color.c = -20;
0676         color.h = 10;
0677         myWidget.setCurrentColor(color);
0678         nearestInGamutColor = //
0679             myWidget.d_pointer->nearestInGamutColorByAdjustingChromaLightness(color.c, color.l);
0680         QCOMPARE(nearestInGamutColor.l, 50);
0681         QCOMPARE(nearestInGamutColor.c, 0);
0682         QCOMPARE(nearestInGamutColor.h, 10);
0683     }
0684 
0685     void testNearestInGamutColorByAdjustingChromaLightnessSmallSize()
0686     {
0687         ChromaLightnessDiagram myWidget{m_rgbColorSpace};
0688 
0689         // Variables
0690         LchDouble color;
0691         LchDouble nearestInGamutColor;
0692 
0693         // In-gamut colors should not be changed.
0694         color.l = 50;
0695         color.c = 20;
0696         color.h = 10;
0697         myWidget.setCurrentColor(color);
0698 
0699         // nearestInGamutColorByAdjustingChromaLightness() is only
0700         // guaranteed to work correctly for an image size of at least
0701         // two pixel width and two pixel height. Test here if at least
0702         // we can call the function without crash, even if the result
0703         // does not make sense.
0704         myWidget.resize(1, 1);
0705         nearestInGamutColor = //
0706             myWidget.d_pointer->nearestInGamutColorByAdjustingChromaLightness(color.c, color.l);
0707     }
0708 
0709     void testDistanceFromRange()
0710     {
0711         QCOMPARE(ChromaLightnessDiagramPrivate::distanceFromRange(1, 2, 3), 0);
0712         QCOMPARE(ChromaLightnessDiagramPrivate::distanceFromRange(-5, -4, -3), 0);
0713         QCOMPARE(ChromaLightnessDiagramPrivate::distanceFromRange(-1, 0, 1), 0);
0714         QCOMPARE(ChromaLightnessDiagramPrivate::distanceFromRange(5, 6, 7), 0);
0715         QCOMPARE(ChromaLightnessDiagramPrivate::distanceFromRange(5, 5, 7), 0);
0716         QCOMPARE(ChromaLightnessDiagramPrivate::distanceFromRange(5, 7, 7), 0);
0717         QCOMPARE(ChromaLightnessDiagramPrivate::distanceFromRange(5, 4, 7), 1);
0718         QCOMPARE(ChromaLightnessDiagramPrivate::distanceFromRange(5, 3, 7), 2);
0719         QCOMPARE(ChromaLightnessDiagramPrivate::distanceFromRange(5, 8, 7), 1);
0720         QCOMPARE(ChromaLightnessDiagramPrivate::distanceFromRange(5, 9, 7), 2);
0721 
0722         // Special case: low == hight
0723         QCOMPARE(ChromaLightnessDiagramPrivate::distanceFromRange(5, 5, 5), 0);
0724         QCOMPARE(ChromaLightnessDiagramPrivate::distanceFromRange(5, 4, 5), 1);
0725         QCOMPARE(ChromaLightnessDiagramPrivate::distanceFromRange(5, 3, 5), 2);
0726         QCOMPARE(ChromaLightnessDiagramPrivate::distanceFromRange(5, 6, 5), 1);
0727         QCOMPARE(ChromaLightnessDiagramPrivate::distanceFromRange(5, 7, 5), 2);
0728 
0729         // Special cases for floating point operations
0730         if constexpr (std::numeric_limits<double>::has_infinity //
0731                       && std::numeric_limits<double>::has_signaling_NaN //
0732                       && std::numeric_limits<double>::has_quiet_NaN //
0733         ) {
0734             constexpr auto inf = std::numeric_limits<double>::infinity();
0735             constexpr auto qnan = std::numeric_limits<double>::quiet_NaN();
0736             constexpr auto snan = std::numeric_limits<double>::signaling_NaN();
0737 
0738             // Infinity
0739             QCOMPARE( //
0740                 ChromaLightnessDiagramPrivate::distanceFromRange(-inf, 7., 5.),
0741                 2);
0742             QCOMPARE( //
0743                 ChromaLightnessDiagramPrivate::distanceFromRange(-inf, 5., 5.),
0744                 0);
0745             QCOMPARE( //
0746                 ChromaLightnessDiagramPrivate::distanceFromRange(-inf, 3., 5.),
0747                 0);
0748             QCOMPARE( //
0749                 ChromaLightnessDiagramPrivate::distanceFromRange(3., -inf, 5.),
0750                 inf);
0751             QCOMPARE( //
0752                 ChromaLightnessDiagramPrivate::distanceFromRange(3., inf, 5.),
0753                 inf);
0754             QCOMPARE( //
0755                 ChromaLightnessDiagramPrivate::distanceFromRange(3., 5., inf),
0756                 0);
0757             QCOMPARE( //
0758                 ChromaLightnessDiagramPrivate::distanceFromRange(3., 3., inf),
0759                 0);
0760             QCOMPARE( //
0761                 ChromaLightnessDiagramPrivate::distanceFromRange(3., 1., inf),
0762                 2);
0763 
0764             // Nan
0765             QVERIFY(std::isnan( //
0766                 ChromaLightnessDiagramPrivate::distanceFromRange(qnan, 2., 3.)));
0767             QVERIFY(std::isnan( //
0768                 ChromaLightnessDiagramPrivate::distanceFromRange(1., qnan, 3.)));
0769             QVERIFY(std::isnan( //
0770                 ChromaLightnessDiagramPrivate::distanceFromRange(1., 2., qnan)));
0771             QVERIFY(std::isnan( //
0772                 ChromaLightnessDiagramPrivate::distanceFromRange(qnan, qnan, 3.)));
0773             QVERIFY(std::isnan( //
0774                 ChromaLightnessDiagramPrivate::distanceFromRange(qnan, 2., qnan)));
0775             QVERIFY(std::isnan( //
0776                 ChromaLightnessDiagramPrivate::distanceFromRange(1., qnan, qnan)));
0777             QVERIFY(std::isnan( //
0778                 ChromaLightnessDiagramPrivate::distanceFromRange(qnan, qnan, qnan)));
0779             QVERIFY(std::isnan( //
0780                 ChromaLightnessDiagramPrivate::distanceFromRange(snan, snan, snan)));
0781         }
0782     }
0783 
0784     void testNearestNeighborSearch()
0785     {
0786         // Setup
0787         const auto doesExist = [](const QPoint point) -> bool {
0788             // Our valid search rectangle is from (2, 2) to (8, 8).
0789             if (isInRange(-2, point.x(), 8) && isInRange(-2, point.y(), 8)) {
0790                 QList<QPoint> existingPoints({//
0791                                               QPoint(-2, -2),
0792                                               QPoint(5, 5),
0793                                               QPoint(8, 8)});
0794                 return existingPoints.contains(point);
0795             }
0796             // A correct implementation of nearestNeighborSearch should never
0797             // call the callback function with values outside the valid range,
0798             // so we should never get here:
0799             return true;
0800         };
0801         constexpr auto searchRectangle = QRect(QPoint(-2, -2), QSize(11, 11));
0802         QVERIFY(searchRectangle.contains(QPoint(-3, -3)) == false); // assert
0803         QVERIFY(searchRectangle.contains(QPoint(-2, -2))); // assert
0804         QVERIFY(searchRectangle.contains(QPoint(8, 8))); // assert
0805         QVERIFY(searchRectangle.contains(QPoint(9, 9)) == false); // assert
0806 
0807         // Actual tests
0808         QCOMPARE(ChromaLightnessDiagramPrivate::nearestNeighborSearch(QPoint(-2, -2), searchRectangle, doesExist), //
0809                  QPoint(-2, -2));
0810         QCOMPARE(ChromaLightnessDiagramPrivate::nearestNeighborSearch(QPoint(-1, -2), searchRectangle, doesExist), //
0811                  QPoint(-2, -2));
0812         QCOMPARE(ChromaLightnessDiagramPrivate::nearestNeighborSearch(QPoint(-2, -1), searchRectangle, doesExist), //
0813                  QPoint(-2, -2));
0814         QCOMPARE(ChromaLightnessDiagramPrivate::nearestNeighborSearch(QPoint(-3, -2), searchRectangle, doesExist), //
0815                  QPoint(-2, -2));
0816         QCOMPARE(ChromaLightnessDiagramPrivate::nearestNeighborSearch(QPoint(-2, -3), searchRectangle, doesExist), //
0817                  QPoint(-2, -2));
0818         QCOMPARE(ChromaLightnessDiagramPrivate::nearestNeighborSearch(QPoint(-3, -3), searchRectangle, doesExist), //
0819                  QPoint(-2, -2));
0820         QCOMPARE(ChromaLightnessDiagramPrivate::nearestNeighborSearch(QPoint(1, 1), searchRectangle, doesExist), //
0821                  QPoint(-2, -2));
0822         QCOMPARE(ChromaLightnessDiagramPrivate::nearestNeighborSearch(QPoint(4, 4), searchRectangle, doesExist), //
0823                  QPoint(5, 5));
0824         QCOMPARE(ChromaLightnessDiagramPrivate::nearestNeighborSearch(QPoint(5, 5), searchRectangle, doesExist), //
0825                  QPoint(5, 5));
0826         QCOMPARE(ChromaLightnessDiagramPrivate::nearestNeighborSearch(QPoint(-100, 5), searchRectangle, doesExist), //
0827                  QPoint(-2, -2));
0828         QCOMPARE(ChromaLightnessDiagramPrivate::nearestNeighborSearch(QPoint(-100, -100), searchRectangle, doesExist), //
0829                  QPoint(-2, -2));
0830         QCOMPARE(ChromaLightnessDiagramPrivate::nearestNeighborSearch(QPoint(100, 100), searchRectangle, doesExist), //
0831                  QPoint(8, 8));
0832         QCOMPARE(ChromaLightnessDiagramPrivate::nearestNeighborSearch(QPoint(7, 100), searchRectangle, doesExist), //
0833                  QPoint(8, 8));
0834         QCOMPARE(ChromaLightnessDiagramPrivate::nearestNeighborSearch(QPoint(100, 7), searchRectangle, doesExist), //
0835                  QPoint(8, 8));
0836         QCOMPARE(ChromaLightnessDiagramPrivate::nearestNeighborSearch(QPoint(-2, 8), searchRectangle, doesExist), //
0837                  QPoint(5, 5));
0838         QCOMPARE(ChromaLightnessDiagramPrivate::nearestNeighborSearch(QPoint(8, -2), searchRectangle, doesExist), //
0839                  QPoint(5, 5));
0840     }
0841 };
0842 
0843 } // namespace PerceptualColor
0844 
0845 QTEST_MAIN(PerceptualColor::TestChromaLightnessDiagram)
0846 
0847 // The following “include” is necessary because we do not use a header file:
0848 #include "testchromalightnessdiagram.moc"