Files
OmaPhoto/tests/TypeToolTests.cpp
T
雷电芽衣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

222 lines
8.9 KiB
C++

#include "BrushFixtures.h"
#include "SessionFixtures.h"
#include "Document/EditorSession.h"
#include "IO/ImageExporter.h"
#include <QTemporaryDir>
// Swift's TypeToolTests: text layers made, edited, saved, clipped.
namespace {
std::unique_ptr<EditorSession> makeSession()
{
auto session = std::make_unique<EditorSession>();
session->createDocument(800, 600, true);
session->selectTool(NavigationTool::type);
return session;
}
// Swift writes `session.textDraft?.style.content` in place.
void type(EditorSession &session, const QString &content)
{
TextDraft draft = session.textDraft().value();
draft.style.content = content;
session.setTextDraft(draft);
}
// Opaque ink and clear pixels of an image, counted.
std::pair<int, int> inkAndClear(const QImage &image, const std::function<bool(const std::vector<int> &)> &inked)
{
int ink = 0, clear = 0;
for (int y = 0; y < image.height(); ++y) {
for (int x = 0; x < image.width(); ++x) {
const std::vector<int> at = pixel(image, x, y);
if (at[3] == 0)
++clear;
else if (inked(at))
++ink;
}
}
return {ink, clear};
}
}
class TypeToolTests : public QObject {
Q_OBJECT
private slots:
void createEditCancelAndUndo();
void transformsDuplicatesAndClippingKeepTextEditable();
void saveReopenAndRasterize();
void rasterHasTransparentBackgroundAndColoredGlyphs();
void clippingToTextExportsColoredGlyphsOnTransparency();
void paragraphBoxAndToolSwitchCommitEditableText();
void emptyNewParagraphIsDiscarded();
void invalidAndStaleDraftsDoNotChangeDocument();
};
void TypeToolTests::createEditCancelAndUndo()
{
const auto session = makeSession();
const int before = session->history.undoCount();
session->beginText(QPointF(30, 40));
type(*session, QStringLiteral("Text"));
QCOMPARE(session->document().value().layers.size(), size_t(1));
TextDraft draft = session->textDraft().value();
draft.style.content = QStringLiteral("Hello\nCompositor");
draft.style.fontSize = 48;
QVERIFY(session->applyText(draft));
QCOMPARE(session->activeLayer().value().liveText().value().style, draft.style);
QCOMPARE(session->activeLayer().value().origin(), QPointF(30, 40));
QCOMPARE(session->history.undoCount(), before + 1);
session->editActiveText();
session->setTextDraft(std::nullopt);
QCOMPARE(session->history.undoCount(), before + 1);
session->editActiveText();
draft = session->textDraft().value();
draft.style.content = QStringLiteral("Changed");
QVERIFY(session->applyText(draft));
session->undo();
QCOMPARE(session->activeLayer().value().liveText().value().style.content, QString("Hello\nCompositor"));
session->undo();
QCOMPARE(session->document().value().layers.size(), size_t(1));
session->redo();
QVERIFY(session->activeLayer().value().liveText());
}
void TypeToolTests::transformsDuplicatesAndClippingKeepTextEditable()
{
const auto session = makeSession();
session->beginText(QPointF(20, 20));
type(*session, QStringLiteral("Text"));
QVERIFY(session->applyText(session->textDraft().value()));
const QUuid id = session->activeLayerID().value();
rewrite(*session, [&](ProjectSnapshot &snapshot) {
record(snapshot, id).transform.rotation = 30;
record(snapshot, id).transform.size.rwidth() *= 2;
});
const LayerTransform old = session->activeLayer().value().transform;
session->editActiveText();
TextDraft draft = session->textDraft().value();
draft.style.content = QStringLiteral("Longer text");
QVERIFY(session->applyText(draft));
const LayerTransform updated = session->activeLayer().value().transform;
QCOMPARE(updated.rotation, 30.0);
QVERIFY(std::abs(updated.point(QPointF(0, 0)).x() - old.point(QPointF(0, 0)).x()) < 0.001);
QVERIFY(std::abs(updated.point(QPointF(0, 0)).y() - old.point(QPointF(0, 0)).y()) < 0.001);
session->duplicateActiveLayer();
QCOMPARE(session->activeLayer().value().liveText().value().style.content, QString("Longer text"));
const QUuid target = session->activeLayerID().value();
QVERIFY(session->linkMask(id, target));
QCOMPARE(session->activeLayer().value().maskSourceID, std::optional(id));
QVERIFY(layerWith(*session, id).liveText());
}
void TypeToolTests::saveReopenAndRasterize()
{
const auto session = makeSession();
session->beginText(QPointF(0, 0));
type(*session, QStringLiteral("Text"));
TextDraft draft = session->textDraft().value();
draft.style.content = QStringLiteral("Café 日本語\nSecond line");
draft.style.alignment = TextAlignment::right;
draft.style.tracking = 3;
QVERIFY(session->applyText(draft));
QTemporaryDir folder;
const QString path = folder.filePath(QStringLiteral("text.comp"));
ProjectStore::save(session->projectSnapshot().value(), path);
const auto reopened = makeSession();
reopened->installProject(ProjectStore::load(path), path);
QCOMPARE(reopened->activeLayer().value().liveText().value().style, draft.style);
// Other pixels in its place: no longer text.
ImageLayer painted = reopened->activeLayer().value();
const QImage replacement = EditorSession::shapeImage(ShapeKind::rectangle, QSizeF(10, 10), PaletteColor{1, 0, 0});
painted.asset = ImportedImage(replacement, replacement, QStringLiteral("Painted"));
QVERIFY(!painted.liveText());
QVERIFY(!painted.hierarchyRecord().text);
}
void TypeToolTests::rasterHasTransparentBackgroundAndColoredGlyphs()
{
LayerTextStyle style;
style.content = QStringLiteral("TYPE");
style.red = 1;
const QImage image = EditorSession::textImage(style);
bool pure = true;
const auto [ink, clear] = inkAndClear(image, [&](const std::vector<int> &at) {
pure = pure && at[0] > 0 && at[1] == 0 && at[2] == 0;
return true;
});
QVERIFY(pure);
QVERIFY(ink > 100 && clear > 100);
}
void TypeToolTests::clippingToTextExportsColoredGlyphsOnTransparency()
{
const auto session = makeSession();
session->beginText(QPointF(0, 0));
type(*session, QStringLiteral("Text"));
QVERIFY(session->applyText(session->textDraft().value()));
const QUuid source = session->activeLayerID().value();
const QSizeF size = session->activeLayer().value().size();
const QImage fill = EditorSession::shapeImage(ShapeKind::rectangle, size, PaletteColor{1, 0, 0});
session->addPixelLayer(fill, QPointF(0, 0), QStringLiteral("Clipped color"), QStringLiteral("Fill"));
QVERIFY(session->linkMask(source, session->activeLayerID().value()));
const QImage exported = ImageExporter::render(session->projectSnapshot().value()).image;
bool red = true;
const auto [ink, clear] = inkAndClear(exported, [&](const std::vector<int> &at) {
if (at[3] != 255)
return false;
red = red && at[0] == 255 && at[1] == 0;
return true;
});
QVERIFY(red);
QVERIFY(ink > 100 && clear > 100);
}
void TypeToolTests::paragraphBoxAndToolSwitchCommitEditableText()
{
const auto session = makeSession();
session->beginText(QRectF(40, 60, 200, 120));
QCOMPARE(session->textDraft().value().style.content, QString());
type(*session, QStringLiteral("Text that wraps inside its paragraph box"));
session->selectTool(NavigationTool::brush);
QVERIFY(!session->textDraft() && session->tool() == NavigationTool::brush);
QCOMPARE(session->activeLayer().value().size(), QSizeF(200, 120));
QCOMPARE(session->activeLayer().value().liveText().value().style.boxSize, std::optional(QSizeF(200, 120)));
session->selectTool(NavigationTool::type);
session->editActiveText();
type(*session, QStringLiteral("Edited on canvas"));
session->cancelText();
QCOMPARE(session->activeLayer().value().liveText().value().style.content, QString("Text that wraps inside its paragraph box"));
}
void TypeToolTests::emptyNewParagraphIsDiscarded()
{
const auto session = makeSession();
const size_t count = session->document().value().layers.size();
session->beginText(QRectF(0, 0, 100, 100));
session->selectTool(NavigationTool::brush);
QCOMPARE(session->document().value().layers.size(), count);
QVERIFY(!session->textDraft());
}
void TypeToolTests::invalidAndStaleDraftsDoNotChangeDocument()
{
const auto session = makeSession();
session->beginText(QPointF(0, 0));
type(*session, QStringLiteral("Text"));
TextDraft draft = session->textDraft().value();
draft.style.fontSize = NAN;
QVERIFY(!session->applyText(draft));
draft.style.fontSize = 72;
draft.style.boxSize = QSizeF(0, 100);
QVERIFY(!session->applyText(draft));
draft.style.boxSize = QSizeF(360, 160);
draft.style.content = QStringLiteral("Valid");
session->setTextDraft(std::nullopt);
session->createDocument(100, 100, true);
QVERIFY(!session->applyText(draft));
QCOMPARE(session->document().value().layers.size(), size_t(1));
}
QTEST_MAIN(TypeToolTests)
#include "TypeToolTests.moc"