Typed text drawn as its pixels; effects kept while typing, painting masks and undoing (12.12a)
This commit is contained in:
1 parent
8fbb104c48
commit
136ee65a48
23 files changed
+1125
-153
No files matched your search
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
#include "Document/LayerEffects+Renderer.h"
|
||||
#include "IO/ImageExporter.h"
|
||||
#include "Rendering/EditorCanvas.h"
|
||||
#include "RenderFixtures.h"
|
||||
#include "SessionFixtures.h"
|
||||
#include <QtTest>
|
||||
|
||||
// A square with a stroke effect, on a white canvas.
|
||||
namespace {
|
||||
const QColor yellow(255, 255, 0);
|
||||
|
||||
LayerEffects outline(double size)
|
||||
{
|
||||
LayerEffects effects;
|
||||
effects.stroke = StrokeEffect{.size = size, .red = 1, .green = 1, .blue = 0, .opacity = 1};
|
||||
return effects;
|
||||
}
|
||||
|
||||
// White document, a square 25..35 by 15..25, at 100%.
|
||||
struct Scene {
|
||||
EditorSession session;
|
||||
CanvasView canvas{session};
|
||||
QUuid square;
|
||||
explicit Scene(QRgb colour = qRgba(0, 0, 255, 255))
|
||||
{
|
||||
session.createDocument(60, 40);
|
||||
session.setShowsTransformControls(false);
|
||||
canvas.resize(300, 200);
|
||||
session.viewport.resize(QSizeF(300, 200), 1, QSizeF(60, 40));
|
||||
session.zoom(1);
|
||||
const QImage white = solid(60, 40, qRgba(255, 255, 255, 255));
|
||||
session.insert(ImportedImage(white, white, QStringLiteral("White")));
|
||||
const QImage pixels = solid(10, 10, colour);
|
||||
session.insert(ImportedImage(pixels, pixels, QStringLiteral("Square")), QPointF(30, 20));
|
||||
square = session.activeLayerID().value();
|
||||
}
|
||||
QImage shot() { return canvas.grab().toImage().convertToFormat(QImage::Format_ARGB32); }
|
||||
// The colour shown at a document pixel's centre.
|
||||
QColor at(QPoint pixel)
|
||||
{
|
||||
return shot().pixelColor(shown(QPointF(pixel) + QPointF(0.5, 0.5)));
|
||||
}
|
||||
// The view pixel holding a document point.
|
||||
QPoint shown(QPointF point)
|
||||
{
|
||||
const QPointF view = session.viewport.viewPoint(point, QSizeF(60, 40));
|
||||
return QPoint(int(std::floor(view.x())), int(std::floor(view.y())));
|
||||
}
|
||||
// Drawn once, then the worker's preview awaited.
|
||||
EffectsPreviewCache::Result landed()
|
||||
{
|
||||
shot();
|
||||
if (!QTest::qWaitFor([&] { return session.effectsPreviews.rendered(square).has_value(); }, 5000))
|
||||
throw std::runtime_error("no preview landed");
|
||||
return session.effectsPreviews.rendered(square).value();
|
||||
}
|
||||
// The worst channel gap to the export, pixel by pixel.
|
||||
int gapToExport()
|
||||
{
|
||||
const QImage exported = ImageExporter::render(session.projectSnapshot().value()).image.convertToFormat(QImage::Format_ARGB32);
|
||||
const QImage image = shot();
|
||||
int worst = 0;
|
||||
for (int y = 0; y < 40; ++y) {
|
||||
for (int x = 0; x < 60; ++x) {
|
||||
const QRgb expected = exported.pixel(x, y);
|
||||
const QRgb actual = image.pixel(shown(QPointF(x + 0.5, y + 0.5)));
|
||||
worst = std::max({worst, std::abs(qRed(expected) - qRed(actual)), std::abs(qGreen(expected) - qGreen(actual)),
|
||||
std::abs(qBlue(expected) - qBlue(actual))});
|
||||
}
|
||||
}
|
||||
return worst;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -2,76 +2,12 @@
|
||||
#include "IO/ImageExporter.h"
|
||||
#include "Rendering/EditorCanvas.h"
|
||||
#include "RenderFixtures.h"
|
||||
#include "EffectsCanvasFixtures.h"
|
||||
#include "SessionFixtures.h"
|
||||
#include <QtTest>
|
||||
|
||||
// Swift's effects on the canvas: previews, the paint's surface, distortions.
|
||||
namespace {
|
||||
const QColor yellow(255, 255, 0);
|
||||
|
||||
LayerEffects outline(double size)
|
||||
{
|
||||
LayerEffects effects;
|
||||
effects.stroke = StrokeEffect{.size = size, .red = 1, .green = 1, .blue = 0, .opacity = 1};
|
||||
return effects;
|
||||
}
|
||||
|
||||
// White document, a square 25..35 by 15..25, at 100%.
|
||||
struct Scene {
|
||||
EditorSession session;
|
||||
CanvasView canvas{session};
|
||||
QUuid square;
|
||||
explicit Scene(QRgb colour = qRgba(0, 0, 255, 255))
|
||||
{
|
||||
session.createDocument(60, 40);
|
||||
session.setShowsTransformControls(false);
|
||||
canvas.resize(300, 200);
|
||||
session.viewport.resize(QSizeF(300, 200), 1, QSizeF(60, 40));
|
||||
session.zoom(1);
|
||||
const QImage white = solid(60, 40, qRgba(255, 255, 255, 255));
|
||||
session.insert(ImportedImage(white, white, QStringLiteral("White")));
|
||||
const QImage pixels = solid(10, 10, colour);
|
||||
session.insert(ImportedImage(pixels, pixels, QStringLiteral("Square")), QPointF(30, 20));
|
||||
square = session.activeLayerID().value();
|
||||
}
|
||||
QImage shot() { return canvas.grab().toImage().convertToFormat(QImage::Format_ARGB32); }
|
||||
// The colour shown at a document pixel's centre.
|
||||
QColor at(QPoint pixel)
|
||||
{
|
||||
return shot().pixelColor(shown(QPointF(pixel) + QPointF(0.5, 0.5)));
|
||||
}
|
||||
// The view pixel holding a document point.
|
||||
QPoint shown(QPointF point)
|
||||
{
|
||||
const QPointF view = session.viewport.viewPoint(point, QSizeF(60, 40));
|
||||
return QPoint(int(std::floor(view.x())), int(std::floor(view.y())));
|
||||
}
|
||||
// Drawn once, then the worker's preview awaited.
|
||||
EffectsPreviewCache::Result landed()
|
||||
{
|
||||
shot();
|
||||
if (!QTest::qWaitFor([&] { return session.effectsPreviews.rendered(square).has_value(); }, 5000))
|
||||
throw std::runtime_error("no preview landed");
|
||||
return session.effectsPreviews.rendered(square).value();
|
||||
}
|
||||
// The worst channel gap to the export, pixel by pixel.
|
||||
int gapToExport()
|
||||
{
|
||||
const QImage exported = ImageExporter::render(session.projectSnapshot().value()).image.convertToFormat(QImage::Format_ARGB32);
|
||||
const QImage image = shot();
|
||||
int worst = 0;
|
||||
for (int y = 0; y < 40; ++y) {
|
||||
for (int x = 0; x < 60; ++x) {
|
||||
const QRgb expected = exported.pixel(x, y);
|
||||
const QRgb actual = image.pixel(shown(QPointF(x + 0.5, y + 0.5)));
|
||||
worst = std::max({worst, std::abs(qRed(expected) - qRed(actual)), std::abs(qGreen(expected) - qGreen(actual)),
|
||||
std::abs(qBlue(expected) - qBlue(actual))});
|
||||
}
|
||||
}
|
||||
return worst;
|
||||
}
|
||||
};
|
||||
|
||||
// Counts the paints a widget receives.
|
||||
struct Painted : QObject {
|
||||
int &count;
|
||||
|
||||
@@ -68,6 +68,8 @@ private slots:
|
||||
void aMaskHidesPixelsInTheReducedPreview();
|
||||
void aReducedPreviewScalesEveryLength();
|
||||
void aClosedCacheLandsNothing();
|
||||
void undoneStepsReuseTheirEffects();
|
||||
void renderNowRendersAtThePreviewSize();
|
||||
};
|
||||
|
||||
void EffectsPreviewCacheTests::aPreviewLandsAfterItsDelayAsTheRendererDrawsIt()
|
||||
@@ -150,17 +152,82 @@ void EffectsPreviewCacheTests::settingsKeepTheLastPreviewButNewPixelsDropIt()
|
||||
layer.effects->colorOverlay = ColorOverlayEffect();
|
||||
QCOMPARE(ask(cache, layer, landings).value().image.cacheKey(), second.image.cacheKey());
|
||||
landed(cache, layer, landings, 3);
|
||||
// New pixels, or a mask switched on, drop it too.
|
||||
// New pixels drop it.
|
||||
layer.asset = ImportedImage(filled(20, 10, QColor(255, 0, 0)), QImage(), QStringLiteral("Red"));
|
||||
QVERIFY(!ask(cache, layer, landings));
|
||||
landed(cache, layer, landings, 4);
|
||||
const qint64 red = landed(cache, layer, landings, 4).image.cacheKey();
|
||||
// A mask added or removed on those pixels keeps it.
|
||||
layer.mask = LayerMask(LayerMask::assetFrom(gray(20, 10, 255)));
|
||||
QVERIFY(!ask(cache, layer, landings, layer.mask->enabledImage()));
|
||||
landed(cache, layer, landings, 5);
|
||||
// A mask switched off is no source.
|
||||
QCOMPARE(ask(cache, layer, landings, layer.mask->enabledImage()).value().image.cacheKey(), red);
|
||||
QVERIFY(landed(cache, layer, landings, 5).image.cacheKey() != red);
|
||||
// Switched off again, the unmasked effects are known: no render.
|
||||
layer.mask->isEnabled = false;
|
||||
QCOMPARE(ask(cache, layer, landings).value().image.cacheKey(), red);
|
||||
QTest::qWait(200);
|
||||
QCOMPARE(*landings.count, 5);
|
||||
}
|
||||
|
||||
void EffectsPreviewCacheTests::undoneStepsReuseTheirEffects()
|
||||
{
|
||||
EffectsPreviewCache cache;
|
||||
ImageLayer layer = layerOf(filled(20, 10, QColor(0, 0, 255)), outline(3));
|
||||
const Landings landings;
|
||||
// Four pixels in turn, each rendered and landed.
|
||||
std::vector<ImportedImage> images;
|
||||
std::vector<qint64> keys;
|
||||
for (int step = 0; step < 4; ++step) {
|
||||
images.emplace_back(filled(20, 10, QColor(0, 0, 60 + 60 * step)), QImage(), QStringLiteral("Step"));
|
||||
layer.asset = images.back();
|
||||
QVERIFY(!ask(cache, layer, landings));
|
||||
keys.push_back(landed(cache, layer, landings, step + 1).image.cacheKey());
|
||||
}
|
||||
// Undone and redone across draws: the three newest are known.
|
||||
for (const int step : {2, 1, 3}) {
|
||||
cache.prepare({layer});
|
||||
layer.asset = images[size_t(step)];
|
||||
QCOMPARE(ask(cache, layer, landings).value().image.cacheKey(), keys[size_t(step)]);
|
||||
}
|
||||
QTest::qWait(200);
|
||||
QCOMPARE(*landings.count, 4);
|
||||
// Undone and redone across draws: three newest are known.
|
||||
cache.seed(layer.id, filled(4, 4, QColor(0, 255, 0)), layer.transform);
|
||||
layer.asset = images[2];
|
||||
QCOMPARE(ask(cache, layer, landings).value().image.cacheKey(), keys[2]);
|
||||
layer.asset = images[0];
|
||||
QVERIFY(!ask(cache, layer, landings));
|
||||
landed(cache, layer, landings, 6);
|
||||
// A layer without effects forgets them.
|
||||
cache.prepare({});
|
||||
layer.asset = images[3];
|
||||
QVERIFY(!ask(cache, layer, landings));
|
||||
}
|
||||
|
||||
void EffectsPreviewCacheTests::renderNowRendersAtThePreviewSize()
|
||||
{
|
||||
EffectsPreviewCache cache;
|
||||
const QImage large = filled(400, 200, QColor(0, 0, 255));
|
||||
const LayerEffectsRenderer::Rendered whole = LayerEffectsRenderer::render(large, std::nullopt, outline(3));
|
||||
// Within the default 1536 a side: the whole render.
|
||||
const EffectsPreviewCache::Result now = cache.renderNow(large, std::nullopt, outline(3)).value();
|
||||
QCOMPARE(now.image, whole.image);
|
||||
QCOMPARE(now.inset, whole.inset);
|
||||
// Sharing the budget with a thousand, as the worker reduces.
|
||||
const ImageLayer layer = layerOf(large, outline(3));
|
||||
std::vector<ImageLayer> many(999, layerOf(filled(2, 2, QColor(0, 0, 255)), outline(3)));
|
||||
for (ImageLayer &each : many)
|
||||
each.id = QUuid::createUuid();
|
||||
many.push_back(layer);
|
||||
cache.prepare(many);
|
||||
const Landings landings;
|
||||
ask(cache, layer, landings);
|
||||
const EffectsPreviewCache::Result worker = landed(cache, layer, landings, 1);
|
||||
const EffectsPreviewCache::Result reduced = cache.renderNow(large, std::nullopt, outline(3)).value();
|
||||
QCOMPARE(reduced.image, worker.image);
|
||||
QCOMPARE(reduced.inset, worker.inset);
|
||||
// 129 a side: sides at 121 over the margined width.
|
||||
const double factor = 121.0 / (400 + 2 * LayerEffectsRenderer::margin(outline(3)));
|
||||
const LayerEffects scaledOutline = outline(3 * factor);
|
||||
QCOMPARE(reduced.image.size(), LayerEffectsRenderer::render(filled(int(std::lround(400 * factor)), int(std::lround(200 * factor)), QColor(0, 0, 255)),
|
||||
std::nullopt, scaledOutline).image.size());
|
||||
}
|
||||
|
||||
void EffectsPreviewCacheTests::onlyTheNewestRequestLands()
|
||||
|
||||
@@ -41,6 +41,14 @@ inline bool reddish(QColor colour)
|
||||
return colour.red() > 128 && colour.green() < 90 && colour.blue() < 90;
|
||||
}
|
||||
|
||||
// The selection's highlight at 45% over what shows beneath.
|
||||
inline bool highlightedOver(QColor under, QColor over, QColor highlight)
|
||||
{
|
||||
const auto mixed = [](int beneath, int ink, int shown) { return std::abs(shown - (beneath * 0.55 + ink * 0.45)) <= 2; };
|
||||
return under != over && mixed(under.red(), highlight.red(), over.red()) && mixed(under.green(), highlight.green(), over.green())
|
||||
&& mixed(under.blue(), highlight.blue(), over.blue());
|
||||
}
|
||||
|
||||
inline bool greenish(QColor colour)
|
||||
{
|
||||
return colour.green() > 128 && colour.red() < 90 && colour.blue() < 90;
|
||||
|
||||
@@ -58,12 +58,15 @@ void InlineTextDrawTests::theFrameAndHandlesSitOnTheBox()
|
||||
QVERIFY(image.pixelColor(shown.onScreen(QPointF(size.width() / 4, 4))) != frame);
|
||||
// No plus without overflow.
|
||||
QVERIFY(image.pixelColor(shown.onScreen(QPointF(size.width(), size.height()))) == QColor(Qt::white));
|
||||
// Text draws over the frame, a subview over its view.
|
||||
// Tight leading lifts the H into the frame, still over.
|
||||
shown.session.changeTextStyle([](LayerTextStyle &style) {
|
||||
style.content = QStringLiteral("H");
|
||||
style.leading = 30;
|
||||
});
|
||||
QVERIFY(reddish(shown.grab().pixelColor(shown.onScreen(QPointF(24, 0.25)))));
|
||||
QCOMPARE(qAlpha(EditorSession::textImage(shown.session.textDraft().value().style).pixel(24, 0)), 255);
|
||||
const QColor over = shown.grab().pixelColor(shown.onScreen(QPointF(24, 0.25)));
|
||||
QVERIFY2(!reddish(over) && std::abs(over.red() - accent.red()) < 40 && std::abs(over.blue() - accent.blue()) < 40, qPrintable(over.name()));
|
||||
QVERIFY(where(shown.grab(), reddish).isValid());
|
||||
}
|
||||
|
||||
void InlineTextDrawTests::aTurnedFlippedLayerIsEditedInPlace()
|
||||
@@ -151,17 +154,18 @@ void InlineTextDrawTests::aSelectionShowsBehindItsText()
|
||||
QTRY_VERIFY(shown.canvas->hasFocus());
|
||||
beginTextAt(shown.session, QPointF(20, 30));
|
||||
QTest::keyClicks(shown.canvas, QStringLiteral("iiii"));
|
||||
const QImage plain = shown.grab();
|
||||
QTest::keyClick(shown.canvas, Qt::Key_A, Qt::ControlModifier);
|
||||
const auto mixed = [&](QPoint at, QColor highlight) { return highlightedOver(plain.pixelColor(at), shown.grab().pixelColor(at), highlight); };
|
||||
const auto highlighted = [&](QPoint at) { return mixed(at, shown.canvas->palette().color(QPalette::Active, QPalette::Highlight)); };
|
||||
// Above the letters, the line's height is highlighted.
|
||||
const QPoint above = shown.editor().textTransform().map(QPointF(LayerTextStyle::padding + 3, LayerTextStyle::padding + 3)).toPoint();
|
||||
QCOMPARE(shown.grab().pixelColor(above), shown.canvas->palette().color(QPalette::Active, QPalette::Highlight));
|
||||
QVERIFY(highlighted(above));
|
||||
const double line = shown.session.textDraft().value().style.lineHeight();
|
||||
const QPoint below = shown.editor().textTransform().map(QPointF(LayerTextStyle::padding + 3, LayerTextStyle::padding + line - 3)).toPoint();
|
||||
QCOMPARE(shown.grab().pixelColor(below), shown.canvas->palette().color(QPalette::Active, QPalette::Highlight));
|
||||
QVERIFY(highlighted(shown.editor().textTransform().map(QPointF(LayerTextStyle::padding + 3, LayerTextStyle::padding + line - 3)).toPoint()));
|
||||
// To the last selected letter's edge.
|
||||
const double selected = QFontMetricsF(TextLayout::font(shown.session.textDraft().value().style)).horizontalAdvance(QStringLiteral("iiii"));
|
||||
const QPoint last = shown.editor().textTransform().map(QPointF(LayerTextStyle::padding + selected - 3, LayerTextStyle::padding + 3)).toPoint();
|
||||
QCOMPARE(shown.grab().pixelColor(last), shown.canvas->palette().color(QPalette::Active, QPalette::Highlight));
|
||||
QVERIFY(highlighted(shown.editor().textTransform().map(QPointF(LayerTextStyle::padding + selected - 3, LayerTextStyle::padding + 3)).toPoint()));
|
||||
// No caret while something is selected.
|
||||
const QPointF mapped = shown.editor().textTransform().map(
|
||||
QPointF(LayerTextStyle::padding + QFontMetricsF(TextLayout::font(shown.session.textDraft().value().style)).horizontalAdvance(QStringLiteral("iiii")) + 0.25,
|
||||
@@ -170,7 +174,7 @@ void InlineTextDrawTests::aSelectionShowsBehindItsText()
|
||||
QVERIFY(!reddish(shown.grab().pixelColor(end)));
|
||||
QTest::keyClick(shown.canvas, Qt::Key_End);
|
||||
QVERIFY(reddish(shown.grab().pixelColor(end)));
|
||||
QVERIFY(shown.grab().pixelColor(above) != shown.canvas->palette().color(QPalette::Active, QPalette::Highlight));
|
||||
QCOMPARE(shown.grab().pixelColor(above), plain.pixelColor(above));
|
||||
// Unfocused, a selection shows in the inactive colour.
|
||||
QPalette palette = shown.canvas->palette();
|
||||
palette.setColor(QPalette::Inactive, QPalette::Highlight, QColor(40, 200, 40));
|
||||
@@ -180,7 +184,7 @@ void InlineTextDrawTests::aSelectionShowsBehindItsText()
|
||||
field->show();
|
||||
field->setFocus();
|
||||
QTRY_VERIFY(!shown.canvas->hasFocus());
|
||||
QCOMPARE(shown.grab().pixelColor(above), QColor(40, 200, 40));
|
||||
QVERIFY(mixed(above, QColor(40, 200, 40)));
|
||||
}
|
||||
|
||||
void InlineTextDrawTests::textPastTheBoxShowsAPlus()
|
||||
|
||||
@@ -22,6 +22,18 @@ void InlineTextPreeditTests::markedTextIsUnderlined()
|
||||
const QRect band = shown.editor().textTransform().mapRect(
|
||||
QRectF(LayerTextStyle::padding, baseline + 1, metrics.horizontalAdvance(QStringLiteral("nn")) - 4, metrics.descent() - 2)).toAlignedRect();
|
||||
QVERIFY(where(shown.grab().copy(band), reddish).isValid());
|
||||
// The input method's own underline, uncoloured, takes the text's.
|
||||
QTextCharFormat underlined;
|
||||
underlined.setFontUnderline(true);
|
||||
QInputMethodEvent styled(QStringLiteral("nn"), {QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat, 0, 2, underlined)});
|
||||
QApplication::sendEvent(shown.canvas, &styled);
|
||||
QVERIFY(where(shown.grab().copy(band), reddish).isValid());
|
||||
// Its own colour stands.
|
||||
underlined.setUnderlineColor(Qt::blue);
|
||||
QInputMethodEvent blue(QStringLiteral("nn"), {QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat, 0, 2, underlined)});
|
||||
QApplication::sendEvent(shown.canvas, &blue);
|
||||
QVERIFY(!where(shown.grab().copy(band), reddish).isValid());
|
||||
QVERIFY(where(shown.grab().copy(band), [](QColor colour) { return colour.blue() > 128 && colour.red() < 90 && colour.green() < 90; }).isValid());
|
||||
// A format that is none keeps the underline, as Qt's.
|
||||
QInputMethodEvent formless(QStringLiteral("nn"), {QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat, 0, 2, QVariant())});
|
||||
QApplication::sendEvent(shown.canvas, &formless);
|
||||
@@ -87,12 +99,13 @@ void InlineTextPreeditTests::theInputMethodStylesItsPreedit()
|
||||
shown.session.cancelText();
|
||||
beginTextAt(shown.session, QPointF(20, 30));
|
||||
QTest::keyClicks(shown.canvas, QStringLiteral("xx"));
|
||||
const QImage plain = shown.grab();
|
||||
QInputMethodEvent selecting(QStringLiteral("nn"), {QInputMethodEvent::Attribute(QInputMethodEvent::Selection, 0, 2, QVariant())});
|
||||
QApplication::sendEvent(shown.canvas, &selecting);
|
||||
QCOMPARE(shown.session.textDraft().value().style.content, QString("xxnn"));
|
||||
const QPalette::ColorGroup group = shown.canvas->hasFocus() ? QPalette::Active : QPalette::Inactive;
|
||||
const QPoint first = shown.editor().textTransform().map(QPointF(padding + 3, padding + 3)).toPoint();
|
||||
QCOMPARE(shown.grab().pixelColor(first), shown.canvas->palette().color(group, QPalette::Highlight));
|
||||
QVERIFY(highlightedOver(plain.pixelColor(first), shown.grab().pixelColor(first), shown.canvas->palette().color(group, QPalette::Highlight)));
|
||||
// Past the preedit, it sits after the preedit's letters.
|
||||
shown.session.cancelText();
|
||||
beginTextAt(shown.session, QPointF(20, 30));
|
||||
@@ -102,8 +115,8 @@ void InlineTextPreeditTests::theInputMethodStylesItsPreedit()
|
||||
QCOMPARE(shown.session.textDraft().value().style.content, QString("nnxx"));
|
||||
const double nn = metrics.horizontalAdvance(QStringLiteral("nn"));
|
||||
const QPoint x = shown.editor().textTransform().map(QPointF(padding + nn + 3, padding + 3)).toPoint();
|
||||
QCOMPARE(shown.grab().pixelColor(x), shown.canvas->palette().color(group, QPalette::Highlight));
|
||||
QVERIFY(shown.grab().pixelColor(first) != shown.canvas->palette().color(group, QPalette::Highlight));
|
||||
QVERIFY(highlightedOver(plain.pixelColor(x), shown.grab().pixelColor(x), shown.canvas->palette().color(group, QPalette::Highlight)));
|
||||
QCOMPARE(shown.grab().pixelColor(first), plain.pixelColor(first));
|
||||
}
|
||||
|
||||
QTEST_MAIN(InlineTextPreeditTests)
|
||||
|
||||
@@ -96,6 +96,7 @@ private slots:
|
||||
void aSmallerMaskScalesSmoothlyAsTheRenderersDoes();
|
||||
void aSurfaceHoldsEightyMillionPixelsAtMost();
|
||||
void aSurfaceFitsTheStrokeItWasMadeFor();
|
||||
void aMaskStrokeRedoesWhereItsTilesMap();
|
||||
};
|
||||
|
||||
void LayerEffectsSurfaceTests::theFirstUpdateDrawsWhatTheRendererDraws()
|
||||
@@ -192,5 +193,35 @@ void LayerEffectsSurfaceTests::aSurfaceFitsTheStrokeItWasMadeFor()
|
||||
QVERIFY(!surface->matches(id, everyKind(), QSizeF(40, 30), QRectF(6, 5, 20, 10)));
|
||||
}
|
||||
|
||||
void LayerEffectsSurfaceTests::aMaskStrokeRedoesWhereItsTilesMap()
|
||||
{
|
||||
LayerEffects effects;
|
||||
effects.stroke = StrokeEffect{.size = 3, .red = 1, .green = 1, .blue = 0, .opacity = 1};
|
||||
const ImportedImage base(solid(40, 40, qRgba(0, 0, 255, 255)), QImage(), QStringLiteral("Blue"));
|
||||
// The mask as the stroke leaves it, over a region.
|
||||
auto mask = std::make_shared<QImage>(BrushRaster::context(40, 40, true));
|
||||
mask->fill(255);
|
||||
const auto coverage = [mask](const QRectF ®ion) {
|
||||
QImage live = BrushRaster::context(int(region.width()), int(region.height()), true);
|
||||
QPainter painter(&live);
|
||||
BrushRaster::draw(*mask, QRectF(-region.topLeft(), QSizeF(40, 40)), painter);
|
||||
return live;
|
||||
};
|
||||
// A finer grid's tile, landing at 10 to 12.56.
|
||||
const QTransform toGrid = QTransform::fromScale(0.01, 0.01);
|
||||
const QRectF tile(1000, 1000, 256, 256);
|
||||
const std::unique_ptr<LayerEffectsSurface> surface = LayerEffectsSurface::make(QUuid::createUuid(), effects, QSizeF(40, 40), QRectF(0, 0, 40, 40));
|
||||
surface->update(base, {}, std::nullopt, LayerEffectsSurface::MaskStroke{{BrushPatch{tile, solid(4, 4, 0)}}, toGrid, coverage});
|
||||
// A hole under the tile, its pixels anew: redone there.
|
||||
QPainter hole(mask.get());
|
||||
hole.fillRect(QRectF(10, 10, 3, 3), Qt::black);
|
||||
hole.end();
|
||||
surface->update(base, {}, std::nullopt, LayerEffectsSurface::MaskStroke{{BrushPatch{tile, solid(4, 4, 0)}}, toGrid, coverage});
|
||||
const std::unique_ptr<LayerEffectsSurface> fresh = LayerEffectsSurface::make(QUuid::createUuid(), effects, QSizeF(40, 40), QRectF(0, 0, 40, 40));
|
||||
fresh->update(base, {}, std::nullopt, LayerEffectsSurface::MaskStroke{{}, toGrid, coverage});
|
||||
QCOMPARE(surface->image().value(), fresh->image().value());
|
||||
QVERIFY(surface->image().value() != LayerEffectsRenderer::render(base.image(), std::nullopt, effects).image);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(LayerEffectsSurfaceTests)
|
||||
#include "LayerEffectsSurfaceTests.moc"
|
||||
@@ -0,0 +1,159 @@
|
||||
#include "EffectsCanvasFixtures.h"
|
||||
|
||||
// Swift 1.2.11 (b683acb): a painted mask keeps effects on.
|
||||
namespace {
|
||||
bool nearly(QColor colour, QColor expected)
|
||||
{
|
||||
return std::abs(colour.red() - expected.red()) <= 40 && std::abs(colour.green() - expected.green()) <= 40
|
||||
&& std::abs(colour.blue() - expected.blue()) <= 40;
|
||||
}
|
||||
|
||||
// Black down the mask, eight wide; a later pass cuts.
|
||||
void paintHole(Scene &scene)
|
||||
{
|
||||
EditorSession &session = scene.session;
|
||||
session.selectLayerTarget(session.activeLayerID().value(), true);
|
||||
session.selectTool(NavigationTool::brush);
|
||||
session.setMaskPaintWhite(false);
|
||||
BrushSettings settings = session.brushSettings();
|
||||
settings.diameter = 8;
|
||||
settings.hardness = 1;
|
||||
session.setBrushSettings(settings);
|
||||
session.beginBrush(QPointF(30, 2));
|
||||
session.continueBrush(QPointF(30, 6));
|
||||
scene.shot();
|
||||
session.continueBrush(QPointF(30, 35));
|
||||
}
|
||||
}
|
||||
|
||||
class MaskEffectsCanvasTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void aPaintedMaskRedoesTheEffects();
|
||||
void aPlacedMaskRedoesThemInTheLayersGrid();
|
||||
void untouchedTilesKeepTheOldMask();
|
||||
void aFinerPlacedMaskMapsIntoTheLayersGrid();
|
||||
};
|
||||
|
||||
void MaskEffectsCanvasTests::aPaintedMaskRedoesTheEffects()
|
||||
{
|
||||
Scene scene;
|
||||
scene.session.setEffects(outline(2));
|
||||
scene.session.addLayerMask(true);
|
||||
scene.landed();
|
||||
QVERIFY(nearly(scene.at(QPoint(27, 20)), QColor(0, 0, 255)));
|
||||
paintHole(scene);
|
||||
// Mid-stroke the outline follows the hole: its middle is clear.
|
||||
QVERIFY2(nearly(scene.at(QPoint(27, 20)), yellow), qPrintable(scene.at(QPoint(27, 20)).name()));
|
||||
QVERIFY2(nearly(scene.at(QPoint(33, 20)), yellow), qPrintable(scene.at(QPoint(33, 20)).name()));
|
||||
QCOMPARE(scene.at(QPoint(30, 20)), QColor(Qt::white));
|
||||
QVERIFY(nearly(scene.at(QPoint(25, 20)), QColor(0, 0, 255)));
|
||||
// Committed, the surface stands in until new effects land.
|
||||
QVERIFY(scene.session.finishBrushImmediately());
|
||||
scene.canvas.synchronizeDisplay();
|
||||
QVERIFY(scene.session.effectsPreviews.rendered(scene.square).value().placement);
|
||||
QVERIFY(nearly(scene.at(QPoint(27, 20)), yellow));
|
||||
QTRY_VERIFY(!scene.session.effectsPreviews.rendered(scene.square).value().placement);
|
||||
QVERIFY(nearly(scene.at(QPoint(27, 20)), yellow));
|
||||
}
|
||||
|
||||
void MaskEffectsCanvasTests::aPlacedMaskRedoesThemInTheLayersGrid()
|
||||
{
|
||||
Scene scene;
|
||||
scene.session.setEffects(outline(2));
|
||||
scene.session.addLayerMask(true);
|
||||
// A white mask in the layer's grid, placed on it.
|
||||
rewrite(scene.session, [&](ProjectSnapshot &snapshot) {
|
||||
ProjectLayerRecord &square = record(snapshot, scene.square);
|
||||
QImage white = BrushRaster::context(10, 10, true);
|
||||
white.fill(255);
|
||||
snapshot.masks.insert_or_assign(scene.square, ImportedImage(white, white, QStringLiteral("Mask")));
|
||||
square.maskPlacement = square.transform;
|
||||
square.maskLinked = false;
|
||||
});
|
||||
scene.session.selectLayer(scene.square);
|
||||
QVERIFY(layerWith(scene.session, scene.square).mask.value().placement);
|
||||
scene.landed();
|
||||
paintHole(scene);
|
||||
QVERIFY2(nearly(scene.at(QPoint(27, 20)), yellow), qPrintable(scene.at(QPoint(27, 20)).name()));
|
||||
QVERIFY2(nearly(scene.at(QPoint(33, 20)), yellow), qPrintable(scene.at(QPoint(33, 20)).name()));
|
||||
QCOMPARE(scene.at(QPoint(30, 20)), QColor(Qt::white));
|
||||
QVERIFY(scene.session.finishBrushImmediately());
|
||||
scene.canvas.synchronizeDisplay();
|
||||
QVERIFY(nearly(scene.at(QPoint(27, 20)), yellow));
|
||||
}
|
||||
|
||||
void MaskEffectsCanvasTests::untouchedTilesKeepTheOldMask()
|
||||
{
|
||||
// A layer wider than a tile, painted at one end.
|
||||
EditorSession session;
|
||||
CanvasView canvas{session};
|
||||
session.createDocument(700, 40);
|
||||
canvas.resize(700, 40);
|
||||
session.viewport.resize(QSizeF(700, 40), 1, QSizeF(700, 40));
|
||||
session.zoom(1);
|
||||
const QImage blue = solid(600, 10, qRgba(0, 0, 255, 255));
|
||||
session.insert(ImportedImage(blue, blue, QStringLiteral("Wide")), QPointF(350, 20));
|
||||
session.setEffects(outline(2));
|
||||
session.addLayerMask(true);
|
||||
canvas.grab();
|
||||
session.selectLayerTarget(session.activeLayerID().value(), true);
|
||||
session.selectTool(NavigationTool::brush);
|
||||
session.setMaskPaintWhite(false);
|
||||
session.beginBrush(QPointF(100, 20));
|
||||
const QImage shot = canvas.grab().toImage();
|
||||
const QPointF far = session.viewport.viewPoint(QPointF(600.5, 20.5), QSizeF(700, 40));
|
||||
QCOMPARE(shot.pixelColor(far.toPoint()), QColor(0, 0, 255));
|
||||
}
|
||||
|
||||
void MaskEffectsCanvasTests::aFinerPlacedMaskMapsIntoTheLayersGrid()
|
||||
{
|
||||
// The committed mask's landed effects are the oracle.
|
||||
Scene scene;
|
||||
scene.session.setEffects(outline(2));
|
||||
scene.session.addLayerMask(true);
|
||||
rewrite(scene.session, [&](ProjectSnapshot &snapshot) {
|
||||
ProjectLayerRecord &square = record(snapshot, scene.square);
|
||||
QImage white = BrushRaster::context(600, 600, true);
|
||||
white.fill(255);
|
||||
snapshot.masks.insert_or_assign(scene.square, ImportedImage(white, white, QStringLiteral("Mask")));
|
||||
square.maskPlacement = square.transform;
|
||||
square.maskLinked = false;
|
||||
});
|
||||
scene.session.selectLayer(scene.square);
|
||||
scene.landed();
|
||||
scene.session.selectLayerTarget(scene.square, true);
|
||||
scene.session.selectTool(NavigationTool::brush);
|
||||
scene.session.setMaskPaintWhite(false);
|
||||
BrushSettings settings = scene.session.brushSettings();
|
||||
settings.diameter = 1;
|
||||
settings.hardness = 1;
|
||||
scene.session.setBrushSettings(settings);
|
||||
scene.session.beginBrush(QPointF(32, 16));
|
||||
scene.shot();
|
||||
scene.session.continueBrush(QPointF(32, 24));
|
||||
const QImage painting = scene.shot();
|
||||
// The oracle: a whole render through the stroke's mask.
|
||||
const ImageLayer square = layerWith(scene.session, scene.square);
|
||||
const QImage preview = scene.session.brushStroke()->placedMaskPreview(square.mask.value().placement.value()).value();
|
||||
const LayerEffectsRenderer::Rendered expected = LayerEffectsRenderer::render(square.asset.value().image(), preview, outline(2));
|
||||
int largest = 0;
|
||||
for (int y = 10; y < 30; ++y) {
|
||||
for (int x = 20; x < 40; ++x) {
|
||||
const QPoint at(x - 25 + int(expected.inset), y - 15 + int(expected.inset));
|
||||
if (!expected.image.rect().contains(at))
|
||||
continue;
|
||||
const QColor over = expected.image.pixelColor(at);
|
||||
const QColor shown = painting.pixelColor(scene.shown(QPointF(x + 0.5, y + 0.5)));
|
||||
// Over white, premultiplied: the channel plus what shows through.
|
||||
const auto on = [&](int channel) { return channel + 255 - over.alpha(); };
|
||||
largest = std::max({largest, std::abs(shown.red() - on(qRound(over.redF() * over.alphaF() * 255))),
|
||||
std::abs(shown.green() - on(qRound(over.greenF() * over.alphaF() * 255))),
|
||||
std::abs(shown.blue() - on(qRound(over.blueF() * over.alphaF() * 255)))});
|
||||
}
|
||||
}
|
||||
QVERIFY2(largest <= 2, qPrintable(QString::number(largest)));
|
||||
}
|
||||
|
||||
QTEST_MAIN(MaskEffectsCanvasTests)
|
||||
#include "MaskEffectsCanvasTests.moc"
|
||||
@@ -49,6 +49,7 @@ compositor_test(ShapeToolTests)
|
||||
compositor_test(ShapeLayerTests)
|
||||
compositor_test(ShapeCanvasTests)
|
||||
compositor_test(TypeToolTests)
|
||||
compositor_test(TypedTextCanvasTests)
|
||||
compositor_test(LiveTextDoubleClickTests)
|
||||
compositor_test(SVGImportTests)
|
||||
compositor_test(TextLayoutTests)
|
||||
@@ -198,6 +199,7 @@ compositor_test(LayerEffectsKernelTests)
|
||||
compositor_test(EffectsPreviewCacheTests)
|
||||
compositor_test(LayerEffectsSurfaceTests)
|
||||
compositor_test(EffectsCanvasTests)
|
||||
compositor_test(MaskEffectsCanvasTests)
|
||||
compositor_test(EffectsFailureTests)
|
||||
compositor_test(EffectsPanelTests)
|
||||
compositor_test(LayerEffectRowTests)
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
#include "InlineTextDrawFixtures.h"
|
||||
#include "Document/LayerEffects+Renderer.h"
|
||||
|
||||
// Swift 1.2.11: typed text drawn as its pixels.
|
||||
namespace {
|
||||
// Only the box's inside, where frame and handles never draw.
|
||||
int largestChange(const QImage &before, const QImage &after, QRect inside)
|
||||
{
|
||||
int largest = 0;
|
||||
inside &= before.rect();
|
||||
for (int y = inside.top(); y <= inside.bottom(); ++y) {
|
||||
for (int x = inside.left(); x <= inside.right(); ++x) {
|
||||
const QRgb a = before.pixel(x, y), b = after.pixel(x, y);
|
||||
largest = std::max({largest, std::abs(qRed(a) - qRed(b)), std::abs(qGreen(a) - qGreen(b)), std::abs(qBlue(a) - qBlue(b))});
|
||||
}
|
||||
}
|
||||
return largest;
|
||||
}
|
||||
|
||||
LayerEffects yellowStroke()
|
||||
{
|
||||
LayerEffects effects;
|
||||
effects.stroke = StrokeEffect{.size = 3, .red = 1, .green = 1, .blue = 0, .opacity = 1};
|
||||
return effects;
|
||||
}
|
||||
|
||||
// The caret draws in the text's colour: keys go elsewhere.
|
||||
void unfocus(DrawnText &shown)
|
||||
{
|
||||
QCoreApplication::processEvents();
|
||||
shown.canvas->clearFocus();
|
||||
if (!QTest::qWaitFor([&] { return !shown.canvas->hasFocus(); }))
|
||||
throw std::runtime_error("the canvas kept the keys");
|
||||
}
|
||||
|
||||
// Debug lines on the rendering category, counted.
|
||||
int remakes = 0;
|
||||
QtMessageHandler passOn = nullptr;
|
||||
void countRemakes(QtMsgType type, const QMessageLogContext &context, const QString &message)
|
||||
{
|
||||
if (message == QLatin1String("typed text remade"))
|
||||
++remakes;
|
||||
passOn(type, context, message);
|
||||
}
|
||||
|
||||
// The union of the regions a widget paints.
|
||||
struct PaintedRegion : QObject {
|
||||
QRegion region;
|
||||
bool eventFilter(QObject *, QEvent *event) override
|
||||
{
|
||||
if (event->type() == QEvent::Paint)
|
||||
region |= static_cast<QPaintEvent *>(event)->region();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
bool yellowish(QColor colour)
|
||||
{
|
||||
return colour.red() > 200 && colour.green() > 200 && colour.blue() < 60;
|
||||
}
|
||||
}
|
||||
|
||||
class TypedTextCanvasTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void textLooksTheSameWhileEditingAndOnceCommitted_data();
|
||||
void textLooksTheSameWhileEditingAndOnceCommitted();
|
||||
void newTextSitsAboveTheActiveLayer();
|
||||
void editedTextKeepsItsOpacityAndEffects();
|
||||
void typedTextFollowsEachChange();
|
||||
void aMaskHidesTypedTextsEffects();
|
||||
void cancelledTextHandsOnNothing();
|
||||
void typingRepaintsTheWholeView();
|
||||
void aHandleThatOnlyMovesRepaintsTheWholeView();
|
||||
void typedTextBlendsAsItsLayer();
|
||||
void typedTextIsRemadeOnlyWhenItChanges();
|
||||
void aMovedBoxRedoesItsTypedEffects();
|
||||
};
|
||||
|
||||
void TypedTextCanvasTests::textLooksTheSameWhileEditingAndOnceCommitted_data()
|
||||
{
|
||||
QTest::addColumn<double>("zoom");
|
||||
QTest::newRow("smooth") << 1.5;
|
||||
QTest::newRow("hard") << 4.0;
|
||||
}
|
||||
|
||||
void TypedTextCanvasTests::textLooksTheSameWhileEditingAndOnceCommitted()
|
||||
{
|
||||
QFETCH(double, zoom);
|
||||
DrawnText shown;
|
||||
shown.session.zoom(zoom);
|
||||
shown.canvas->synchronizeDisplay();
|
||||
const QImage blank = shown.grab();
|
||||
// The box's middle near the view's; it grows by four.
|
||||
shown.session.beginText(QPointF(163, 205), true);
|
||||
shown.session.changeTextStyle([](LayerTextStyle &style) {
|
||||
style.content = QStringLiteral("Sharp");
|
||||
style.fontSize = 24;
|
||||
});
|
||||
// Unfocused, no caret draws; the frame stays at the edge.
|
||||
unfocus(shown);
|
||||
const QRect inside = shown.editor().boxTransform().mapRect(QRectF(QPointF(0, 0), shown.editor().logicalSize())).toAlignedRect().adjusted(10, 10, -10, -10);
|
||||
const QImage editing = shown.grab();
|
||||
QVERIFY(largestChange(blank, editing, inside) > 100);
|
||||
QVERIFY(shown.session.finishText());
|
||||
QVERIFY(shown.session.activeLayer().value().liveText());
|
||||
const QImage committed = shown.grab();
|
||||
QVERIFY2(largestChange(editing, committed, inside) <= 2, qPrintable(QString::number(largestChange(editing, committed, inside))));
|
||||
}
|
||||
|
||||
void TypedTextCanvasTests::newTextSitsAboveTheActiveLayer()
|
||||
{
|
||||
DrawnText shown;
|
||||
// A drawn active layer, a green dot, under a cover.
|
||||
QImage green = BrushRaster::context(2, 2, false);
|
||||
green.fill(Qt::green);
|
||||
shown.session.addPixelLayer(green, QPointF(300, 250), QStringLiteral("Dot"), QStringLiteral("Add"));
|
||||
const QUuid below = shown.session.activeLayerID().value();
|
||||
QImage white = BrushRaster::context(400, 300, false);
|
||||
white.fill(Qt::white);
|
||||
shown.session.addPixelLayer(white, QPointF(0, 0), QStringLiteral("Cover"), QStringLiteral("Add"));
|
||||
const QUuid cover = shown.session.activeLayerID().value();
|
||||
shown.session.selectLayer(below);
|
||||
shown.session.selectTool(NavigationTool::type);
|
||||
beginTextAt(shown.session, QPointF(40, 40));
|
||||
shown.session.changeTextStyle([](LayerTextStyle &style) { style.content = QStringLiteral("Hidden"); });
|
||||
unfocus(shown);
|
||||
QVERIFY(!where(shown.grab(), reddish).isValid());
|
||||
shown.session.cancelText();
|
||||
// Above the cover it shows, as its layer will.
|
||||
shown.session.selectLayer(cover);
|
||||
beginTextAt(shown.session, QPointF(40, 40));
|
||||
shown.session.changeTextStyle([](LayerTextStyle &style) { style.content = QStringLiteral("Shown"); });
|
||||
unfocus(shown);
|
||||
QVERIFY(where(shown.grab(), reddish).isValid());
|
||||
shown.session.cancelText();
|
||||
// A hidden active layer draws nothing: text goes on top.
|
||||
shown.session.toggleLayerVisibility(below);
|
||||
shown.session.selectLayer(below);
|
||||
beginTextAt(shown.session, QPointF(40, 40));
|
||||
shown.session.changeTextStyle([](LayerTextStyle &style) { style.content = QStringLiteral("Top"); });
|
||||
unfocus(shown);
|
||||
QVERIFY(where(shown.grab(), reddish).isValid());
|
||||
}
|
||||
|
||||
void TypedTextCanvasTests::editedTextKeepsItsOpacityAndEffects()
|
||||
{
|
||||
DrawnText shown;
|
||||
const QUuid id = shown.text(QPointF(40, 40), QStringLiteral("HH"));
|
||||
shown.session.beginOpacityEdit();
|
||||
shown.session.setLayerOpacity(0.5);
|
||||
shown.session.finishOpacityEdit();
|
||||
shown.session.editActiveText();
|
||||
unfocus(shown);
|
||||
// Half opacity: half red over the checkerboard, no full red.
|
||||
const QRect ink = where(shown.grab(), [](QColor colour) { return colour.red() > 150 && colour.red() < 190 && colour.green() < 60; });
|
||||
QVERIFY(ink.isValid());
|
||||
QVERIFY(!where(shown.grab(), [](QColor colour) { return colour.red() > 240 && colour.green() < 60; }).isValid());
|
||||
shown.session.cancelText();
|
||||
shown.session.beginOpacityEdit();
|
||||
shown.session.setLayerOpacity(1);
|
||||
shown.session.finishOpacityEdit();
|
||||
// Effects stay on while typing, redone at once.
|
||||
shown.session.setEffects(yellowStroke());
|
||||
shown.session.editActiveText();
|
||||
shown.session.changeTextStyle([](LayerTextStyle &style) { style.content = QStringLiteral("HHH"); });
|
||||
QVERIFY(where(shown.grab(), yellowish).isValid());
|
||||
// Committed, the typed effects stand in: nothing blinks.
|
||||
QVERIFY(shown.session.finishText());
|
||||
shown.grab();
|
||||
const EffectsPreviewCache::Result seeded = shown.session.effectsPreviews.rendered(id).value();
|
||||
QVERIFY(seeded.placement);
|
||||
QVERIFY(where(shown.grab(), yellowish).isValid());
|
||||
// Handed on once: drawing again seeds nothing more.
|
||||
QTRY_VERIFY((shown.grab(), !shown.session.effectsPreviews.rendered(id).value().placement));
|
||||
// Cancelled, typed effects the layer never took hand on nothing.
|
||||
shown.session.editActiveText();
|
||||
shown.session.changeTextStyle([](LayerTextStyle &style) { style.content = QStringLiteral("HHHH"); });
|
||||
shown.grab();
|
||||
shown.session.cancelText();
|
||||
shown.grab();
|
||||
QVERIFY(!shown.session.effectsPreviews.rendered(id).value().placement);
|
||||
}
|
||||
|
||||
void TypedTextCanvasTests::typedTextFollowsEachChange()
|
||||
{
|
||||
DrawnText shown;
|
||||
shown.text(QPointF(40, 40), QStringLiteral("H"));
|
||||
shown.session.setEffects(yellowStroke());
|
||||
shown.session.editActiveText();
|
||||
unfocus(shown);
|
||||
const auto count = [&](bool (*test)(QColor)) {
|
||||
const QImage image = shown.grab();
|
||||
int found = 0;
|
||||
for (int y = 0; y < image.height(); ++y)
|
||||
for (int x = 0; x < image.width(); ++x)
|
||||
found += test(image.pixelColor(x, y));
|
||||
return found;
|
||||
};
|
||||
const int ink = count(reddish), rim = count(yellowish);
|
||||
QVERIFY(ink > 0 && rim > 0);
|
||||
// More letters: more pixels and more effects, redrawn.
|
||||
shown.session.changeTextStyle([](LayerTextStyle &style) { style.content = QStringLiteral("HHH"); });
|
||||
QVERIFY(count(reddish) > ink * 2);
|
||||
QVERIFY(count(yellowish) > rim * 2);
|
||||
}
|
||||
|
||||
void TypedTextCanvasTests::aMaskHidesTypedTextsEffects()
|
||||
{
|
||||
// A black mask hides the whole text, linked.
|
||||
DrawnText shown;
|
||||
const QUuid id = shown.text(QPointF(40, 150), QStringLiteral("HHHH"));
|
||||
shown.session.setEffects(yellowStroke());
|
||||
shown.session.addLayerMask(false);
|
||||
shown.session.editActiveText();
|
||||
unfocus(shown);
|
||||
QVERIFY(!where(shown.grab(), reddish).isValid());
|
||||
QVERIFY(!where(shown.grab(), yellowish).isValid());
|
||||
shown.session.cancelText();
|
||||
// A frame between, as the app always draws one.
|
||||
shown.grab();
|
||||
// Placed over the left half, its black middle hides there.
|
||||
LayerTransform box;
|
||||
rewrite(shown.session, [&](ProjectSnapshot &snapshot) {
|
||||
ProjectLayerRecord &text = record(snapshot, id);
|
||||
box = text.transform;
|
||||
QImage ring = BrushRaster::context(30, 30, true);
|
||||
ring.fill(255);
|
||||
QPainter hole(&ring);
|
||||
hole.fillRect(QRect(10, 10, 10, 10), Qt::black);
|
||||
hole.end();
|
||||
snapshot.masks.insert_or_assign(id, ImportedImage(ring, ring, QStringLiteral("Mask")));
|
||||
text.maskPlacement = LayerTransform{.origin = box.origin, .size = QSizeF(box.size.width() / 2, box.size.height())};
|
||||
text.maskLinked = false;
|
||||
});
|
||||
shown.session.selectLayer(id);
|
||||
shown.session.editActiveText();
|
||||
unfocus(shown);
|
||||
const QImage image = shown.grab();
|
||||
const QSizeF size = shown.documentSize();
|
||||
const auto view = [&](double x, double y) {
|
||||
return shown.session.viewport.viewPoint(box.origin + QPointF(box.size.width() * x, box.size.height() * y), size).toPoint();
|
||||
};
|
||||
// Stretched over the whole text, that band would show.
|
||||
QVERIFY(!where(image.copy(QRect(view(1 / 6.0 + 0.02, 0.4), view(1 / 3.0 - 0.02, 0.6))), reddish).isValid());
|
||||
QVERIFY(where(image.copy(QRect(view(0.5, 0), view(1, 1))), reddish).isValid());
|
||||
}
|
||||
|
||||
void TypedTextCanvasTests::cancelledTextHandsOnNothing()
|
||||
{
|
||||
// Effects never rendered for these pixels: a seed would show.
|
||||
DrawnText shown;
|
||||
const QUuid id = shown.text(QPointF(40, 40), QStringLiteral("HH"));
|
||||
shown.session.setEffects(yellowStroke());
|
||||
shown.session.editActiveText();
|
||||
shown.session.changeTextStyle([](LayerTextStyle &style) { style.content = QStringLiteral("HHHH"); });
|
||||
shown.grab();
|
||||
shown.session.cancelText();
|
||||
shown.grab();
|
||||
const std::optional<EffectsPreviewCache::Result> shownEffects = shown.session.effectsPreviews.rendered(id);
|
||||
QVERIFY(!shownEffects || !shownEffects.value().placement);
|
||||
}
|
||||
|
||||
void TypedTextCanvasTests::typingRepaintsTheWholeView()
|
||||
{
|
||||
// Box text's style alone changes; its effects need the view.
|
||||
DrawnText shown;
|
||||
shown.session.beginText(QRectF(40, 100, 100, 60));
|
||||
shown.session.changeTextStyle([](LayerTextStyle &style) {
|
||||
style.content = QStringLiteral("H");
|
||||
style.fontSize = 20;
|
||||
});
|
||||
QVERIFY(shown.session.finishText());
|
||||
shown.session.setEffects(yellowStroke());
|
||||
shown.session.editActiveText();
|
||||
// Any preview still to land has landed and painted.
|
||||
QTest::qWait(400);
|
||||
PaintedRegion painted;
|
||||
shown.canvas->installEventFilter(&painted);
|
||||
shown.session.changeTextStyle([](LayerTextStyle &style) { style.content = QStringLiteral("HH"); });
|
||||
QTRY_VERIFY(!painted.region.isEmpty());
|
||||
QCOMPARE(painted.region.boundingRect(), shown.canvas->rect());
|
||||
}
|
||||
|
||||
void TypedTextCanvasTests::aHandleThatOnlyMovesRepaintsTheWholeView()
|
||||
{
|
||||
// At 4x, one point's drag moves the box, same size.
|
||||
DrawnText shown;
|
||||
shown.session.beginText(QRectF(185, 135, 50, 40));
|
||||
shown.session.changeTextStyle([](LayerTextStyle &style) {
|
||||
style.content = QStringLiteral("H");
|
||||
style.fontSize = 20;
|
||||
});
|
||||
shown.session.zoom(4);
|
||||
shown.canvas->synchronizeDisplay();
|
||||
const LayerTextStyle before = shown.session.textDraft().value().style;
|
||||
const QPointF left = shown.editor().boxTransform().map(QPointF(0, shown.editor().logicalSize().height() / 2));
|
||||
shown.press(left);
|
||||
QTest::qWait(50);
|
||||
PaintedRegion painted;
|
||||
shown.canvas->installEventFilter(&painted);
|
||||
shown.move(left + QPointF(1, 0));
|
||||
QCOMPARE(shown.session.textDraft().value().style, before);
|
||||
QTRY_VERIFY(!painted.region.isEmpty());
|
||||
QCOMPARE(painted.region.boundingRect(), shown.canvas->rect());
|
||||
shown.release(left + QPointF(1, 0));
|
||||
}
|
||||
|
||||
void TypedTextCanvasTests::typedTextBlendsAsItsLayer()
|
||||
{
|
||||
// Red multiplied over green shows black, typed as committed.
|
||||
for (const bool effects : {false, true}) {
|
||||
DrawnText shown;
|
||||
QImage green = BrushRaster::context(400, 300, false);
|
||||
green.fill(Qt::green);
|
||||
shown.session.addPixelLayer(green, QPointF(0, 0), QStringLiteral("Green"), QStringLiteral("Add"));
|
||||
shown.text(QPointF(40, 150), QStringLiteral("HH"));
|
||||
shown.session.setLayerBlendMode(LayerBlendMode::multiply);
|
||||
if (effects)
|
||||
shown.session.setEffects(yellowStroke());
|
||||
QTRY_VERIFY(!effects || (shown.grab(), shown.session.effectsPreviews.rendered(shown.session.activeLayerID().value())));
|
||||
const QImage committed = shown.grab();
|
||||
shown.session.editActiveText();
|
||||
unfocus(shown);
|
||||
const QRect inside = shown.editor().boxTransform().mapRect(QRectF(QPointF(0, 0), shown.editor().logicalSize())).toAlignedRect().adjusted(10, 10, -10, -10);
|
||||
const QImage typed = shown.grab();
|
||||
QVERIFY(!where(typed.copy(inside), reddish).isValid());
|
||||
QVERIFY2(largestChange(committed, typed, inside) <= 2, qPrintable(QString::number(largestChange(committed, typed, inside))));
|
||||
}
|
||||
}
|
||||
|
||||
void TypedTextCanvasTests::typedTextIsRemadeOnlyWhenItChanges()
|
||||
{
|
||||
QLoggingCategory::setFilterRules(QStringLiteral("omaphoto.rendering.debug=true"));
|
||||
passOn = qInstallMessageHandler(countRemakes);
|
||||
DrawnText shown;
|
||||
beginTextAt(shown.session, QPointF(40, 150));
|
||||
shown.session.changeTextStyle([](LayerTextStyle &style) { style.content = QStringLiteral("H"); });
|
||||
remakes = 0;
|
||||
shown.grab();
|
||||
shown.grab();
|
||||
QCOMPARE(remakes, 1);
|
||||
shown.session.changeTextStyle([](LayerTextStyle &style) { style.content = QStringLiteral("HH"); });
|
||||
shown.grab();
|
||||
shown.grab();
|
||||
QCOMPARE(remakes, 2);
|
||||
// Ended, the image goes: the same style is made again.
|
||||
const LayerTextStyle typed = shown.session.textDraft().value().style;
|
||||
shown.session.cancelText();
|
||||
shown.grab();
|
||||
beginTextAt(shown.session, QPointF(40, 150));
|
||||
shown.session.changeTextStyle([&](LayerTextStyle &style) { style = typed; });
|
||||
shown.grab();
|
||||
QCOMPARE(remakes, 3);
|
||||
qInstallMessageHandler(passOn);
|
||||
QLoggingCategory::setFilterRules(QString());
|
||||
}
|
||||
|
||||
void TypedTextCanvasTests::aMovedBoxRedoesItsTypedEffects()
|
||||
{
|
||||
// A placed mask's band over box text with effects.
|
||||
DrawnText shown;
|
||||
shown.session.beginText(QRectF(185, 135, 50, 60));
|
||||
shown.session.changeTextStyle([](LayerTextStyle &style) {
|
||||
style.content = QStringLiteral("HH");
|
||||
style.fontSize = 20;
|
||||
});
|
||||
QVERIFY(shown.session.finishText());
|
||||
const QUuid id = shown.session.activeLayerID().value();
|
||||
shown.session.setEffects(yellowStroke());
|
||||
shown.session.addLayerMask(true);
|
||||
rewrite(shown.session, [&](ProjectSnapshot &snapshot) {
|
||||
ProjectLayerRecord &text = record(snapshot, id);
|
||||
QImage band = BrushRaster::context(50, 40, true);
|
||||
band.fill(255);
|
||||
QPainter painter(&band);
|
||||
painter.fillRect(QRect(20, 0, 10, 40), Qt::black);
|
||||
painter.end();
|
||||
snapshot.masks.insert_or_assign(id, ImportedImage(band, band, QStringLiteral("Mask")));
|
||||
text.maskPlacement = LayerTransform{.origin = text.transform.origin, .size = QSizeF(40, text.transform.size.height())};
|
||||
text.maskLinked = false;
|
||||
});
|
||||
shown.session.selectLayer(id);
|
||||
shown.session.editActiveText();
|
||||
unfocus(shown);
|
||||
shown.session.zoom(4);
|
||||
shown.canvas->synchronizeDisplay();
|
||||
shown.grab();
|
||||
// A fresh canvas renders the effects anew; they agree.
|
||||
const LayerTextStyle before = shown.session.textDraft().value().style;
|
||||
const LayerTransform moved = shown.editor().shownTransform();
|
||||
const QPointF left = shown.editor().boxTransform().map(QPointF(0, shown.editor().logicalSize().height() / 2));
|
||||
shown.press(left);
|
||||
shown.move(left + QPointF(1, 0));
|
||||
shown.release(left + QPointF(1, 0));
|
||||
QCOMPARE(shown.session.textDraft().value().style, before);
|
||||
QVERIFY(shown.editor().shownTransform().origin != moved.origin);
|
||||
QVERIFY(layerWith(shown.session, id).mask.value().placement);
|
||||
const QImage kept = shown.grab();
|
||||
QVERIFY(where(kept, reddish).isValid() && where(kept, yellowish).isValid());
|
||||
// A fresh canvas renders the effects anew: they must agree.
|
||||
CanvasView fresh(shown.session);
|
||||
fresh.resize(shown.canvas->size());
|
||||
fresh.synchronizeDisplay();
|
||||
const QImage anew = fresh.grab().toImage().convertToFormat(QImage::Format_RGB32);
|
||||
const QRect inside = shown.editor().boxTransform().mapRect(QRectF(QPointF(0, 0), shown.editor().logicalSize())).toAlignedRect().adjusted(10, 10, -10, -10);
|
||||
QVERIFY2(largestChange(kept, anew, inside) <= 2, qPrintable(QString::number(largestChange(kept, anew, inside))));
|
||||
}
|
||||
|
||||
QTEST_MAIN(TypedTextCanvasTests)
|
||||
#include "TypedTextCanvasTests.moc"
|
||||
Reference in new issue
Block a user