/* SPDX-FileCopyrightText: 2025 Vlad Zahorodnii SPDX-License-Identifier: LGPL-2.0-or-later */ #include #include "core/region.h" using namespace KWin; template static constexpr int bitCount() { return sizeof(T) * 8; } static int xyToPow2(int x, int y, int stride) { return std::pow(2, y * stride + x); } static QMap enumerateRects(const QSize &gridSize, const QSize &unit = QSize(5, 5)) { Q_ASSERT(gridSize.width() * gridSize.height() <= bitCount()); QList sumAreaTable; sumAreaTable.resize((gridSize.width() + 1) * (gridSize.height() + 1)); const int satStride = gridSize.width() + 1; for (int x = 0; x <= gridSize.width(); ++x) { sumAreaTable[x] = 0; } for (int y = 0; y < gridSize.height(); ++y) { sumAreaTable[(y + 1) * satStride] = 0; for (int x = 0; x < gridSize.width(); ++x) { sumAreaTable[(y + 1) * satStride + x + 1] = sumAreaTable[(y + 1) * satStride + x] - sumAreaTable[y * satStride + x] + sumAreaTable[y * satStride + x + 1] + xyToPow2(x, y, gridSize.width()); } } QMap rects; rects.insert(0, Rect()); for (int y1 = 0; y1 < gridSize.height(); ++y1) { for (int y2 = y1 + 1; y2 <= gridSize.height(); ++y2) { for (int x1 = 0; x1 < gridSize.width(); ++x1) { for (int x2 = x1 + 1; x2 <= gridSize.width(); ++x2) { const int a = y1 * satStride + x1; const int b = y1 * satStride + x2; const int c = y2 * satStride + x1; const int d = y2 * satStride + x2; const int variant = sumAreaTable[d] + sumAreaTable[a] - sumAreaTable[b] - sumAreaTable[c]; rects.insert(variant, Rect(QPoint(x1 * unit.width(), y1 * unit.height()), QPoint(x2 * unit.width(), y2 * unit.height()))); } } } } return rects; } static int coalesce(QList &rects, int previousStart, int currentStart) { const int previousCount = currentStart - previousStart; const int currentCount = rects.size() - currentStart; if (!currentCount || previousCount != currentCount) { return currentStart; } if (rects[previousStart].bottom() != rects[currentStart].top()) { return currentStart; } for (int i = 0; i < currentCount; ++i) { const Rect &a = rects[previousStart + i]; const Rect &b = rects[currentStart + i]; if (a.left() != b.left() || a.right() != b.right()) { return currentStart; } } const int currentBottom = rects[currentStart].bottom(); for (int i = 0; i < previousCount; ++i) { rects[previousStart + i].setBottom(currentBottom); } rects.remove(currentStart, currentCount); return previousStart; } /** * Returns a region that corresponds to the pattern indicated by the @a bits argument. * * The regions are sampled from a two dimensional grid of size @a xSize by @a ySize. For example, * consider a 3x3 grid: * * a b c * d e f * g h i * * Every item in the grid has a bit assigned to it, "a" is assigned to bit 0, "b" is assigned to * bit 1, and so on. Then numbers between 0 and 2 ^ (xSize * ySize) describe all possible combinations * of rectangles that can be used to construct a region. * * Also note that bit operations such as OR, XOR, and AND on bit patterns directly map to the * corresponding region operations. */ static Region bitsToRegion(int bits, const QSize &gridSize, const QSize &unit = QSize(5, 5)) { QList rects; rects.reserve(gridSize.width() * gridSize.height()); int current = 0; for (int i = 0; i < gridSize.height(); ++i) { std::optional currentRect; const int previous = current; current = rects.size(); for (int j = 0; j < gridSize.width(); ++j) { const int bitIndex = i * gridSize.width() + j; const int bitValue = bits & (1 << bitIndex); if (bitValue) { if (currentRect) { currentRect->setRight((j + 1) * unit.width()); } else { currentRect = Rect(j * unit.width(), i * unit.height(), unit.width(), unit.height()); } } else if (currentRect) { rects.append(*currentRect); currentRect = std::nullopt; } } if (currentRect) { rects.append(*currentRect); } current = coalesce(rects, previous, current); } return Region::fromSortedRects(rects); } static QList enumerateRegions(const QSize &gridSize, const QSize &unit = QSize(5, 5)) { Q_ASSERT(gridSize.width() * gridSize.height() <= bitCount()); const int maxVariations = std::pow(2, gridSize.width() * gridSize.height()); QList regions; regions.reserve(maxVariations); for (int i = 0; i < maxVariations; ++i) { regions.append(bitsToRegion(i, gridSize, unit)); } return regions; } static QSize testGridSize() { const QString text = qEnvironmentVariable("KWIN_TEST_REGION_GRID_SIZE"); if (text.isEmpty()) { return QSize(3, 3); } const qsizetype x = text.indexOf('x'); Q_ASSERT(x != -1); return QSize(text.left(x).toInt(), text.mid(x + 1).toInt()); } class TestRegion : public QObject { Q_OBJECT public: TestRegion() = default; private Q_SLOTS: void equals_data(); void equals(); void notEquals_data(); void notEquals(); void empty_data(); void empty(); void boundingRect_data(); void boundingRect(); void containsRect(); void containsPoint_data(); void containsPoint(); void intersectsRect(); void intersectsRegion(); void united(); void unitedRect(); void subtracted(); void subtractedRect(); void xored(); void xoredRect(); void intersected(); void intersectedRect(); void translated_data(); void translated(); void scaledAndRoundedOut_data(); void scaledAndRoundedOut(); void fromSortedRects(); void fromUnsortedRects(); void fromRectsSortedByY(); void fromAndToQRegion_data(); void fromAndToQRegion(); private: const QSize gridSize = testGridSize(); const QList m_regions = enumerateRegions(gridSize); const QMap m_rects = enumerateRects(gridSize); }; void TestRegion::equals_data() { QTest::addColumn("region1"); QTest::addColumn("region2"); QTest::addColumn("expected"); QTest::addRow("default and default") << Region() << Region() << true; QTest::addRow("(1,2 3x4) and default") << Region(Rect(1, 2, 3, 4)) << Region() << false; QTest::addRow("(1,2 3x4) and (1,2 3x4)") << Region(Rect(1, 2, 3, 4)) << Region(Rect(1, 2, 3, 4)) << true; QTest::addRow("[(1,2 3x4), (5,6 7x8)] and (1,2 3x4)") << (Region(Rect(1, 2, 3, 4)) | Region(Rect(5, 6, 7, 8))) << Region(Rect(1, 2, 3, 4)) << false; QTest::addRow("[(1,2 3x4), (5,6 7x8)] and (5,6 7x8)") << (Region(Rect(1, 2, 3, 4)) | Region(Rect(5, 6, 7, 8))) << Region(Rect(5, 6, 7, 8)) << false; QTest::addRow("[(1,2 3x4), (5,6 7x8)] and [(1,2 3x4), (5,6 7x8)]") << (Region(Rect(1, 2, 3, 4)) | Region(Rect(5, 6, 7, 8))) << (Region(Rect(1, 2, 3, 4)) | Region(Rect(5, 6, 7, 8))) << true; } void TestRegion::equals() { QFETCH(Region, region1); QFETCH(Region, region2); QFETCH(bool, expected); QCOMPARE(region1 == region2, expected); QCOMPARE(region2 == region1, expected); } void TestRegion::notEquals_data() { QTest::addColumn("region1"); QTest::addColumn("region2"); QTest::addColumn("expected"); QTest::addRow("default and default") << Region() << Region() << false; QTest::addRow("(1,2 3x4) and default") << Region(Rect(1, 2, 3, 4)) << Region() << true; QTest::addRow("(1,2 3x4) and (1,2 3x4)") << Region(Rect(1, 2, 3, 4)) << Region(Rect(1, 2, 3, 4)) << false; QTest::addRow("[(1,2 3x4), (5,6 7x8)] and (1,2 3x4)") << (Region(Rect(1, 2, 3, 4)) | Region(Rect(5, 6, 7, 8))) << Region(Rect(1, 2, 3, 4)) << true; QTest::addRow("[(1,2 3x4), (5,6 7x8)] and (5,6 7x8)") << (Region(Rect(1, 2, 3, 4)) | Region(Rect(5, 6, 7, 8))) << Region(Rect(5, 6, 7, 8)) << true; QTest::addRow("[(1,2 3x4), (5,6 7x8)] and [(1,2 3x4), (5,6 7x8)]") << (Region(Rect(1, 2, 3, 4)) | Region(Rect(5, 6, 7, 8))) << (Region(Rect(1, 2, 3, 4)) | Region(Rect(5, 6, 7, 8))) << false; } void TestRegion::notEquals() { QFETCH(Region, region1); QFETCH(Region, region2); QFETCH(bool, expected); QCOMPARE(region1 != region2, expected); QCOMPARE(region2 != region1, expected); } void TestRegion::empty_data() { QTest::addColumn("region"); QTest::addColumn("empty"); QTest::addRow("default") << Region() << true; QTest::addRow("0,0 0x0") << Region(0, 0, 0, 0) << true; QTest::addRow("0,0 1x0") << Region(0, 0, 1, 0) << true; QTest::addRow("0,0 0x1") << Region(0, 0, 0, 1) << true; QTest::addRow("0,0 1x1") << Region(0, 0, 1, 1) << false; } void TestRegion::empty() { QFETCH(Region, region); QTEST(region.isEmpty(), "empty"); } void TestRegion::boundingRect_data() { QTest::addColumn("region"); QTest::addColumn("boundingRect"); QTest::addRow("default") << Region() << Rect(); QTest::addRow("(1,2 0x0)") << Region(1, 2, 0, 0) << Rect(1, 2, 0, 0); QTest::addRow("(1,2 3x4)") << Region(1, 2, 3, 4) << Rect(1, 2, 3, 4); QTest::addRow("[(1,2 3x4), (5,6 7x8)]") << (Region(1, 2, 3, 4) | Region(5, 6, 7, 8)) << Rect(1, 2, 11, 12); } void TestRegion::boundingRect() { QFETCH(Region, region); QTEST(region.boundingRect(), "boundingRect"); } void TestRegion::containsRect() { for (int i = 0; i < m_regions.size(); ++i) { for (const auto &[j, rect] : m_rects.asKeyValueRange()) { QCOMPARE(m_regions[i].contains(rect), j && (i & j) == j); } } } void TestRegion::containsPoint_data() { QTest::addColumn("region"); QTest::addColumn("point"); QTest::addColumn("contains"); QTest::addRow("empty region contains 0,0") << Region() << QPoint(0, 0) << false; QTest::addRow("empty region contains 1,1") << Region() << QPoint(1, 1) << false; const Region simpleRegion = Region(0, 0, 5, 5); { QTest::addRow("above the simple region") << simpleRegion << QPoint(0, -1) << false; QTest::addRow("below the simple region") << simpleRegion << QPoint(0, 6) << false; QTest::addRow("to the left of the simple region") << simpleRegion << QPoint(-1, 0) << false; QTest::addRow("to the right of the simple region") << simpleRegion << QPoint(6, 0) << false; QTest::addRow("at the top edge of the simple region") << simpleRegion << QPoint(2, 0) << true; QTest::addRow("at the bottom edge of the simple region") << simpleRegion << QPoint(2, 5) << false; QTest::addRow("at the left edge of the simple region") << simpleRegion << QPoint(0, 2) << true; QTest::addRow("at the right edge of the simple region") << simpleRegion << QPoint(5, 2) << false; QTest::addRow("inside the simple region") << simpleRegion << QPoint(2, 2) << true; } const Region complexRegion = Region(0, 0, 10, 6) | Region(20, 0, 10, 6) | Region(0, 12, 10, 6) | Region(20, 12, 10, 6); { QTest::addRow("above the top-left rect in the complex region") << complexRegion << QPoint(5, -1) << false; QTest::addRow("below the top-left rect in the complex region") << complexRegion << QPoint(5, 7) << false; QTest::addRow("to the left of the top-left rect in the complex region") << complexRegion << QPoint(-1, 3) << false; QTest::addRow("to the right of the top-left rect in the complex region") << complexRegion << QPoint(11, 3) << false; QTest::addRow("at the top edge of the top-left rect in the complex region") << complexRegion << QPoint(5, 0) << true; QTest::addRow("at the bottom edge of the top-left rect in the complex region") << complexRegion << QPoint(5, 6) << false; QTest::addRow("at the left edge of the top-left rect in the complex region") << complexRegion << QPoint(0, 3) << true; QTest::addRow("at the right edge of the top-left rect in the complex region") << complexRegion << QPoint(10, 3) << false; QTest::addRow("inside the top-left rect in the complex region") << complexRegion << QPoint(5, 3) << true; } { QTest::addRow("above the top-right rect in the complex region") << complexRegion << QPoint(25, -1) << false; QTest::addRow("below the top-right rect in the complex region") << complexRegion << QPoint(25, 7) << false; QTest::addRow("to the left of the top-right rect in the complex region") << complexRegion << QPoint(19, 3) << false; QTest::addRow("to the right of the top-right rect in the complex region") << complexRegion << QPoint(31, 3) << false; QTest::addRow("at the top edge of the top-right rect in the complex region") << complexRegion << QPoint(25, 0) << true; QTest::addRow("at the bottom edge of the top-right rect in the complex region") << complexRegion << QPoint(25, 6) << false; QTest::addRow("at the left edge of the top-right rect in the complex region") << complexRegion << QPoint(20, 3) << true; QTest::addRow("at the right edge of the top-right rect in the complex region") << complexRegion << QPoint(30, 3) << false; QTest::addRow("inside the top-right rect in the complex region") << complexRegion << QPoint(25, 3) << true; } { QTest::addRow("above the bottom-left rect in the complex region") << complexRegion << QPoint(5, 11) << false; QTest::addRow("below the bottom-left rect in the complex region") << complexRegion << QPoint(5, 19) << false; QTest::addRow("to the left of the bottom-left rect in the complex region") << complexRegion << QPoint(-1, 15) << false; QTest::addRow("to the right of the bottom-left rect in the complex region") << complexRegion << QPoint(11, 15) << false; QTest::addRow("at the top edge of the bottom-left rect in the complex region") << complexRegion << QPoint(5, 12) << true; QTest::addRow("at the bottom edge of the bottom-left rect in the complex region") << complexRegion << QPoint(5, 18) << false; QTest::addRow("at the left edge of the bottom-left rect in the complex region") << complexRegion << QPoint(0, 15) << true; QTest::addRow("at the right edge of the bottom-left rect in the complex region") << complexRegion << QPoint(10, 15) << false; QTest::addRow("inside the bottom-left rect in the complex region") << complexRegion << QPoint(5, 15) << true; } { QTest::addRow("above the bottom-right rect in the complex region") << complexRegion << QPoint(25, 11) << false; QTest::addRow("below the bottom-right rect in the complex region") << complexRegion << QPoint(25, 19) << false; QTest::addRow("to the left of the bottom-right rect in the complex region") << complexRegion << QPoint(19, 15) << false; QTest::addRow("to the right of the bottom-right rect in the complex region") << complexRegion << QPoint(31, 15) << false; QTest::addRow("at the top edge of the bottom-right rect in the complex region") << complexRegion << QPoint(25, 12) << true; QTest::addRow("at the bottom edge of the bottom-right rect in the complex region") << complexRegion << QPoint(25, 18) << false; QTest::addRow("at the left edge of the bottom-right rect in the complex region") << complexRegion << QPoint(20, 15) << true; QTest::addRow("at the right edge of the bottom-right rect in the complex region") << complexRegion << QPoint(30, 15) << false; QTest::addRow("inside the bottom-right rect in the complex region") << complexRegion << QPoint(25, 15) << true; } QTest::addRow("above the gap between top-left and top-right rects in the complex region") << complexRegion << QPoint(15, -1) << false; QTest::addRow("below the gap between bottom-left and bottom-right rects in the complex region") << complexRegion << QPoint(15, 19) << false; QTest::addRow("to the left of the gap between top-left and bottom-left rects in the complex region") << complexRegion << QPoint(-1, 9) << false; QTest::addRow("to the right of the gap between top-right and bottom-right rects in the complex region") << complexRegion << QPoint(31, 9) << false; QTest::addRow("inside gap between four rects in the complex region") << complexRegion << QPoint(15, 9) << false; } void TestRegion::containsPoint() { QFETCH(Region, region); QFETCH(QPoint, point); QTEST(region.contains(point), "contains"); } void TestRegion::intersectsRect() { for (int i = 0; i < m_regions.size(); ++i) { for (const auto &[j, rect] : m_rects.asKeyValueRange()) { QCOMPARE(m_regions[i].intersects(rect), bool(i & j)); } } } void TestRegion::intersectsRegion() { for (int i = 0; i < m_regions.size(); ++i) { for (int j = 0; j <= i; ++j) { QCOMPARE(m_regions[i].intersects(m_regions[j]), bool(i & j)); QCOMPARE(m_regions[j].intersects(m_regions[i]), bool(i & j)); } } } void TestRegion::united() { for (int i = 0; i < m_regions.size(); ++i) { for (int j = 0; j <= i; ++j) { const Region expected = m_regions[i | j]; QCOMPARE(m_regions[i].united(m_regions[j]), expected); QCOMPARE(m_regions[j].united(m_regions[i]), expected); } } } void TestRegion::unitedRect() { for (int i = 0; i < m_regions.size(); ++i) { for (const auto &[j, rect] : m_rects.asKeyValueRange()) { const Region expected = m_regions[i | j]; QCOMPARE(m_regions[i].united(rect), expected); } } } void TestRegion::subtracted() { for (int i = 0; i < m_regions.size(); ++i) { for (int j = 0; j <= i; ++j) { QCOMPARE(m_regions[i].subtracted(m_regions[j]), m_regions[i & ~j]); QCOMPARE(m_regions[j].subtracted(m_regions[i]), m_regions[j & ~i]); } } } void TestRegion::subtractedRect() { for (int i = 0; i < m_regions.size(); ++i) { for (const auto &[j, rect] : m_rects.asKeyValueRange()) { const Region expected = m_regions[i & ~j]; QCOMPARE(m_regions[i].subtracted(rect), expected); } } } void TestRegion::xored() { for (int i = 0; i < m_regions.size(); ++i) { for (int j = 0; j <= i; ++j) { const Region expected = m_regions[i ^ j]; QCOMPARE(m_regions[i].xored(m_regions[j]), expected); QCOMPARE(m_regions[j].xored(m_regions[i]), expected); } } } void TestRegion::xoredRect() { for (int i = 0; i < m_regions.size(); ++i) { for (const auto &[j, rect] : m_rects.asKeyValueRange()) { const Region expected = m_regions[i ^ j]; QCOMPARE(m_regions[i].xored(rect), expected); } } } void TestRegion::intersected() { for (int i = 0; i < m_regions.size(); ++i) { for (int j = 0; j <= i; ++j) { const Region expected = m_regions[i & j]; QCOMPARE(m_regions[i].intersected(m_regions[j]), expected); QCOMPARE(m_regions[j].intersected(m_regions[i]), expected); } } } void TestRegion::intersectedRect() { for (int i = 0; i < m_regions.size(); ++i) { for (const auto &[j, rect] : m_rects.asKeyValueRange()) { const Region expected = m_regions[i & j]; QCOMPARE(m_regions[i].intersected(rect), expected); } } } void TestRegion::translated_data() { QTest::addColumn("region"); QTest::addColumn("translation"); QTest::addColumn("expected"); QTest::addRow("empty") << Region() << QPoint(1, 2) << Region(); QTest::addRow("simple") << Region(1, 2, 3, 4) << QPoint(10, 11) << Region(11, 13, 3, 4); QTest::addRow("complex") << (Region(1, 2, 3, 4) | Region(5, 6, 7, 8)) << QPoint(10, 11) << (Region(11, 13, 3, 4) | Region(15, 17, 7, 8)); } void TestRegion::translated() { QFETCH(Region, region); QFETCH(QPoint, translation); { Region translated = region; translated.translate(translation.x(), translation.y()); QTEST(translated, "expected"); } { Region translated = region; translated.translate(translation); QTEST(translated, "expected"); } { const Region translated = region.translated(translation.x(), translation.y()); QTEST(translated, "expected"); } { const Region translated = region.translated(translation); QTEST(translated, "expected"); } } void TestRegion::scaledAndRoundedOut_data() { QTest::addColumn("region"); QTest::addColumn("scale"); QTest::addColumn("expected"); QTest::addRow("empty @ 1.0") << Region() << 1.0 << Region(); QTest::addRow("empty @ 1.25") << Region() << 1.25 << Region(); QTest::addRow("empty @ 1.5") << Region() << 1.5 << Region(); QTest::addRow("empty @ 1.75") << Region() << 1.75 << Region(); QTest::addRow("empty @ 2.0") << Region() << 2.0 << Region(); const Region simple = Region(1, 2, 3, 4); QTest::addRow("simple @ 1.0") << simple << 1.0 << simple; QTest::addRow("simple @ 1.25") << simple << 1.25 << Region(1, 2, 4, 6); QTest::addRow("simple @ 1.5") << simple << 1.5 << Region(1, 3, 5, 6); QTest::addRow("simple @ 1.75") << simple << 1.75 << Region(1, 3, 6, 8); QTest::addRow("simple @ 2.0") << simple << 2.0 << Region(2, 4, 6, 8); const Region complex = Region(1, 2, 3, 4) | Region(5, 6, 7, 8); QTest::addRow("complex @ 1.0") << complex << 1.0 << complex; QTest::addRow("complex @ 1.25") << complex << 1.25 << (Region(1, 2, 4, 6) | Region(6, 7, 9, 11)); QTest::addRow("complex @ 1.5") << complex << 1.5 << (Region(1, 3, 5, 6) | Region(7, 9, 11, 12)); QTest::addRow("complex @ 1.75") << complex << 1.75 << (Region(1, 3, 6, 8) | Region(8, 10, 13, 15)); QTest::addRow("complex @ 2.0") << complex << 2.0 << (Region(2, 4, 6, 8) | Region(10, 12, 14, 16)); } void TestRegion::scaledAndRoundedOut() { QFETCH(Region, region); QFETCH(qreal, scale); QTEST(region.scaledAndRoundedOut(scale), "expected"); } template static QList spanToList(const QSpan &span) { QList list; list.assign(span.begin(), span.end()); return list; } void TestRegion::fromSortedRects() { for (int i = 0; i < m_regions.size(); ++i) { QCOMPARE(Region::fromSortedRects(spanToList(m_regions[i].rects())), m_regions[i]); } } void TestRegion::fromUnsortedRects() { for (int i = 0; i < m_regions.size(); ++i) { for (int j = 0; j < m_regions.size(); ++j) { const QList rects = spanToList(m_regions[i].rects()) + spanToList(m_regions[j].rects()); QCOMPARE(Region::fromUnsortedRects(rects), m_regions[i | j]); } } } void TestRegion::fromRectsSortedByY() { for (int i = 0; i < m_regions.size(); ++i) { for (int j = 0; j < m_regions.size(); ++j) { QList rects = spanToList(m_regions[i].rects()) + spanToList(m_regions[j].rects()); std::sort(rects.begin(), rects.end(), [](const Rect &a, const Rect &b) { return a.top() < b.top(); }); QCOMPARE(Region::fromRectsSortedByY(rects), m_regions[i | j]); } } } void TestRegion::fromAndToQRegion_data() { QTest::addColumn("region"); QTest::addColumn("qtRegion"); QTest::addRow("empty") << Region() << QRegion(); QTest::addRow("simple") << Region(1, 2, 3, 4) << QRegion(1, 2, 3, 4); QTest::addRow("complex") << (Region(1, 2, 3, 4) | Region(5, 6, 7, 8)) << (QRegion(1, 2, 3, 4) | QRegion(5, 6, 7, 8)); } void TestRegion::fromAndToQRegion() { QFETCH(Region, region); QFETCH(QRegion, qtRegion); QCOMPARE(Region(qtRegion), region); QCOMPARE(QRegion(region), qtRegion); } QTEST_MAIN(TestRegion) #include "test_region.moc"