Files
OmaPhoto/tests/LevelsEdgeTests.cpp
雷电芽衣andClaude c864304695
Release / deb (push) Canceled after 0s
Release / rpm (push) Canceled after 0s
Release / arch (push) Canceled after 0s
Release / publish (push) Canceled after 0s
OmaPhoto 1.0.0: the Linux port of Compositor
A layered image editor for Linux and Omarchy, ported from Compositor
(github.com/robbietilton/Compositor), whose Swift source is its
specification. It reads and writes Compositor's .comp projects.

Packages for Ubuntu, Fedora and Arch come from the release workflow;
scripts/install/ holds one install script per distribution family,
and flake.nix serves NixOS.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-26 00:44:52 -04:00

240 lines
8.9 KiB
C++

#include "LevelsFixtures.h"
#include "Rendering/RasterSnapshot.h"
#include <QSignalSpy>
#include <QThreadPool>
// Levels at its edges: order, signals, stale runs, sizes, samples.
namespace {
LevelsSettings inverted()
{
LevelsSettings settings;
settings.setCurrent(LevelRange{0, 1, 255, 255, 0});
return settings;
}
LevelsSettings brighter()
{
LevelsSettings settings;
settings.setCurrent(LevelRange{0, 2, 255, 0, 255});
return settings;
}
}
class LevelsEdgeTests : public QObject {
Q_OBJECT
private slots:
void beginEndsATransformAndALasso();
void signalsFollowEveryStep();
void turningPreviewOffDropsTheQueue();
void anIdentityCommitIsImmediate();
void stalePreviewsNeverLand();
void aDoneRunLandsBeforeTheNextStarts();
void theRebuildDropsText();
void thePreviewIsTheEditsLayerAlone();
void scaledSidesTruncateAsSwifts();
void samplesStopAtTheDocumentAndClearPixels();
};
void LevelsEdgeTests::beginEndsATransformAndALasso()
{
const std::unique_ptr<EditorSession> session = rampSession();
session->selectTool(NavigationTool::move);
session->beginTransform();
LayerTransform moved = session->activeLayer().value().transform;
moved.origin = QPointF(1, 0);
session->previewTransform(moved);
session->beginLevels();
QVERIFY(!session->transformEdit());
QCOMPARE(session->levels().value().transform.origin, QPointF(1, 0));
// Placed: its pixels map through the new place.
QCOMPARE(session->levels().value().mapping.map(QPointF(0, 0)), QPointF(1, 0));
QCOMPARE(session->levels().value().previewMapping, session->levels().value().mapping);
session->cancelLevels();
session->selectTool(NavigationTool::lasso);
session->beginLasso(QPointF(0, 0), SelectionMode::replace);
QVERIFY(session->lassoDraft());
session->beginLevels();
QVERIFY(!session->lassoDraft());
session->cancelLevels();
}
void LevelsEdgeTests::signalsFollowEveryStep()
{
const std::unique_ptr<EditorSession> session = rampSession();
QSignalSpy changed(session.get(), &EditorSession::changed);
session->beginLevels();
QCOMPARE(changed.count(), 1);
QTRY_VERIFY(session->levels().value().histogramReady);
QCOMPARE(changed.count(), 2);
changed.clear();
session->updateLevels(inverted(), true);
QCOMPARE(changed.count(), 1);
// The preview lands with a redraw and a signal.
const int revision = session->brushRevision();
QTRY_VERIFY(session->levels().value().preparedPreview);
QCOMPARE(session->brushRevision(), revision + 1);
QCOMPARE(changed.count(), 2);
changed.clear();
session->updateLevels(inverted(), false);
QCOMPARE(changed.count(), 1);
QVERIFY(!session->levels().value().preview);
const int shown = session->brushRevision();
changed.clear();
session->cancelLevels();
QCOMPARE(changed.count(), 1);
QCOMPARE(session->brushRevision(), shown + 1);
session->beginLevels();
session->updateLevels(inverted(), false);
const int committing = session->brushRevision();
commit(*session);
QCOMPARE(session->brushRevision(), committing + 1);
}
void LevelsEdgeTests::turningPreviewOffDropsTheQueue()
{
const std::unique_ptr<EditorSession> session = bigSession();
session->beginLevels();
session->updateLevels(brighter(), true);
session->updateLevels(inverted(), true);
QVERIFY(session->levels().value().pending);
session->updateLevels(inverted(), false);
QVERIFY(!session->levels().value().pending);
QTest::qWait(200);
QVERIFY(!session->levels().value().preparedPreview);
session->cancelLevels();
}
void LevelsEdgeTests::anIdentityCommitIsImmediate()
{
const std::unique_ptr<EditorSession> session = rampSession();
session->beginLevels();
bool done = false;
session->commitLevels([&done] { done = true; });
QVERIFY(!session->levels() && !session->isProjectBusy());
QTRY_VERIFY(done);
}
void LevelsEdgeTests::stalePreviewsNeverLand()
{
const std::unique_ptr<EditorSession> session = bigSession();
// A closed edit's run never lands on the next one.
session->beginLevels();
session->updateLevels(brighter(), true);
session->cancelLevels();
session->beginLevels();
session->updateLevels(inverted(), true);
int revision = session->brushRevision();
QTRY_VERIFY(session->levels().value().preparedPreview);
QCOMPARE(session->brushRevision(), revision + 1);
QCOMPARE(session->levels().value().preparedPreview.value().pixelColor(0, 0), QColor(Qt::cyan));
// Nor one turned off, then on again.
session->updateLevels(brighter(), true);
session->updateLevels(brighter(), false);
session->updateLevels(inverted(), true);
revision = session->brushRevision();
QTRY_VERIFY(session->levels().value().preparedPreview && !session->levels().value().pending);
QCOMPARE(session->brushRevision(), revision + 1);
// Nor one a commit overtook, nor the queued one.
session->updateLevels(brighter(), true);
session->updateLevels(inverted(), true);
QVERIFY(session->levels().value().pending);
revision = session->brushRevision();
bool done = false;
session->commitLevels([&done] { done = true; });
QVERIFY(!session->levels().value().pending);
QTRY_VERIFY(done);
QCOMPARE(session->brushRevision(), revision + 1);
}
void LevelsEdgeTests::aDoneRunLandsBeforeTheNextStarts()
{
const std::unique_ptr<EditorSession> session = rampSession();
session->beginLevels();
QTRY_VERIFY(session->levels().value().histogramReady);
const int revision = session->brushRevision();
session->updateLevels(brighter(), true);
// Done but undelivered: Qt would drop it for the next.
QThreadPool::globalInstance()->waitForDone();
session->updateLevels(inverted(), true);
QVERIFY(session->levels().value().pending);
QTRY_VERIFY(!session->levels().value().pending && session->brushRevision() == revision + 2);
QCOMPARE(session->levels().value().preparedPreview.value().pixelColor(0, 0), QColor(Qt::white));
session->cancelLevels();
}
void LevelsEdgeTests::theRebuildDropsText()
{
EditorSession session;
session.createDocument(200, 100);
session.selectTool(NavigationTool::type);
session.beginText(QPointF(10, 10), true);
TextDraft draft = session.textDraft().value();
draft.style.content = QStringLiteral("Hi");
session.setTextDraft(draft);
QVERIFY(session.finishText());
QVERIFY(session.activeLayer().value().liveText());
session.beginLevels();
session.updateLevels(inverted(), false);
commit(session);
QVERIFY(!session.activeLayer().value().text);
}
void LevelsEdgeTests::thePreviewIsTheEditsLayerAlone()
{
const std::unique_ptr<EditorSession> session = rampSession();
session->beginLevels();
session->updateLevels(inverted(), true);
QTRY_VERIFY(session->levels().value().preparedPreview);
const LevelsEdit &edit = session->levels().value();
QVERIFY(edit.previewImage(edit.layerID));
QVERIFY(!edit.previewImage(QUuid::createUuid()));
session->cancelLevels();
}
void LevelsEdgeTests::scaledSidesTruncateAsSwifts()
{
// The long side sets the factor, tall or wide.
for (const auto &[size, preview] : {std::pair(QSize(2, 9000), QSize(1, 8000)), std::pair(QSize(8311, 2), QSize(7999, 1))}) {
EditorSession session;
session.createDocument(size.width(), size.height());
QImage image(size, QImage::Format_RGBA8888_Premultiplied);
image.fill(Qt::red);
session.insert(ImportedImage(image, image, QStringLiteral("Red")));
session.beginLevels();
QCOMPARE(session.levels().value().previewSource.size(), preview);
session.cancelLevels();
}
}
void LevelsEdgeTests::samplesStopAtTheDocumentAndClearPixels()
{
// The ramp at twice its width reaches past the document.
EditorSession session;
session.createDocument(6, 1);
const QImage source = image(ramp);
session.insert(ImportedImage(source, source, QStringLiteral("Ramp")));
LayerTransform wide = session.activeLayer().value().transform;
wide.size = QSizeF(12, 1);
session.beginTransform();
session.previewTransform(wide);
session.commitTransform();
session.beginLevels();
session.setLevelsSampleMode(LevelsSample::black);
session.sampleLevels(QPointF(6, 0.5));
QVERIFY(session.levels().value().settings.isIdentity());
session.cancelLevels();
// A clear pixel sets nothing; a soft one reads unpremultiplied.
const std::unique_ptr<EditorSession> plain = rampSession();
plain->beginLevels();
plain->setLevelsSampleMode(LevelsSample::black);
plain->sampleLevels(QPointF(5.5, 0.5));
QVERIFY(plain->levels().value().settings.isIdentity());
plain->sampleLevels(QPointF(4.5, 0.5));
const LevelsSettings soft = plain->levels().value().settings;
QVERIFY(soft.ranges[1].black == 127.5 && soft.ranges[2].black == 63.75 && soft.ranges[3].black == 0);
plain->cancelLevels();
}
QTEST_MAIN(LevelsEdgeTests)
#include "LevelsEdgeTests.moc"