#include "SessionFixtures.h" #include "UI/ColorPickerSheet.h" #include #include #include #include // Swift's ColorPickerTests, and the session's picker. namespace { QWidget *shownPanel() { for (QWidget *widget : QApplication::topLevelWidgets()) { if (widget->objectName() == ColorPickerPanelController::identifier() && widget->isVisible()) return widget; } return nullptr; } // Red over its top two rows, blue under. std::unique_ptr splitSession() { auto session = std::make_unique(); session->createDocument(4, 4); QImage image(4, 4, QImage::Format_RGBA8888_Premultiplied); image.fill(Qt::blue); for (int y = 0; y < 2; ++y) { for (int x = 0; x < 4; ++x) image.setPixelColor(x, y, Qt::red); } session->insert(ImportedImage(image, image, QStringLiteral("Split"))); return session; } } class ColorPickerTests : public QObject { Q_OBJECT private slots: void hexParsesFullShorthandAndRejectsInvalid(); void hsbRoundTripsEightBitColors(); void graysAndBlackKeepPreviousHueAndSaturation(); void hueTurnsTheWheelBySixths(); void canvasSamplingReadsCompositeAndCommitsOnlyOnOK(); void samplingUndoesPremultiplication(); void pickersOpenOnlyWhereTheyMay(); void textPickersColourTheTextAndTheSwatch(); void pickerReopensWhereItWasLastLeft(); }; void ColorPickerTests::hexParsesFullShorthandAndRejectsInvalid() { QCOMPARE(PaletteColor::fromHex(QStringLiteral("#FF8000")), std::optional(PaletteColor{1, 128.0 / 255, 0})); QCOMPARE(PaletteColor::fromHex(QStringLiteral("0f0")), std::optional(PaletteColor{0, 1, 0})); QCOMPARE(PaletteColor::fromHex(QStringLiteral(" 00ff00 ")), std::optional(PaletteColor{0, 1, 0})); QVERIFY(!PaletteColor::fromHex(QStringLiteral("12345"))); QVERIFY(!PaletteColor::fromHex(QStringLiteral("GGGGGG"))); QCOMPARE((PaletteColor{1, 128.0 / 255, 0}).hex(), QString("FF8000")); // Swift trims tabs and wide spaces, never line breaks. QCOMPARE(PaletteColor::fromHex(QStringLiteral("\t#abc") + QChar(0x00A0)), std::optional(PaletteColor{170.0 / 255, 187.0 / 255, 204.0 / 255})); QVERIFY(!PaletteColor::fromHex(QStringLiteral("\n00ff00"))); // Its UInt32 takes signs, minus for zero only; no 0x. QCOMPARE(PaletteColor::fromHex(QStringLiteral("+0F800")), std::optional(PaletteColor{0, 248.0 / 255, 0})); QCOMPARE(PaletteColor::fromHex(QStringLiteral("-00000")), std::optional(PaletteColor::black())); QVERIFY(!PaletteColor::fromHex(QStringLiteral("-00001"))); QVERIFY(!PaletteColor::fromHex(QStringLiteral("0x1234"))); QVERIFY(!PaletteColor::fromHex(QStringLiteral("#"))); QVERIFY(!PaletteColor::fromHex(QStringLiteral("##abc"))); QCOMPARE((PaletteColor{0.2, 0.5, 1}).quantized(), (PaletteColor{51.0 / 255, 128.0 / 255, 1})); // Halves round up, as Swift's; every digit and case reads. QCOMPARE((PaletteColor{0.5, 0.5, 0.5}).hex(), QString("808080")); QCOMPARE(PaletteColor::fromHex(QStringLiteral("#99AAbb")), std::optional(PaletteColor{153.0 / 255, 170.0 / 255, 187.0 / 255})); QVERIFY(!PaletteColor::fromHex(QStringLiteral("gggggg"))); } void ColorPickerTests::hsbRoundTripsEightBitColors() { for (const char *hex : {"000000", "FFFFFF", "FF0000", "00FF00", "0000FF", "FF8000", "7F3FA2", "123456"}) QCOMPARE(PickerHSB(PaletteColor::fromHex(QString::fromLatin1(hex)).value()).rgb().quantized().hex(), QString::fromLatin1(hex)); } void ColorPickerTests::graysAndBlackKeepPreviousHueAndSaturation() { PickerHSB hsb(PaletteColor::fromHex(QStringLiteral("FF8000")).value()); const double hue = hsb.hue; hsb.setRGB(PaletteColor::fromHex(QStringLiteral("808080")).value()); QVERIFY(hsb.hue == hue && hsb.saturation == 0); hsb.saturation = 0.5; hsb.setRGB(PaletteColor::black()); QVERIFY(hsb.hue == hue && hsb.saturation == 0.5 && hsb.brightness == 0); } void ColorPickerTests::hueTurnsTheWheelBySixths() { // Each sixth, and hues past the circle either way. const std::vector> hues{{0, "FF0000"}, {30, "FF8000"}, {90, "80FF00"}, {150, "00FF80"}, {210, "0080FF"}, {270, "8000FF"}, {330, "FF0080"}, {360, "FF0000"}, {-60, "FF00FF"}, {420, "FFFF00"}}; for (const auto &[hue, hex] : hues) QCOMPARE(PickerHSB(hue, 1, 1).rgb().quantized().hex(), hex); QCOMPARE(PickerHSB(0, 0.5, 0.5).rgb(), (PaletteColor{0.5, 0.25, 0.25})); // Hues from each channel on top, as Swift reads them. QCOMPARE(PickerHSB(PaletteColor{0, 1, 0.5}).hue, 150.0); QCOMPARE(PickerHSB(PaletteColor{0.5, 0, 1}).hue, 270.0); QCOMPARE(PickerHSB(PaletteColor{1, 0, 0.5}).hue, 330.0); } void ColorPickerTests::canvasSamplingReadsCompositeAndCommitsOnlyOnOK() { const std::unique_ptr session = splitSession(); QCOMPARE(session->sampleCompositeColor(QPointF(1.5, 0.5)).value().hex(), QString("FF0000")); QCOMPARE(session->sampleCompositeColor(QPointF(1.5, 3.5)).value().hex(), QString("0000FF")); QVERIFY(!session->sampleCompositeColor(QPointF(-1, 1))); session->openColorPicker(false); session->sampleIntoColorPicker(QPointF(2, 3)); QCOMPARE(session->colorPicker().value().color().hex(), QString("0000FF")); QCOMPARE(session->foregroundColor(), PaletteColor::black()); session->closeColorPicker(false); QVERIFY(session->foregroundColor() == PaletteColor::black() && !session->colorPicker()); session->openColorPicker(true); session->sampleIntoColorPicker(QPointF(2, 0)); session->closeColorPicker(true); QVERIFY(session->backgroundColor().hex() == QString("FF0000") && session->foregroundColor() == PaletteColor::black()); } void ColorPickerTests::samplingUndoesPremultiplication() { auto session = std::make_unique(); session->createDocument(4, 4); // Half-clear orange, stored premultiplied, reads as orange. QImage image(4, 4, QImage::Format_RGBA8888_Premultiplied); image.fill(QColor(255, 128, 0, 128)); session->insert(ImportedImage(image, image, QStringLiteral("Orange"))); QCOMPARE(session->sampleCompositeColor(QPointF(0, 0)).value(), (PaletteColor{1, 128.0 / 255, 0})); // Past the far edges is outside, even over a layer. QImage wide(6, 6, QImage::Format_RGBA8888_Premultiplied); wide.fill(Qt::blue); session->insert(ImportedImage(wide, wide, QStringLiteral("Wide"))); for (const QPointF point : {QPointF(4, 1), QPointF(1, 4), QPointF(std::nan(""), 1), QPointF(1, std::nan(""))}) QVERIFY(!session->sampleCompositeColor(point)); QCOMPARE(session->sampleCompositeColor(QPointF(3.9, 3.9)).value().hex(), QString("0000FF")); // A clear pixel has no colour; neither does no document. session->createDocument(4, 4); QVERIFY(!session->sampleCompositeColor(QPointF(1, 1))); session->openColorPicker(false); const QUuid id = session->colorPicker().value().id; QSignalSpy changed(session.get(), &EditorSession::changed); session->sampleIntoColorPicker(QPointF(1, 1)); QCOMPARE(changed.count(), 0); QCOMPARE(session->colorPicker().value().id, id); QVERIFY(!EditorSession().sampleCompositeColor(QPointF(0, 0))); } void ColorPickerTests::pickersOpenOnlyWhereTheyMay() { const std::unique_ptr session = splitSession(); QSignalSpy changed(session.get(), &EditorSession::changed); // Closing nothing is silent; so is an unchanged colour. session->closeColorPicker(true); QCOMPARE(changed.count(), 0); session->openColorPicker(true); QCOMPARE(changed.count(), 1); QCOMPARE(session->colorPicker().value().target.title(), QString("Color Picker (Background Color)")); QCOMPARE(session->colorPicker().value().original, PaletteColor::white()); session->setColorPickerHSB(session->colorPicker().value().hsb); QCOMPARE(changed.count(), 1); session->setColorPickerHSB(PickerHSB(120, 1, 1)); QCOMPARE(changed.count(), 2); // Another swatch opens anew, its colour its own. const QUuid first = session->colorPicker().value().id; session->openColorPicker(false); QCOMPARE(session->colorPicker().value().target.title(), QString("Color Picker (Foreground Color)")); QVERIFY(session->colorPicker().value().id != first); QCOMPARE(session->colorPicker().value().color(), PaletteColor::black()); // Cancel keeps the palette; OK writes the working colour. session->setColorPickerHSB(PickerHSB(240, 1, 1)); session->closeColorPicker(false); QCOMPARE(session->foregroundColor(), PaletteColor::black()); session->openColorPicker(false); session->setColorPickerHSB(PickerHSB(240, 1, 1)); session->closeColorPicker(true); QCOMPARE(session->foregroundColor(), (PaletteColor{0, 0, 1})); // A mask's palette is black and white: no picker opens. const QUuid id = session->activeLayerID().value(); session->addMask(); session->selectLayerTarget(id, true); QVERIFY(session->isMaskSelected()); changed.clear(); session->openColorPicker(false); QVERIFY(!session->colorPicker()); QCOMPARE(changed.count(), 0); // A mask chosen meanwhile keeps the palette on OK. session->selectLayerTarget(id, false); session->openColorPicker(true); session->setColorPickerHSB(PickerHSB(0, 1, 1)); session->selectLayerTarget(id, true); session->closeColorPicker(true); QVERIFY(!session->colorPicker()); QCOMPARE(session->backgroundColor(), PaletteColor::white()); QVERIFY(!session->maskPaintWhite()); // The background announces itself. changed.clear(); session->selectLayerTarget(id, false); changed.clear(); session->setBackgroundColor(PaletteColor{0, 0, 1}); QCOMPARE(changed.count(), 1); } void ColorPickerTests::textPickersColourTheTextAndTheSwatch() { const std::unique_ptr session = splitSession(); const PaletteColor cool = PickerHSB(PaletteColor{0.2, 0.4, 0.6}).rgb().quantized(); // Every channel apart from cool's, so each write shows. const PaletteColor warm = PickerHSB(PaletteColor{0.7, 0.5, 0.1}).rgb().quantized(); // Type's swatch opens only under Type, and only once. session->openTextColorPicker(); QVERIFY(!session->colorPicker()); session->selectTool(NavigationTool::type); QSignalSpy changed(session.get(), &EditorSession::changed); session->openTextColorPicker(); QCOMPARE(changed.count(), 1); QCOMPARE(session->colorPicker().value().target.title(), QString("Color Picker (Text Color)")); const QUuid id = session->colorPicker().value().id; session->openTextColorPicker(); QCOMPARE(session->colorPicker().value().id, id); // Cancelled, it leaves the next text and the swatch. session->setColorPickerHSB(PickerHSB(cool)); session->closeColorPicker(false); QCOMPARE(session->textDefaults().blue, 0.0); QCOMPARE(session->foregroundColor(), PaletteColor::black()); // No text open: the next text's colour, and the foreground. session->openTextColorPicker(); session->setColorPickerHSB(PickerHSB(cool)); session->closeColorPicker(true); const LayerTextStyle defaults = session->textDefaults(); QCOMPARE((PaletteColor{defaults.red, defaults.green, defaults.blue}), cool); QCOMPARE(session->foregroundColor(), cool); // Open text takes it, the swatch showing the text's colour. session->beginText(QPointF(1, 1), true); QCOMPARE(session->typeColor(), cool); session->openTextColorPicker(); QCOMPARE(session->colorPicker().value().original, cool); session->setColorPickerHSB(PickerHSB(warm)); session->closeColorPicker(true); QCOMPARE(session->typeColor(), warm); QCOMPARE(session->foregroundColor(), warm); // Another draft by then, or another tool, takes nothing. session->openTextColorPicker(); session->setColorPickerHSB(PickerHSB(120, 1, 1)); session->cancelText(); session->closeColorPicker(true); QCOMPARE(session->textDefaults().blue, cool.blue); QCOMPARE(session->foregroundColor(), warm); session->openTextColorPicker(); session->setColorPickerHSB(PickerHSB(120, 1, 1)); session->selectTool(NavigationTool::brush); session->closeColorPicker(true); QCOMPARE(session->foregroundColor(), warm); // Beside live text, no draft: the defaults; nothing opens. session->selectTool(NavigationTool::type); session->beginText(QPointF(1, 1), true); TextDraft draft = session->textDraft().value(); draft.style.content = QStringLiteral("Hi"); session->setTextDraft(draft); QVERIFY(session->finishText()); QVERIFY(session->activeLayer().value().liveText()); // With its mask chosen, the swatch keeps its colour. session->addMask(); QVERIFY(session->isMaskSelected()); session->openTextColorPicker(); session->setColorPickerHSB(PickerHSB(240, 1, 1)); session->closeColorPicker(true); QVERIFY(!session->textDraft()); QCOMPARE(session->textDefaults().blue, 1.0); QCOMPARE(session->foregroundColor(), warm); // Busy, nothing opens. session->setIsProjectBusy(true); session->openTextColorPicker(); session->openColorPicker(false); QVERIFY(!session->colorPicker()); } void ColorPickerTests::pickerReopensWhereItWasLastLeft() { const std::unique_ptr session = splitSession(); QWidget window; window.resize(400, 300); window.show(); ColorPickerPanelController controller(window); session->openColorPicker(false); controller.show(session->colorPicker().value(), *session); QWidget *panel = shownPanel(); QVERIFY(panel); const QPoint spot(40, 40); panel->move(spot); session->closeColorPicker(false); controller.close(); QVERIFY(!panel->isVisible()); session->openColorPicker(true); controller.show(session->colorPicker().value(), *session); QCOMPARE(shownPanel(), panel); QCOMPARE(panel->pos(), spot); QCOMPARE(panel->windowTitle(), QString("Color Picker (Background Color)")); // Switching swatches while open keeps it in place too. session->openColorPicker(false); controller.show(session->colorPicker().value(), *session); QCOMPARE(panel->pos(), spot); session->closeColorPicker(false); controller.close(); } QTEST_MAIN(ColorPickerTests) #include "ColorPickerTests.moc"