OmaPhoto 1.0.0: the Linux port of Compositor
Release / deb (push) Canceled after 0s
Release / rpm (push) Canceled after 0s
Release / arch (push) Canceled after 0s
Release / publish (push) Canceled after 0s

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>
This commit is contained in:
雷电芽衣andClaude committed 2026-09-26 00:44:52 -04:00
commit c864304695
564 files changed
+100804

No files matched your search

+219
View File
@@ -0,0 +1,219 @@
#include "CanvasFixtures.h"
#include "Rendering/TransformOverlay.h"
// The Move tool's box: its handles, hits, cursors and drawing.
namespace {
bool near(QPointF a, QPointF b)
{
return std::hypot(a.x() - b.x(), a.y() - b.y()) < 0.0001;
}
}
class TransformOverlayTests : public QObject {
Q_OBJECT
private slots:
void hoverRegionsMatchRotatedEdgesCornersAndRotationHandle();
void documentMappingAndRotatedHitTesting();
void resizeCursorsTurnWithTheBox();
void theBoxShowsForAMovableLayerAndHidesWithTheControls();
void handlesAndGuidesArePaintedWhereTheyBelong();
void anotherLayersHandlesRepaintOnlyTheirRects();
};
namespace {
// Every paint's region, to see how much a change repaints.
struct PaintSpy : QObject {
std::vector<QRect> rects;
bool eventFilter(QObject *, QEvent *event) override
{
if (event->type() == QEvent::Paint)
rects.push_back(static_cast<QPaintEvent *>(event)->region().boundingRect());
return false;
}
};
}
void TransformOverlayTests::hoverRegionsMatchRotatedEdgesCornersAndRotationHandle()
{
CanvasViewport viewport;
const QSizeF size(1000, 800);
viewport.resize(QSizeF(1000, 800), 1, size);
const LayerTransform transform{.origin = {100, 100}, .size = {400, 300}, .rotation = 37};
const TransformOverlayGeometry geometry(transform, viewport, size);
for (const auto &[start, end, expected] : {std::tuple(0, 2, 1), std::tuple(2, 4, 3), std::tuple(4, 6, 5), std::tuple(6, 0, 7)}) {
const QPointF a = geometry.handles[size_t(start)], b = geometry.handles[size_t(end)];
const QPointF point(a.x() * 0.75 + b.x() * 0.25, a.y() * 0.75 + b.y() * 0.25);
const TransformDrag::Mode hit = geometry.hit(point).value();
QVERIFY(hit.kind == TransformDrag::Kind::resize);
QCOMPARE(hit.index, expected);
}
for (const int index : {0, 2, 4, 6}) {
const TransformDrag::Mode hit = geometry.hit(geometry.handles[size_t(index)]).value();
QVERIFY(hit.kind == TransformDrag::Kind::resize);
QCOMPARE(hit.index, index);
}
QVERIFY(geometry.hit(geometry.rotationHandle).value().kind == TransformDrag::Kind::rotate);
QVERIFY(!geometry.hit(viewport.viewPoint(transform.center(), size)).has_value());
// The grip stands 28 points off the top edge, turned.
const double radians = transform.radians();
QVERIFY(near(geometry.rotationHandle, geometry.handles[1] + QPointF(std::sin(radians) * 28, -std::cos(radians) * 28)));
}
void TransformOverlayTests::documentMappingAndRotatedHitTesting()
{
CanvasViewport viewport;
const QSizeF size(1000, 800);
viewport.resize(QSizeF(900, 600), 2, size);
viewport.setZoom(1.5, QPointF(300, 200), size);
viewport.translate(QSizeF(57, -30));
const LayerTransform transform{.origin = {100, 200}, .size = {100, 50}, .rotation = 90};
const TransformOverlayGeometry geometry(transform, viewport, size);
QVERIFY(near(viewport.documentPoint(geometry.handles[4], size), transform.point(QPointF(1, 1))));
QVERIFY(geometry.hit(geometry.rotationHandle).value().kind == TransformDrag::Kind::rotate);
for (size_t index = 0; index < geometry.handles.size(); ++index) {
const TransformDrag::Mode hit = geometry.hit(geometry.handles[index]).value();
QVERIFY(hit.kind == TransformDrag::Kind::resize);
QCOMPARE(hit.index, int(index));
}
QVERIFY(!geometry.hit(viewport.viewPoint(transform.center(), size)).has_value());
}
void TransformOverlayTests::resizeCursorsTurnWithTheBox()
{
CanvasViewport viewport;
const QSizeF size(400, 300);
viewport.resize(QSizeF(400, 300), 1, size);
const TransformOverlayGeometry upright({.origin = {100, 100}, .size = {100, 50}}, viewport, size);
QCOMPARE(upright.resizeCursor(3), Qt::SizeHorCursor);
QCOMPARE(upright.resizeCursor(7), Qt::SizeHorCursor);
QCOMPARE(upright.resizeCursor(1), Qt::SizeVerCursor);
QCOMPARE(upright.resizeCursor(5), Qt::SizeVerCursor);
QCOMPARE(upright.resizeCursor(0), Qt::SizeFDiagCursor);
QCOMPARE(upright.resizeCursor(4), Qt::SizeFDiagCursor);
QCOMPARE(upright.resizeCursor(2), Qt::SizeBDiagCursor);
QCOMPARE(upright.resizeCursor(6), Qt::SizeBDiagCursor);
// Ten points reach a handle; more do not.
QCOMPARE(upright.hit(upright.handles[0] + QPointF(-9.5, 0)).value().index, 0);
QVERIFY(!upright.hit(upright.handles[0] + QPointF(-10.5, 0)).has_value());
QVERIFY(!upright.hit(upright.handles[0] + QPointF(-10.5, 0.5)).has_value());
// A quarter turn swaps the edges' cursors.
const TransformOverlayGeometry turned({.origin = {100, 100}, .size = {100, 50}, .rotation = 90}, viewport, size);
QCOMPARE(turned.resizeCursor(3), Qt::SizeVerCursor);
QCOMPARE(turned.resizeCursor(1), Qt::SizeHorCursor);
QCOMPARE(turned.resizeCursor(0), Qt::SizeBDiagCursor);
}
void TransformOverlayTests::theBoxShowsForAMovableLayerAndHidesWithTheControls()
{
Shown shown;
EditorSession &session = shown.session;
const TransformOverlay overlay(session);
QVERIFY(!overlay.geometry().has_value());
session.addBlankLayer();
QVERIFY(!session.canTransform());
QVERIFY(!overlay.geometry().has_value());
session.insert(filled(20, 20, qRgba(255, 0, 0, 255), "Red"));
const QUuid red = session.activeLayerID().value();
QVERIFY(session.canTransform());
QVERIFY(overlay.geometry().has_value());
QCOMPARE(overlay.geometry().value(), TransformOverlayGeometry(layerWith(session, red).transform, session.viewport, shown.documentSize()));
session.selectTool(NavigationTool::hand);
QVERIFY(!overlay.geometry().has_value());
session.selectTool(NavigationTool::move);
// Hidden controls: only a Ctrl+T transform shows its box.
session.setShowsTransformControls(false);
QVERIFY(!overlay.geometry().has_value());
session.beginTransform(false);
QVERIFY(!overlay.geometry().has_value());
session.cancelTransform();
session.beginTransform(true);
QVERIFY(overlay.geometry().has_value());
session.cancelTransform();
session.setShowsTransformControls(true);
session.toggleLayerVisibility(red);
QVERIFY(!overlay.geometry().has_value());
session.toggleLayerVisibility(red);
// A selection or a folder: the box around them all.
session.insert(filled(10, 10, qRgba(0, 0, 255, 255), "Blue"), QPointF(85, 85));
const QUuid blue = session.activeLayerID().value();
session.selectLayers({red, blue}, red);
QCOMPARE(overlay.geometry().value(), TransformOverlayGeometry(session.groupTransformBox().value(), session.viewport, shown.documentSize()));
session.beginTransform();
LayerTransform draft = session.transformEdit().value().draft;
draft.origin += QPointF(3, 0);
session.previewTransform(draft);
QCOMPARE(overlay.geometry().value(), TransformOverlayGeometry(draft, session.viewport, shown.documentSize()));
session.cancelTransform();
session.addGroup();
const QUuid folder = session.activeLayerID().value();
QVERIFY(session.placeLayer(blue, folder));
session.selectLayer(folder);
QCOMPARE(overlay.geometry().value(), TransformOverlayGeometry(session.groupTransformBox().value(), session.viewport, shown.documentSize()));
}
void TransformOverlayTests::handlesAndGuidesArePaintedWhereTheyBelong()
{
Shown shown;
CanvasView &canvas = *shown.canvas;
EditorSession &session = shown.session;
shown.settle();
session.zoom(1);
session.insert(filled(20, 20, qRgba(255, 0, 0, 255), "Red"));
QVERIFY(canvas.synchronizeDisplay());
QCOMPARE(session.viewport.viewPoint(QPointF(0, 0), shown.documentSize()), QPointF(150, 100));
const TransformOverlay overlay(session);
QCOMPARE(overlay.drawnRect(canvas.rect()), QRect(185, 107, 30, 58));
// White squares on the handles, a disc on the grip.
QImage shot = canvas.grab().toImage();
QCOMPARE(shot.pixelColor(190, 140), QColor(Qt::white));
QCOMPARE(shot.pixelColor(210, 160), QColor(Qt::white));
QCOMPARE(shot.pixelColor(200, 112), QColor(Qt::white));
// The accent outline runs between the handles, over the red.
QVERIFY(shot.pixelColor(190, 145) != QColor(Qt::red));
QVERIFY(shot.pixelColor(190, 145).blue() > shot.pixelColor(190, 145).red());
session.setShowsTransformControls(false);
QVERIFY(overlay.drawnRect(canvas.rect()).isEmpty());
shot = canvas.grab().toImage();
QVERIFY(shot.pixelColor(190, 140) != QColor(Qt::white));
QVERIFY(shot.pixelColor(200, 112) != QColor(Qt::white));
QCOMPARE(shot.pixelColor(190, 145), QColor(Qt::red));
// A guide: an accent line across the document.
session.snapGuides = {{50}, {}};
QVERIFY(!canvas.synchronizeDisplay());
QCOMPARE(overlay.drawnRect(canvas.rect()), canvas.rect());
shot = canvas.grab().toImage();
QCOMPARE(shot.pixelColor(200, 190), canvas.palette().color(QPalette::Highlight));
QCOMPARE(shot.pixelColor(200, 101), canvas.palette().color(QPalette::Highlight));
QVERIFY(shot.pixelColor(230, 190) != canvas.palette().color(QPalette::Highlight));
session.snapGuides = {{}, {30}};
canvas.synchronizeDisplay();
shot = canvas.grab().toImage();
QCOMPARE(shot.pixelColor(170, 130), canvas.palette().color(QPalette::Highlight));
QVERIFY(shot.pixelColor(200, 190) != canvas.palette().color(QPalette::Highlight));
}
void TransformOverlayTests::anotherLayersHandlesRepaintOnlyTheirRects()
{
Shown shown;
CanvasView &canvas = *shown.canvas;
EditorSession &session = shown.session;
shown.settle();
session.zoom(1);
session.insert(filled(20, 20, qRgba(255, 0, 0, 255), "Red"), QPointF(20, 20));
const QUuid red = session.activeLayerID().value();
session.insert(filled(10, 10, qRgba(0, 0, 255, 255), "Blue"), QPointF(80, 80));
canvas.synchronizeDisplay();
QCoreApplication::processEvents();
PaintSpy spy;
canvas.installEventFilter(&spy);
// The old box and the new one, nothing more.
session.selectLayer(red);
QVERIFY(!canvas.synchronizeDisplay());
QCoreApplication::processEvents();
QCOMPARE(int(spy.rects.size()), 1);
QCOMPARE(spy.rects[0], QRect(155, 77, 85, 113));
canvas.removeEventFilter(&spy);
}
QTEST_MAIN(TransformOverlayTests)
#include "TransformOverlayTests.moc"