til::bitmap (#4967)

## Summary of the Pull Request
Introduces type `til::bitmap` which implements an NxM grid of bits that can be used to track dirty/clean state on a per-cell basis throughout a rectangle.

## PR Checklist
* [x] In support of Differential Rendering #778
* [X] I work here.
* [x] Tests added/passed
* [x] I'm a core contributor.

## Detailed Description of the Pull Request / Additional comments
- Adds `const_iterator` to `til::rectangle` that will walk from top to bottom, left to right every position in the rectangle as a `til::point` and associated test.
- Adds `bool til::rectangle::contains(til::point)` to determine if a point lies within the rectangle and the associated test
- Adds complementary methods to `til::rectangle` of `index_of(til::point)` and `point_at(ptrdiff_t)` which will convert between a valid `point` position that lies inside the `rectangle` and the index as a count of cells from the top left corner (origin) in a top to bottom & left to right counting fashion (and associated tests).
- Adds `til::some<T, N>::clear()` to empty out the contents of the `some` and associated test.
THEN with all that support...
- Adds `til::bitmap` which represents a 2 dimensional grid of boolean/bit flags. This class contains set and reset methods for the entire region, and set only for a single `til::point` or a subregion as specified by a `til::rectangle` (and associated tests.) 
- Adds convenience methods of `any()`, `one()`, `none()`, and `all()` to the `til::bitmap` to check some of its state.
- Adds convenience method of `resize()` to `til::bitmap` that will grow or shrink the bitmap, copying whatever is left of the previous one that still fits and optionally filling or blanking the new space.
- Adds a `const_iterator` for `til::bitmap` that will walk top to bottom, left to right and return a `til::rectangle` representing a run of bits that are all on sequentially in a row. Breaks per row. Exactly as we expect to be drawing things (and associated tests.)

## Validation Steps Performed
- See automated tests of functionality.
This commit is contained in:
Michael Niksa 2020-03-19 09:10:13 -07:00 committed by GitHub
parent ddcdff15d3
commit 9e9473cfb2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 1141 additions and 0 deletions

View file

@ -9,6 +9,7 @@
#include "til/size.h"
#include "til/point.h"
#include "til/rectangle.h"
#include "til/bitmap.h"
#include "til/u8u16convert.h"
namespace til // Terminal Implementation Library. Also: "Today I Learned"

267
src/inc/til/bitmap.h Normal file
View file

@ -0,0 +1,267 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#ifdef UNIT_TESTING
class BitmapTests;
#endif
namespace til // Terminal Implementation Library. Also: "Today I Learned"
{
namespace details
{
class _bitmap_const_iterator
{
public:
_bitmap_const_iterator(const std::vector<bool>& values, til::rectangle rc, ptrdiff_t pos) :
_values(values),
_rc(rc),
_pos(pos),
_end(rc.size().area())
{
_calculateArea();
}
_bitmap_const_iterator& operator++()
{
_pos = _nextPos;
_calculateArea();
return (*this);
}
constexpr bool operator==(const _bitmap_const_iterator& other) const noexcept
{
return _pos == other._pos && _values == other._values;
}
constexpr bool operator!=(const _bitmap_const_iterator& other) const noexcept
{
return !(*this == other);
}
constexpr bool operator<(const _bitmap_const_iterator& other) const noexcept
{
return _pos < other._pos;
}
constexpr bool operator>(const _bitmap_const_iterator& other) const noexcept
{
return _pos > other._pos;
}
constexpr til::rectangle operator*() const noexcept
{
return _run;
}
private:
const std::vector<bool>& _values;
const til::rectangle _rc;
ptrdiff_t _pos;
ptrdiff_t _nextPos;
const ptrdiff_t _end;
til::rectangle _run;
void _calculateArea()
{
// Backup the position as the next one.
_nextPos = _pos;
// Seek forward until we find an on bit.
while (_nextPos < _end && !_values.at(_nextPos))
{
++_nextPos;
}
// If we haven't reached the end yet...
if (_nextPos < _end)
{
// pos is now at the first on bit.
const auto runStart = _rc.point_at(_nextPos);
// We'll only count up until the end of this row.
// a run can be a max of one row tall.
const ptrdiff_t rowEndIndex = _rc.index_of(til::point(_rc.right() - 1, runStart.y())) + 1;
// Find the length for the rectangle.
ptrdiff_t runLength = 0;
// We have at least 1 so start with a do/while.
do
{
++_nextPos;
++runLength;
} while (_nextPos < rowEndIndex && _values.at(_nextPos));
// Keep going until we reach end of row, end of the buffer, or the next bit is off.
// Assemble and store that run.
_run = til::rectangle{ runStart, til::size{ runLength, static_cast<ptrdiff_t>(1) } };
}
else
{
// If we reached the end, set the pos because the run is empty.
_pos = _nextPos;
_run = til::rectangle{};
}
}
};
}
class bitmap
{
public:
using const_iterator = details::_bitmap_const_iterator;
bitmap() noexcept :
_sz{},
_rc{},
_bits{},
_dirty{}
{
}
bitmap(til::size sz) :
bitmap(sz, false)
{
}
bitmap(til::size sz, bool fill) :
_sz(sz),
_rc(sz),
_bits(sz.area(), fill),
_dirty(fill ? sz : til::rectangle{})
{
}
const_iterator begin() const
{
return const_iterator(_bits, _sz, 0);
}
const_iterator end() const
{
return const_iterator(_bits, _sz, _sz.area());
}
void set(const til::point pt)
{
THROW_HR_IF(E_INVALIDARG, !_rc.contains(pt));
til::at(_bits, _rc.index_of(pt)) = true;
_dirty |= til::rectangle{ pt };
}
void set(const til::rectangle rc)
{
THROW_HR_IF(E_INVALIDARG, !_rc.contains(rc));
for (const auto pt : rc)
{
til::at(_bits, _rc.index_of(pt)) = true;
}
_dirty |= rc;
}
void set_all()
{
// .clear() then .resize(_size(), true) throws an assert (unsupported operation)
// .assign(_size(), true) throws an assert (unsupported operation)
_bits = std::vector<bool>(_sz.area(), true);
_dirty = _rc;
}
void reset_all()
{
// .clear() then .resize(_size(), false) throws an assert (unsupported operation)
// .assign(_size(), false) throws an assert (unsupported operation)
_bits = std::vector<bool>(_sz.area(), false);
_dirty = {};
}
// True if we resized. False if it was the same size as before.
// Set fill if you want the new region (on growing) to be marked dirty.
bool resize(til::size size, bool fill = false)
{
// FYI .resize(_size(), true/false) throws an assert (unsupported operation)
// Don't resize if it's not different
if (_sz != size)
{
// Make a new bitmap for the other side, empty initially.
auto newMap = bitmap(size, false);
// Copy any regions that overlap from this map to the new one.
// Just iterate our runs...
for (const auto run : *this)
{
// intersect them with the new map
// so we don't attempt to set bits that fit outside
// the new one.
const auto intersect = run & newMap._rc;
// and if there is still anything left, set them.
if (!intersect.empty())
{
newMap.set(intersect);
}
}
// Then, if we were requested to fill the new space on growing,
// find the space in the new rectangle that wasn't in the old
// and fill it up.
if (fill)
{
// A subtraction will yield anything in the new that isn't
// a part of the old.
const auto newAreas = newMap._rc - _rc;
for (const auto& area : newAreas)
{
newMap.set(area);
}
}
// Swap and return.
std::swap(newMap, *this);
return true;
}
else
{
return false;
}
}
bool one() const
{
return _dirty.size() == til::size{ 1, 1 };
}
constexpr bool any() const noexcept
{
return !none();
}
constexpr bool none() const noexcept
{
return _dirty.empty();
}
constexpr bool all() const noexcept
{
return _dirty == _rc;
}
private:
til::rectangle _dirty;
til::size _sz;
til::rectangle _rc;
std::vector<bool> _bits;
#ifdef UNIT_TESTING
friend class ::BitmapTests;
#endif
};
}

View file

@ -13,9 +13,90 @@ class RectangleTests;
namespace til // Terminal Implementation Library. Also: "Today I Learned"
{
namespace details
{
class _rectangle_const_iterator
{
public:
constexpr _rectangle_const_iterator(point topLeft, point bottomRight) :
_topLeft(topLeft),
_bottomRight(bottomRight),
_current(topLeft)
{
}
constexpr _rectangle_const_iterator(point topLeft, point bottomRight, point start) :
_topLeft(topLeft),
_bottomRight(bottomRight),
_current(start)
{
}
_rectangle_const_iterator& operator++()
{
ptrdiff_t nextX;
THROW_HR_IF(E_ABORT, !::base::CheckAdd(_current.x(), 1).AssignIfValid(&nextX));
if (nextX >= _bottomRight.x())
{
ptrdiff_t nextY;
THROW_HR_IF(E_ABORT, !::base::CheckAdd(_current.y(), 1).AssignIfValid(&nextY));
// Note for the standard Left-to-Right, Top-to-Bottom walk,
// the end position is one cell below the bottom left.
// (or more accurately, on the exclusive bottom line in the inclusive left column.)
_current = { _topLeft.x(), nextY };
}
else
{
_current = { nextX, _current.y() };
}
return (*this);
}
constexpr bool operator==(const _rectangle_const_iterator& other) const
{
return _current == other._current &&
_topLeft == other._topLeft &&
_bottomRight == other._bottomRight;
}
constexpr bool operator!=(const _rectangle_const_iterator& other) const
{
return !(*this == other);
}
constexpr bool operator<(const _rectangle_const_iterator& other) const
{
return _current < other._current;
}
constexpr bool operator>(const _rectangle_const_iterator& other) const
{
return _current > other._current;
}
constexpr point operator*() const
{
return _current;
}
protected:
point _current;
const point _topLeft;
const point _bottomRight;
#ifdef UNIT_TESTING
friend class ::RectangleTests;
#endif
};
}
class rectangle
{
public:
using const_iterator = details::_rectangle_const_iterator;
constexpr rectangle() noexcept :
rectangle(til::point{ 0, 0 }, til::point{ 0, 0 })
{
@ -115,6 +196,22 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
_topLeft.y() < _bottomRight.y();
}
constexpr const_iterator begin() const
{
return const_iterator(_topLeft, _bottomRight);
}
constexpr const_iterator end() const
{
// For the standard walk: Left-To-Right then Top-To-Bottom
// the end box is one cell below the left most column.
// |----| 5x2 square. Remember bottom & right are exclusive
// | | while top & left are inclusive.
// X----- X is the end position.
return const_iterator(_topLeft, _bottomRight, { _topLeft.x(), _bottomRight.y() });
}
// OR = union
constexpr rectangle operator|(const rectangle& other) const noexcept
{
@ -147,6 +244,12 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
return rectangle{ l, t, r, b };
}
constexpr rectangle& operator|=(const rectangle& other) noexcept
{
*this = *this | other;
return *this;
}
// AND = intersect
constexpr rectangle operator&(const rectangle& other) const noexcept
{
@ -400,6 +503,57 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
return !operator bool();
}
constexpr bool contains(til::point pt) const
{
return pt.x() >= _topLeft.x() && pt.x() < _bottomRight.x() &&
pt.y() >= _topLeft.y() && pt.y() < _bottomRight.y();
}
bool contains(ptrdiff_t index) const
{
return index >= 0 && index < size().area();
}
constexpr bool contains(til::rectangle rc) const
{
// Union the other rectangle and ourselves.
// If the result of that didn't grow at all, then we already
// fully contained the rectangle we were given.
return (*this | rc) == *this;
}
ptrdiff_t index_of(til::point pt) const
{
THROW_HR_IF(E_INVALIDARG, !contains(pt));
// Take Y away from the top to find how many rows down
auto check = base::CheckSub(pt.y(), top());
// Multiply by the width because we've passed that many
// widths-worth of indices.
check *= width();
// Then add in the last few indices in the x position this row
// and subtract left to find the offset from left edge.
check = check + pt.x() - left();
ptrdiff_t result;
THROW_HR_IF(E_ABORT, !check.AssignIfValid(&result));
return result;
}
til::point point_at(ptrdiff_t index) const
{
THROW_HR_IF(E_INVALIDARG, !contains(index));
const auto div = std::div(index, width());
// Not checking math on these because we're presuming
// that the point can't be in bounds of a rectangle where
// this would overflow on addition after the division.
return til::point{ div.rem + left(), div.quot + top() };
}
#ifdef _WINCONTYPES_
// NOTE: This will convert back to INCLUSIVE on the way out because
// that is generally how SMALL_RECTs are handled in console code and via the APIs.

View file

@ -127,6 +127,12 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
return !_used;
}
constexpr void clear() noexcept
{
_used = 0;
_array = {}; // should free members, if necessary.
}
constexpr const_reference at(size_type pos) const
{
if (_used <= pos)
@ -169,6 +175,18 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
++_used;
}
void push_back(T&& val)
{
if (_used >= N)
{
_outOfRange();
}
til::at(_array, _used) = std::move(val);
++_used;
}
void pop_back()
{
if (_used <= 0)

View file

@ -0,0 +1,445 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "til/bitmap.h"
using namespace WEX::Common;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
class BitmapTests
{
TEST_CLASS(BitmapTests);
void _checkBits(const til::rectangle& bitsOn,
const til::bitmap& map)
{
_checkBits(std::vector<til::rectangle>{ bitsOn }, map);
}
void _checkBits(const std::vector<til::rectangle>& bitsOn,
const til::bitmap& map)
{
Log::Comment(L"Check dirty rectangles.");
// Union up all the dirty rectangles into one big one.
auto dirtyExpected = bitsOn.front();
for (auto it = bitsOn.cbegin() + 1; it < bitsOn.cend(); ++it)
{
dirtyExpected |= *it;
}
// Check if it matches.
VERIFY_ARE_EQUAL(dirtyExpected, map._dirty);
Log::Comment(L"Check all bits in map.");
// For every point in the map...
for (const auto pt : map._rc)
{
// If any of the rectangles we were given contains this point, we expect it should be on.
const auto expected = std::any_of(bitsOn.cbegin(), bitsOn.cend(), [&pt](auto bitRect) { return bitRect.contains(pt); });
// Get the actual bit out of the map.
const auto actual = map._bits[map._rc.index_of(pt)];
// Do it this way and not with equality so you can see it in output.
if (expected)
{
VERIFY_IS_TRUE(actual);
}
else
{
VERIFY_IS_FALSE(actual);
}
}
}
TEST_METHOD(DefaultConstruct)
{
const til::bitmap bitmap;
const til::size expectedSize{ 0, 0 };
const til::rectangle expectedRect{ 0, 0, 0, 0 };
VERIFY_ARE_EQUAL(expectedSize, bitmap._sz);
VERIFY_ARE_EQUAL(expectedRect, bitmap._rc);
VERIFY_ARE_EQUAL(0u, bitmap._bits.size());
VERIFY_ARE_EQUAL(til::rectangle{}, bitmap._dirty);
// The find will go from begin to end in the bits looking for a "true".
// It should miss so the result should be "cend" and turn out true here.
VERIFY_IS_TRUE(bitmap._bits.cend() == std::find(bitmap._bits.cbegin(), bitmap._bits.cend(), true));
}
TEST_METHOD(SizeConstruct)
{
const til::size expectedSize{ 5, 10 };
const til::rectangle expectedRect{ 0, 0, 5, 10 };
const til::bitmap bitmap{ expectedSize };
VERIFY_ARE_EQUAL(expectedSize, bitmap._sz);
VERIFY_ARE_EQUAL(expectedRect, bitmap._rc);
VERIFY_ARE_EQUAL(50u, bitmap._bits.size());
VERIFY_ARE_EQUAL(til::rectangle{}, bitmap._dirty);
// The find will go from begin to end in the bits looking for a "true".
// It should miss so the result should be "cend" and turn out true here.
VERIFY_IS_TRUE(bitmap._bits.cend() == std::find(bitmap._bits.cbegin(), bitmap._bits.cend(), true));
}
TEST_METHOD(SizeConstructWithFill)
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:fill", L"{true, false}")
END_TEST_METHOD_PROPERTIES()
bool fill;
VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"fill", fill));
const til::size expectedSize{ 5, 10 };
const til::rectangle expectedRect{ 0, 0, 5, 10 };
const til::bitmap bitmap{ expectedSize, fill };
VERIFY_ARE_EQUAL(expectedSize, bitmap._sz);
VERIFY_ARE_EQUAL(expectedRect, bitmap._rc);
VERIFY_ARE_EQUAL(50u, bitmap._bits.size());
if (!fill)
{
// The find will go from begin to end in the bits looking for a "true".
// It should miss so the result should be "cend" and turn out true here.
VERIFY_IS_TRUE(bitmap._bits.cend() == std::find(bitmap._bits.cbegin(), bitmap._bits.cend(), true));
VERIFY_ARE_EQUAL(til::rectangle{}, bitmap._dirty);
}
else
{
// The find will go from begin to end in the bits looking for a "false".
// It should miss so the result should be "cend" and turn out true here.
VERIFY_IS_TRUE(bitmap._bits.cend() == std::find(bitmap._bits.cbegin(), bitmap._bits.cend(), false));
VERIFY_ARE_EQUAL(expectedRect, bitmap._dirty);
}
}
TEST_METHOD(SetReset)
{
const til::size sz{ 4, 4 };
til::bitmap bitmap{ sz };
// Every bit should be false.
Log::Comment(L"All bits false on creation.");
for (auto bit : bitmap._bits)
{
VERIFY_IS_FALSE(bit);
}
const til::point point{ 2, 2 };
bitmap.set(point);
til::rectangle expectedSet{ point };
// Run through every bit. Only the one we set should be true.
Log::Comment(L"Only the bit we set should be true.");
_checkBits(expectedSet, bitmap);
Log::Comment(L"Setting all should mean they're all true.");
bitmap.set_all();
expectedSet = til::rectangle{ bitmap._rc };
_checkBits(expectedSet, bitmap);
Log::Comment(L"Now reset them all.");
bitmap.reset_all();
expectedSet = {};
_checkBits(expectedSet, bitmap);
til::rectangle totalZone{ sz };
Log::Comment(L"Set a rectangle of bits and test they went on.");
// 0 0 0 0 |1 1|0 0
// 0 0 0 0 --\ |1 1|0 0
// 0 0 0 0 --/ |1 1|0 0
// 0 0 0 0 0 0 0 0
til::rectangle setZone{ til::point{ 0, 0 }, til::size{ 2, 3 } };
bitmap.set(setZone);
expectedSet = setZone;
_checkBits(expectedSet, bitmap);
Log::Comment(L"Reset all.");
bitmap.reset_all();
expectedSet = {};
_checkBits(expectedSet, bitmap);
}
TEST_METHOD(SetResetExceptions)
{
til::bitmap map{ til::size{ 4, 4 } };
Log::Comment(L"1.) SetPoint out of bounds.");
{
auto fn = [&]() {
map.set(til::point{ 10, 10 });
};
VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; });
}
Log::Comment(L"2.) SetRectangle out of bounds.");
{
auto fn = [&]() {
map.set(til::rectangle{ til::point{
2,
2,
},
til::size{ 10, 10 } });
};
VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; });
}
}
TEST_METHOD(Resize)
{
Log::Comment(L"Set up a bitmap with every location flagged.");
const til::size originalSize{ 2, 2 };
til::bitmap bitmap{ originalSize, true };
std::vector<til::rectangle> expectedFillRects;
// 1 1
// 1 1
expectedFillRects.emplace_back(til::rectangle{ originalSize });
_checkBits(expectedFillRects, bitmap);
Log::Comment(L"Attempt resize to the same size.");
VERIFY_IS_FALSE(bitmap.resize(originalSize));
// 1 1
// 1 1
_checkBits(expectedFillRects, bitmap);
Log::Comment(L"Attempt resize to a new size where both dimensions grow and we didn't ask for fill.");
VERIFY_IS_TRUE(bitmap.resize(til::size{ 3, 3 }));
// 1 1 0
// 1 1 0
// 0 0 0
_checkBits(expectedFillRects, bitmap);
Log::Comment(L"Set a bit out in the new space and check it.");
const til::point spaceBit{ 1, 2 };
expectedFillRects.emplace_back(til::rectangle{ spaceBit });
bitmap.set(spaceBit);
// 1 1 0
// 1 1 0
// 0 1 0
_checkBits(expectedFillRects, bitmap);
Log::Comment(L"Grow vertically and shrink horizontally at the same time. Fill any new space.");
expectedFillRects.emplace_back(til::rectangle{ til::point{ 0, 3 }, til::size{ 2, 1 } });
bitmap.resize(til::size{ 2, 4 }, true);
// 1 1
// 1 1
// 0 1
// 1 1
_checkBits(expectedFillRects, bitmap);
}
TEST_METHOD(One)
{
Log::Comment(L"When created, it should be not be one.");
til::bitmap bitmap{ til::size{ 2, 2 } };
VERIFY_IS_FALSE(bitmap.one());
Log::Comment(L"When a single point is set, it should be one.");
bitmap.set(til::point{ 1, 0 });
VERIFY_IS_TRUE(bitmap.one());
Log::Comment(L"Setting the same point again, should still be one.");
bitmap.set(til::point{ 1, 0 });
VERIFY_IS_TRUE(bitmap.one());
Log::Comment(L"Setting another point, it should no longer be one.");
bitmap.set(til::point{ 0, 0 });
VERIFY_IS_FALSE(bitmap.one());
Log::Comment(L"Clearing it, still not one.");
bitmap.reset_all();
VERIFY_IS_FALSE(bitmap.one());
Log::Comment(L"Set one point, one again.");
bitmap.set(til::point{ 1, 0 });
VERIFY_IS_TRUE(bitmap.one());
Log::Comment(L"And setting all will no longer be one again.");
bitmap.set_all();
VERIFY_IS_FALSE(bitmap.one());
}
TEST_METHOD(Any)
{
Log::Comment(L"When created, it should be not be any.");
til::bitmap bitmap{ til::size{ 2, 2 } };
VERIFY_IS_FALSE(bitmap.any());
Log::Comment(L"When a single point is set, it should be any.");
bitmap.set(til::point{ 1, 0 });
VERIFY_IS_TRUE(bitmap.any());
Log::Comment(L"Setting the same point again, should still be any.");
bitmap.set(til::point{ 1, 0 });
VERIFY_IS_TRUE(bitmap.any());
Log::Comment(L"Setting another point, it should still be any.");
bitmap.set(til::point{ 0, 0 });
VERIFY_IS_TRUE(bitmap.any());
Log::Comment(L"Clearing it, no longer any.");
bitmap.reset_all();
VERIFY_IS_FALSE(bitmap.any());
Log::Comment(L"Set one point, one again, it's any.");
bitmap.set(til::point{ 1, 0 });
VERIFY_IS_TRUE(bitmap.any());
Log::Comment(L"And setting all will be any as well.");
bitmap.set_all();
VERIFY_IS_TRUE(bitmap.any());
}
TEST_METHOD(None)
{
Log::Comment(L"When created, it should be none.");
til::bitmap bitmap{ til::size{ 2, 2 } };
VERIFY_IS_TRUE(bitmap.none());
Log::Comment(L"When it is modified with a set, it should no longer be none.");
bitmap.set(til::point{ 0, 0 });
VERIFY_IS_FALSE(bitmap.none());
Log::Comment(L"Resetting all, it will report none again.");
bitmap.reset_all();
VERIFY_IS_TRUE(bitmap.none());
Log::Comment(L"And setting all will no longer be none again.");
bitmap.set_all();
VERIFY_IS_FALSE(bitmap.none());
}
TEST_METHOD(All)
{
Log::Comment(L"When created, it should be not be all.");
til::bitmap bitmap{ til::size{ 2, 2 } };
VERIFY_IS_FALSE(bitmap.all());
Log::Comment(L"When a single point is set, it should not be all.");
bitmap.set(til::point{ 1, 0 });
VERIFY_IS_FALSE(bitmap.all());
Log::Comment(L"Setting the same point again, should still not be all.");
bitmap.set(til::point{ 1, 0 });
VERIFY_IS_FALSE(bitmap.all());
Log::Comment(L"Setting another point, it should still not be all.");
bitmap.set(til::point{ 0, 0 });
VERIFY_IS_FALSE(bitmap.all());
Log::Comment(L"Clearing it, still not all.");
bitmap.reset_all();
VERIFY_IS_FALSE(bitmap.all());
Log::Comment(L"Set one point, one again, not all.");
bitmap.set(til::point{ 1, 0 });
VERIFY_IS_FALSE(bitmap.all());
Log::Comment(L"And setting all will finally be all.");
bitmap.set_all();
VERIFY_IS_TRUE(bitmap.all());
Log::Comment(L"Clearing it, back to not all.");
bitmap.reset_all();
VERIFY_IS_FALSE(bitmap.all());
}
TEST_METHOD(Iterate)
{
// This map --> Those runs
// 1 1 0 1 A A _ B
// 1 0 1 1 C _ D D
// 0 0 1 0 _ _ E _
// 0 1 1 0 _ F F _
Log::Comment(L"Set up a bitmap with some runs.");
// Note: we'll test setting/resetting some overlapping rects and points.
til::bitmap map{ til::size{ 4, 4 } };
// 0 0 0 0 |1 1|0 0
// 0 0 0 0 0 0 0 0
// 0 0 0 0 --> 0 0 0 0
// 0 0 0 0 0 0 0 0
map.set(til::rectangle{ til::point{ 0, 0 }, til::size{ 2, 1 } });
// 1 1 0 0 1 1 0 0
// 0 0 0 0 0 0|1|0
// 0 0 0 0 --> 0 0|1|0
// 0 0 0 0 0 0|1|0
map.set(til::rectangle{ til::point{ 2, 1 }, til::size{ 1, 3 } });
// 1 1 0 0 1 1 0|1|
// 0 0 1 0 0 0 1|1|
// 0 0 1 0 --> 0 0 1 0
// 0 0 1 0 0 0 1 0
map.set(til::rectangle{ til::point{ 3, 0 }, til::size{ 1, 2 } });
// 1 1 0 1 1 1 0 1
// 0 0 1 1 |1|0 1 1
// 0 0 1 0 --> 0 0 1 0
// 0 0 1 0 0 0 1 0
map.set(til::point{ 0, 1 });
// 1 1 0 1 1 1 0 1
// 1 0 1 1 1 0 1 1
// 0 0 1 0 --> 0 0 1 0
// 0 0 1 0 0|1|1 0
map.set(til::point{ 1, 3 });
Log::Comment(L"Building the expected run rectangles.");
// Reminder, we're making 6 rectangle runs A-F like this:
// A A _ B
// C _ D D
// _ _ E _
// _ F F _
til::some<til::rectangle, 6> expected;
expected.push_back(til::rectangle{ til::point{ 0, 0 }, til::size{ 2, 1 } });
expected.push_back(til::rectangle{ til::point{ 3, 0 }, til::size{ 1, 1 } });
expected.push_back(til::rectangle{ til::point{ 0, 1 }, til::size{ 1, 1 } });
expected.push_back(til::rectangle{ til::point{ 2, 1 }, til::size{ 2, 1 } });
expected.push_back(til::rectangle{ til::point{ 2, 2 }, til::size{ 1, 1 } });
expected.push_back(til::rectangle{ til::point{ 1, 3 }, til::size{ 2, 1 } });
Log::Comment(L"Run the iterator and collect the runs.");
til::some<til::rectangle, 6> actual;
for (auto run : map)
{
actual.push_back(run);
}
Log::Comment(L"Verify they match what we expected.");
VERIFY_ARE_EQUAL(expected, actual);
Log::Comment(L"Clear the map and iterate and make sure we get no results.");
map.reset_all();
expected.clear();
actual.clear();
for (auto run : map)
{
actual.push_back(run);
}
Log::Comment(L"Verify they're empty.");
VERIFY_ARE_EQUAL(expected, actual);
}
};

View file

@ -725,6 +725,141 @@ class RectangleTests
VERIFY_ARE_EQUAL(expected, actual.empty());
}
TEST_METHOD(ContainsPoint)
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:x", L"{-1000,0,4,5,6,14,15,16,1000}")
TEST_METHOD_PROPERTY(L"Data:y", L"{-1000,0,9,10,11,19,20,21,1000}")
END_TEST_METHOD_PROPERTIES()
ptrdiff_t x, y;
VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"x", x));
VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"y", y));
const til::rectangle rc{ 5, 10, 15, 20 };
const til::point pt{ x, y };
const bool xInBounds = x >= 5 && x < 15;
const bool yInBounds = y >= 10 && y < 20;
const bool expected = xInBounds && yInBounds;
if (expected)
{
Log::Comment(L"Expected in bounds.");
}
else
{
Log::Comment(L"Expected OUT of bounds.");
}
VERIFY_ARE_EQUAL(expected, rc.contains(pt));
}
TEST_METHOD(ContainsIndex)
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:idx", L"{-1000,-1,0, 1,50,99,100,101, 1000}")
END_TEST_METHOD_PROPERTIES()
ptrdiff_t idx;
VERIFY_SUCCEEDED_RETURN(TestData::TryGetValue(L"idx", idx));
const til::rectangle rc{ 5, 10, 15, 20 }; // 10x10 rectangle.
const ptrdiff_t area = (15 - 5) * (20 - 10);
const bool expected = idx >= 0 && idx < area;
if (expected)
{
Log::Comment(L"Expected in bounds.");
}
else
{
Log::Comment(L"Expected OUT of bounds.");
}
VERIFY_ARE_EQUAL(expected, rc.contains(idx));
}
TEST_METHOD(ContainsRectangle)
{
const til::rectangle rc{ 5, 10, 15, 20 }; // 10x10 rectangle.
const til::rectangle fitsInside{ 8, 12, 10, 18 };
const til::rectangle spillsOut{ 0, 0, 50, 50 };
const til::rectangle sticksOut{ 14, 12, 30, 13 };
VERIFY_IS_TRUE(rc.contains(rc), L"We contain ourself.");
VERIFY_IS_TRUE(rc.contains(fitsInside), L"We fully contain a smaller rectangle.");
VERIFY_IS_FALSE(rc.contains(spillsOut), L"We do not fully contain rectangle larger than us.");
VERIFY_IS_FALSE(rc.contains(sticksOut), L"We do not contain a rectangle that is smaller, but sticks out our edge.");
}
TEST_METHOD(IndexOfPoint)
{
const til::rectangle rc{ 5, 10, 15, 20 };
Log::Comment(L"0.) Normal in bounds.");
{
const til::point pt{ 7, 17 };
const ptrdiff_t expected = 72;
VERIFY_ARE_EQUAL(expected, rc.index_of(pt));
}
Log::Comment(L"1.) Out of bounds.");
{
auto fn = [&]() {
const til::point pt{ 1, 1 };
rc.index_of(pt);
};
VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; });
}
Log::Comment(L"2.) Overflow.");
{
auto fn = [&]() {
constexpr const ptrdiff_t min = static_cast<ptrdiff_t>(0);
constexpr const ptrdiff_t max = std::numeric_limits<ptrdiff_t>().max();
const til::rectangle bigRc{ min, min, max, max };
const til::point pt{ max - 1, max - 1 };
bigRc.index_of(pt);
};
VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; });
}
}
TEST_METHOD(PointAtIndex)
{
const til::rectangle rc{ 5, 10, 15, 20 };
Log::Comment(L"0.) Normal in bounds.");
{
const ptrdiff_t index = 72;
const til::point expected{ 7, 17 };
VERIFY_ARE_EQUAL(expected, rc.point_at(index));
}
Log::Comment(L"1.) Out of bounds too low.");
{
auto fn = [&]() {
const ptrdiff_t index = -1;
rc.point_at(index);
};
VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; });
}
Log::Comment(L"2.) Out of bounds too high.");
{
auto fn = [&]() {
const ptrdiff_t index = 1000;
rc.point_at(index);
};
VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_INVALIDARG; });
}
}
TEST_METHOD(CastToSmallRect)
{
Log::Comment(L"0.) Typical situation.");
@ -929,4 +1064,89 @@ class RectangleTests
// All ptrdiff_ts fit into a float, so there's no exception tests.
}
#pragma region iterator
TEST_METHOD(Begin)
{
const til::rectangle rc{ 5, 10, 15, 20 };
const til::point expected{ rc.left(), rc.top() };
const auto it = rc.begin();
VERIFY_ARE_EQUAL(expected, *it);
}
TEST_METHOD(End)
{
const til::rectangle rc{ 5, 10, 15, 20 };
const til::point expected{ rc.left(), rc.bottom() };
const auto it = rc.end();
VERIFY_ARE_EQUAL(expected, *it);
}
TEST_METHOD(ConstIteratorIncrement)
{
const til::rectangle rc{ til::size{ 2, 2 } };
auto it = rc.begin();
auto expected = til::point{ 0, 0 };
VERIFY_ARE_EQUAL(expected, *it);
++it;
expected = til::point{ 1, 0 };
VERIFY_ARE_EQUAL(expected, *it);
++it;
expected = til::point{ 0, 1 };
VERIFY_ARE_EQUAL(expected, *it);
++it;
expected = til::point{ 1, 1 };
VERIFY_ARE_EQUAL(expected, *it);
++it;
expected = til::point{ 0, 2 };
VERIFY_ARE_EQUAL(expected, *it);
VERIFY_ARE_EQUAL(expected, *rc.end());
// We wouldn't normally walk one past, but validate it keeps going
// like any STL iterator would.
++it;
expected = til::point{ 1, 2 };
VERIFY_ARE_EQUAL(expected, *it);
}
TEST_METHOD(ConstIteratorEquality)
{
const til::rectangle rc{ 5, 10, 15, 20 };
VERIFY_IS_TRUE(rc.begin() == rc.begin());
VERIFY_IS_FALSE(rc.begin() == rc.end());
}
TEST_METHOD(ConstIteratorInequality)
{
const til::rectangle rc{ 5, 10, 15, 20 };
VERIFY_IS_FALSE(rc.begin() != rc.begin());
VERIFY_IS_TRUE(rc.begin() != rc.end());
}
TEST_METHOD(ConstIteratorLessThan)
{
const til::rectangle rc{ 5, 10, 15, 20 };
VERIFY_IS_TRUE(rc.begin() < rc.end());
VERIFY_IS_FALSE(rc.end() < rc.begin());
}
TEST_METHOD(ConstIteratorGreaterThan)
{
const til::rectangle rc{ 5, 10, 15, 20 };
VERIFY_IS_TRUE(rc.end() > rc.begin());
VERIFY_IS_FALSE(rc.begin() > rc.end());
}
#pragma endregion
};

View file

@ -166,6 +166,40 @@ class SomeTests
VERIFY_IS_TRUE(s.empty());
}
TEST_METHOD(Clear)
{
til::some<int, 2> s;
VERIFY_IS_TRUE(s.empty());
s.push_back(12);
VERIFY_IS_FALSE(s.empty());
VERIFY_ARE_EQUAL(1u, s.size());
s.clear();
VERIFY_IS_TRUE(s.empty());
VERIFY_ARE_EQUAL(0u, s.size());
}
TEST_METHOD(ClearFreesMembers)
{
til::some<std::shared_ptr<int>, 2> s;
auto a = std::make_shared<int>(4);
auto weakA = std::weak_ptr<int>(a);
auto b = std::make_shared<int>(6);
auto weakB = std::weak_ptr<int>(b);
s.push_back(std::move(a));
s.push_back(std::move(b));
VERIFY_IS_FALSE(weakA.expired());
VERIFY_IS_FALSE(weakB.expired());
s.clear();
VERIFY_IS_TRUE(weakA.expired());
VERIFY_IS_TRUE(weakB.expired());
}
TEST_METHOD(Data)
{
til::some<int, 2> s;

View file

@ -10,6 +10,7 @@
</PropertyGroup>
<Import Project="$(SolutionDir)src\common.build.pre.props" />
<ItemGroup>
<ClCompile Include="BitmapTests.cpp" />
<ClCompile Include="PointTests.cpp" />
<ClCompile Include="RectangleTests.cpp" />
<ClCompile Include="SizeTests.cpp" />

View file

@ -11,6 +11,7 @@
<ClCompile Include="ColorTests.cpp" />
<ClCompile Include="PointTests.cpp" />
<ClCompile Include="RectangleTests.cpp" />
<ClCompile Include="BitmapTests.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\precomp.h" />