diff --git a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp index dae8a248f77..981be1c4b20 100644 --- a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp +++ b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp @@ -347,17 +347,14 @@ void ConptyRoundtripTests::WriteAFewSimpleLines() expectedOutput.push_back("AAA"); expectedOutput.push_back("\r\n"); expectedOutput.push_back("BBB"); - expectedOutput.push_back("\r\n"); - // Here, we're going to emit 3 spaces. The region that got invalidated was a - // rectangle from 0,0 to 3,3, so the vt renderer will try to render the - // region in between BBB and CCC as well, because it got included in the - // rectangle Or() operation. - // This behavior should not be seen as binding - if a future optimization - // breaks this test, it wouldn't be the worst. - expectedOutput.push_back(" "); - expectedOutput.push_back("\r\n"); + // Jump down to the fourth line because emitting spaces didn't do anything + // and we will skip to emitting the CCC segment. + expectedOutput.push_back("\x1b[4;1H"); expectedOutput.push_back("CCC"); + // Cursor goes back on. + expectedOutput.push_back("\x1b[?25h"); + VERIFY_SUCCEEDED(renderer.PaintFrame()); verifyData(termTb); @@ -458,14 +455,10 @@ void ConptyRoundtripTests::TestAdvancedWrapping() expectedOutput.push_back(R"(!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnop)"); // Without line breaking, write the remaining 20 chars expectedOutput.push_back(R"(qrstuvwxyz{|}~!"#$%&)"); - // Clear the rest of row 1 - expectedOutput.push_back("\x1b[K"); // This is the hard line break expectedOutput.push_back("\r\n"); // Now write row 2 of the buffer expectedOutput.push_back(" 1234567890"); - // and clear everything after the text, because the buffer is empty. - expectedOutput.push_back("\x1b[K"); VERIFY_SUCCEEDED(renderer.PaintFrame()); verifyBuffer(termTb); @@ -537,8 +530,6 @@ void ConptyRoundtripTests::TestExactWrappingWithoutSpaces() expectedOutput.push_back("\r\n"); // Now write row 2 of the buffer expectedOutput.push_back("1234567890"); - // and clear everything after the text, because the buffer is empty. - expectedOutput.push_back("\x1b[K"); VERIFY_SUCCEEDED(renderer.PaintFrame()); verifyBuffer(termTb); @@ -601,8 +592,6 @@ void ConptyRoundtripTests::TestExactWrappingWithSpaces() expectedOutput.push_back("\r\n"); // Now write row 2 of the buffer expectedOutput.push_back(" 1234567890"); - // and clear everything after the text, because the buffer is empty. - expectedOutput.push_back("\x1b[K"); VERIFY_SUCCEEDED(renderer.PaintFrame()); verifyBuffer(termTb); diff --git a/src/common.build.tests.props b/src/common.build.tests.props index 8a3527ff6e6..79410878836 100644 --- a/src/common.build.tests.props +++ b/src/common.build.tests.props @@ -2,7 +2,7 @@ - UNIT_TESTING;%(PreprocessorDefinitions) + INLINE_TEST_METHOD_MARKUP;UNIT_TESTING;%(PreprocessorDefinitions) diff --git a/src/host/ut_host/ConptyOutputTests.cpp b/src/host/ut_host/ConptyOutputTests.cpp index c24ec99fa15..07493ab4544 100644 --- a/src/host/ut_host/ConptyOutputTests.cpp +++ b/src/host/ut_host/ConptyOutputTests.cpp @@ -306,17 +306,14 @@ void ConptyOutputTests::WriteAFewSimpleLines() expectedOutput.push_back("AAA"); expectedOutput.push_back("\r\n"); expectedOutput.push_back("BBB"); - expectedOutput.push_back("\r\n"); - // Here, we're going to emit 3 spaces. The region that got invalidated was a - // rectangle from 0,0 to 3,3, so the vt renderer will try to render the - // region in between BBB and CCC as well, because it got included in the - // rectangle Or() operation. - // This behavior should not be seen as binding - if a future optimization - // breaks this test, it wouldn't be the worst. - expectedOutput.push_back(" "); - expectedOutput.push_back("\r\n"); + // Jump down to the fourth line because emitting spaces didn't do anything + // and we will skip to emitting the CCC segment. + expectedOutput.push_back("\x1b[4;1H"); expectedOutput.push_back("CCC"); + // Cursor goes back on. + expectedOutput.push_back("\x1b[?25h"); + VERIFY_SUCCEEDED(renderer.PaintFrame()); } diff --git a/src/host/ut_host/VtRendererTests.cpp b/src/host/ut_host/VtRendererTests.cpp index 92410495822..4be9db4b85a 100644 --- a/src/host/ut_host/VtRendererTests.cpp +++ b/src/host/ut_host/VtRendererTests.cpp @@ -257,21 +257,22 @@ void VtRendererTest::Xterm256TestInvalidate() VERIFY_IS_FALSE(engine->_firstPaint); }); - Viewport view = SetUpViewport(); + const Viewport view = SetUpViewport(); Log::Comment(NoThrowString().Format( L"Make sure that invalidating all invalidates the whole viewport.")); VERIFY_SUCCEEDED(engine->InvalidateAll()); TestPaint(*engine, [&]() { - VERIFY_ARE_EQUAL(view, engine->_invalidRect); + VERIFY_IS_TRUE(engine->_invalidMap.all()); }); Log::Comment(NoThrowString().Format( L"Make sure that invalidating anything only invalidates that portion")); - SMALL_RECT invalid = { 1, 1, 1, 1 }; + SMALL_RECT invalid = { 1, 1, 2, 2 }; VERIFY_SUCCEEDED(engine->Invalidate(&invalid)); TestPaint(*engine, [&]() { - VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + VERIFY_IS_TRUE(engine->_invalidMap.one()); + VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, *(engine->_invalidMap.begin())); }); Log::Comment(NoThrowString().Format( @@ -284,7 +285,9 @@ void VtRendererTest::Xterm256TestInvalidate() invalid = view.ToExclusive(); invalid.Bottom = 1; - VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + const auto runs = engine->_invalidMap.runs(); + VERIFY_ARE_EQUAL(1u, runs.size()); + VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, runs.front()); qExpectedInput.push_back("\x1b[H"); // Go Home qExpectedInput.push_back("\x1b[L"); // insert a line @@ -300,7 +303,17 @@ void VtRendererTest::Xterm256TestInvalidate() invalid = view.ToExclusive(); invalid.Bottom = 3; - VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + // we should have 3 runs and build a rectangle out of them + const auto runs = engine->_invalidMap.runs(); + VERIFY_ARE_EQUAL(3u, runs.size()); + auto invalidRect = runs.front(); + for (size_t i = 1; i < runs.size(); ++i) + { + invalidRect |= runs[i]; + } + + // verify the rect matches the invalid one. + VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, invalidRect); // We would expect a CUP here, but the cursor is already at the home position qExpectedInput.push_back("\x1b[3L"); // insert 3 lines VERIFY_SUCCEEDED(engine->ScrollFrame()); @@ -314,7 +327,9 @@ void VtRendererTest::Xterm256TestInvalidate() invalid = view.ToExclusive(); invalid.Top = invalid.Bottom - 1; - VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + const auto runs = engine->_invalidMap.runs(); + VERIFY_ARE_EQUAL(1u, runs.size()); + VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, runs.front()); qExpectedInput.push_back("\x1b[32;1H"); // Bottom of buffer qExpectedInput.push_back("\n"); // Scroll down once @@ -329,7 +344,17 @@ void VtRendererTest::Xterm256TestInvalidate() invalid = view.ToExclusive(); invalid.Top = invalid.Bottom - 3; - VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + // we should have 3 runs and build a rectangle out of them + const auto runs = engine->_invalidMap.runs(); + VERIFY_ARE_EQUAL(3u, runs.size()); + auto invalidRect = runs.front(); + for (size_t i = 1; i < runs.size(); ++i) + { + invalidRect |= runs[i]; + } + + // verify the rect matches the invalid one. + VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, invalidRect); // We would expect a CUP here, but we're already at the bottom from the last call. qExpectedInput.push_back("\n\n\n"); // Scroll down three times @@ -349,7 +374,18 @@ void VtRendererTest::Xterm256TestInvalidate() invalid = view.ToExclusive(); invalid.Bottom = 3; - VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + // we should have 3 runs and build a rectangle out of them + const auto runs = engine->_invalidMap.runs(); + VERIFY_ARE_EQUAL(3u, runs.size()); + auto invalidRect = runs.front(); + for (size_t i = 1; i < runs.size(); ++i) + { + invalidRect |= runs[i]; + } + + // verify the rect matches the invalid one. + VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, invalidRect); + qExpectedInput.push_back("\x1b[H"); // Go to home qExpectedInput.push_back("\x1b[3L"); // insert 3 lines VERIFY_SUCCEEDED(engine->ScrollFrame()); @@ -357,21 +393,39 @@ void VtRendererTest::Xterm256TestInvalidate() scrollDelta = { 0, 1 }; VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); - Log::Comment(NoThrowString().Format( - VerifyOutputTraits::ToString(engine->_invalidRect.ToExclusive()))); + Log::Comment(engine->_invalidMap.to_string().c_str()); scrollDelta = { 0, -1 }; VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); - Log::Comment(NoThrowString().Format( - VerifyOutputTraits::ToString(engine->_invalidRect.ToExclusive()))); + Log::Comment(engine->_invalidMap.to_string().c_str()); qExpectedInput.push_back("\x1b[2J"); TestPaint(*engine, [&]() { Log::Comment(NoThrowString().Format( L"---- Scrolled one down and one up, nothing should change ----" L" But it still does for now MSFT:14169294")); - invalid = view.ToExclusive(); - VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + + const auto runs = engine->_invalidMap.runs(); + auto invalidRect = runs.front(); + for (size_t i = 1; i < runs.size(); ++i) + { + invalidRect |= runs[i]; + } + + // only the bottom line should be dirty. + // When we scrolled down, the bitmap looked like this: + // 1111 + // 0000 + // 0000 + // 0000 + // And then we scrolled up and the top line fell off and a bottom + // line was filled in like this: + // 0000 + // 0000 + // 0000 + // 1111 + const til::rectangle expected{ til::point{ view.Left(), view.BottomInclusive() }, til::size{ view.Width(), 1 } }; + VERIFY_ARE_EQUAL(expected, invalidRect); VERIFY_SUCCEEDED(engine->ScrollFrame()); }); @@ -730,15 +784,16 @@ void VtRendererTest::XtermTestInvalidate() L"Make sure that invalidating all invalidates the whole viewport.")); VERIFY_SUCCEEDED(engine->InvalidateAll()); TestPaint(*engine, [&]() { - VERIFY_ARE_EQUAL(view, engine->_invalidRect); + VERIFY_IS_TRUE(engine->_invalidMap.all()); }); Log::Comment(NoThrowString().Format( L"Make sure that invalidating anything only invalidates that portion")); - SMALL_RECT invalid = { 1, 1, 1, 1 }; + SMALL_RECT invalid = { 1, 1, 2, 2 }; VERIFY_SUCCEEDED(engine->Invalidate(&invalid)); TestPaint(*engine, [&]() { - VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + VERIFY_IS_TRUE(engine->_invalidMap.one()); + VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, *(engine->_invalidMap.begin())); }); Log::Comment(NoThrowString().Format( @@ -751,7 +806,9 @@ void VtRendererTest::XtermTestInvalidate() invalid = view.ToExclusive(); invalid.Bottom = 1; - VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + const auto runs = engine->_invalidMap.runs(); + VERIFY_ARE_EQUAL(1u, runs.size()); + VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, runs.front()); qExpectedInput.push_back("\x1b[H"); // Go Home qExpectedInput.push_back("\x1b[L"); // insert a line @@ -766,7 +823,17 @@ void VtRendererTest::XtermTestInvalidate() invalid = view.ToExclusive(); invalid.Bottom = 3; - VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + // we should have 3 runs and build a rectangle out of them + const auto runs = engine->_invalidMap.runs(); + VERIFY_ARE_EQUAL(3u, runs.size()); + auto invalidRect = runs.front(); + for (size_t i = 1; i < runs.size(); ++i) + { + invalidRect |= runs[i]; + } + + // verify the rect matches the invalid one. + VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, invalidRect); // We would expect a CUP here, but the cursor is already at the home position qExpectedInput.push_back("\x1b[3L"); // insert 3 lines VERIFY_SUCCEEDED(engine->ScrollFrame()); @@ -780,7 +847,9 @@ void VtRendererTest::XtermTestInvalidate() invalid = view.ToExclusive(); invalid.Top = invalid.Bottom - 1; - VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + const auto runs = engine->_invalidMap.runs(); + VERIFY_ARE_EQUAL(1u, runs.size()); + VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, runs.front()); qExpectedInput.push_back("\x1b[32;1H"); // Bottom of buffer qExpectedInput.push_back("\n"); // Scroll down once @@ -795,7 +864,17 @@ void VtRendererTest::XtermTestInvalidate() invalid = view.ToExclusive(); invalid.Top = invalid.Bottom - 3; - VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + // we should have 3 runs and build a rectangle out of them + const auto runs = engine->_invalidMap.runs(); + VERIFY_ARE_EQUAL(3u, runs.size()); + auto invalidRect = runs.front(); + for (size_t i = 1; i < runs.size(); ++i) + { + invalidRect |= runs[i]; + } + + // verify the rect matches the invalid one. + VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, invalidRect); // We would expect a CUP here, but we're already at the bottom from the last call. qExpectedInput.push_back("\n\n\n"); // Scroll down three times @@ -815,7 +894,18 @@ void VtRendererTest::XtermTestInvalidate() invalid = view.ToExclusive(); invalid.Bottom = 3; - VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + // we should have 3 runs and build a rectangle out of them + const auto runs = engine->_invalidMap.runs(); + VERIFY_ARE_EQUAL(3u, runs.size()); + auto invalidRect = runs.front(); + for (size_t i = 1; i < runs.size(); ++i) + { + invalidRect |= runs[i]; + } + + // verify the rect matches the invalid one. + VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, invalidRect); + qExpectedInput.push_back("\x1b[H"); // Go to home qExpectedInput.push_back("\x1b[3L"); // insert 3 lines VERIFY_SUCCEEDED(engine->ScrollFrame()); @@ -823,21 +913,39 @@ void VtRendererTest::XtermTestInvalidate() scrollDelta = { 0, 1 }; VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); - Log::Comment(NoThrowString().Format( - VerifyOutputTraits::ToString(engine->_invalidRect.ToExclusive()))); + Log::Comment(engine->_invalidMap.to_string().c_str()); scrollDelta = { 0, -1 }; VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); - Log::Comment(NoThrowString().Format( - VerifyOutputTraits::ToString(engine->_invalidRect.ToExclusive()))); + Log::Comment(engine->_invalidMap.to_string().c_str()); qExpectedInput.push_back("\x1b[2J"); TestPaint(*engine, [&]() { Log::Comment(NoThrowString().Format( L"---- Scrolled one down and one up, nothing should change ----" L" But it still does for now MSFT:14169294")); - invalid = view.ToExclusive(); - VERIFY_ARE_EQUAL(view, engine->_invalidRect); + + const auto runs = engine->_invalidMap.runs(); + auto invalidRect = runs.front(); + for (size_t i = 1; i < runs.size(); ++i) + { + invalidRect |= runs[i]; + } + + // only the bottom line should be dirty. + // When we scrolled down, the bitmap looked like this: + // 1111 + // 0000 + // 0000 + // 0000 + // And then we scrolled up and the top line fell off and a bottom + // line was filled in like this: + // 0000 + // 0000 + // 0000 + // 1111 + const til::rectangle expected{ til::point{ view.Left(), view.BottomInclusive() }, til::size{ view.Width(), 1 } }; + VERIFY_ARE_EQUAL(expected, invalidRect); VERIFY_SUCCEEDED(engine->ScrollFrame()); }); @@ -1051,15 +1159,15 @@ void VtRendererTest::WinTelnetTestInvalidate() L"Make sure that invalidating all invalidates the whole viewport.")); VERIFY_SUCCEEDED(engine->InvalidateAll()); TestPaint(*engine, [&]() { - VERIFY_ARE_EQUAL(view, engine->_invalidRect); + VERIFY_IS_TRUE(engine->_invalidMap.all()); }); Log::Comment(NoThrowString().Format( L"Make sure that invalidating anything only invalidates that portion")); - SMALL_RECT invalid = { 1, 1, 1, 1 }; + SMALL_RECT invalid = { 1, 1, 2, 2 }; VERIFY_SUCCEEDED(engine->Invalidate(&invalid)); TestPaint(*engine, [&]() { - VERIFY_ARE_EQUAL(invalid, engine->_invalidRect.ToExclusive()); + VERIFY_ARE_EQUAL(til::rectangle{ Viewport::FromExclusive(invalid).ToInclusive() }, *(engine->_invalidMap.begin())); }); Log::Comment(NoThrowString().Format( @@ -1067,7 +1175,7 @@ void VtRendererTest::WinTelnetTestInvalidate() COORD scrollDelta = { 0, 1 }; VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); TestPaint(*engine, [&]() { - VERIFY_ARE_EQUAL(view, engine->_invalidRect); + VERIFY_IS_TRUE(engine->_invalidMap.all()); qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); // sentinel VERIFY_SUCCEEDED(engine->ScrollFrame()); WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback @@ -1076,7 +1184,7 @@ void VtRendererTest::WinTelnetTestInvalidate() scrollDelta = { 0, -1 }; VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); TestPaint(*engine, [&]() { - VERIFY_ARE_EQUAL(view, engine->_invalidRect); + VERIFY_IS_TRUE(engine->_invalidMap.all()); qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); VERIFY_SUCCEEDED(engine->ScrollFrame()); WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback @@ -1085,7 +1193,7 @@ void VtRendererTest::WinTelnetTestInvalidate() scrollDelta = { 1, 0 }; VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); TestPaint(*engine, [&]() { - VERIFY_ARE_EQUAL(view, engine->_invalidRect); + VERIFY_IS_TRUE(engine->_invalidMap.all()); qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); VERIFY_SUCCEEDED(engine->ScrollFrame()); WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback @@ -1094,7 +1202,7 @@ void VtRendererTest::WinTelnetTestInvalidate() scrollDelta = { -1, 0 }; VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); TestPaint(*engine, [&]() { - VERIFY_ARE_EQUAL(view, engine->_invalidRect); + VERIFY_IS_TRUE(engine->_invalidMap.all()); qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); VERIFY_SUCCEEDED(engine->ScrollFrame()); WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback @@ -1103,7 +1211,7 @@ void VtRendererTest::WinTelnetTestInvalidate() scrollDelta = { 1, -1 }; VERIFY_SUCCEEDED(engine->InvalidateScroll(&scrollDelta)); TestPaint(*engine, [&]() { - VERIFY_ARE_EQUAL(view, engine->_invalidRect); + VERIFY_IS_TRUE(engine->_invalidMap.all()); qExpectedInput.push_back(EMPTY_CALLBACK_SENTINEL); VERIFY_SUCCEEDED(engine->ScrollFrame()); WriteCallback(EMPTY_CALLBACK_SENTINEL, 1); // This will make sure nothing was written to the callback @@ -1351,7 +1459,7 @@ void VtRendererTest::TestResize() VERIFY_SUCCEEDED(engine->UpdateViewport(newView.ToInclusive())); TestPaint(*engine, [&]() { - VERIFY_ARE_EQUAL(newView, engine->_invalidRect); + VERIFY_IS_TRUE(engine->_invalidMap.all()); VERIFY_IS_FALSE(engine->_firstPaint); VERIFY_IS_FALSE(engine->_suppressResizeRepaint); }); diff --git a/src/inc/LibraryIncludes.h b/src/inc/LibraryIncludes.h index 2002fa48fb0..92b572075e3 100644 --- a/src/inc/LibraryIncludes.h +++ b/src/inc/LibraryIncludes.h @@ -85,6 +85,12 @@ // WRL #include +// WEX/TAEF testing +// Include before TIL if we're unit testing so it can light up WEX/TAEF template extensions +#ifdef UNIT_TESTING +#include +#endif + // TIL - Terminal Implementation Library #ifndef BLOCK_TIL // Certain projects may want to include TIL manually to gain superpowers #include "til.h" diff --git a/src/inc/til.h b/src/inc/til.h index 6cbe6a0b128..5204284a004 100644 --- a/src/inc/til.h +++ b/src/inc/til.h @@ -3,12 +3,15 @@ #pragma once +#define _TIL_INLINEPREFIX __declspec(noinline) inline + #include "til/at.h" #include "til/color.h" #include "til/some.h" #include "til/size.h" #include "til/point.h" #include "til/rectangle.h" +#include "til/operators.h" #include "til/bitmap.h" #include "til/u8u16convert.h" diff --git a/src/inc/til/bitmap.h b/src/inc/til/bitmap.h index 322956ed849..2070ee7a643 100644 --- a/src/inc/til/bitmap.h +++ b/src/inc/til/bitmap.h @@ -14,6 +14,12 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" class _bitmap_const_iterator { public: + using iterator_category = typename std::input_iterator_tag; + using value_type = typename const til::rectangle; + using difference_type = typename ptrdiff_t; + using pointer = typename const til::rectangle*; + using reference = typename const til::rectangle&; + _bitmap_const_iterator(const std::vector& values, til::rectangle rc, ptrdiff_t pos) : _values(values), _rc(rc), @@ -30,6 +36,13 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" return (*this); } + _bitmap_const_iterator operator++(int) + { + const auto prev = *this; + ++*this; + return prev; + } + constexpr bool operator==(const _bitmap_const_iterator& other) const noexcept { return _pos == other._pos && _values == other._values; @@ -50,11 +63,16 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" return _pos > other._pos; } - constexpr til::rectangle operator*() const noexcept + constexpr reference operator*() const noexcept { return _run; } + constexpr pointer operator->() const noexcept + { + return &_run; + } + private: const std::vector& _values; const til::rectangle _rc; @@ -117,7 +135,8 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" _sz{}, _rc{}, _bits{}, - _dirty{} + _dirty{}, + _runs{} { } @@ -130,8 +149,23 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" _sz(sz), _rc(sz), _bits(sz.area(), fill), - _dirty(fill ? sz : til::rectangle{}) + _dirty(fill ? sz : til::rectangle{}), + _runs{} + { + } + + constexpr bool operator==(const bitmap& other) const noexcept { + return _sz == other._sz && + _rc == other._rc && + _dirty == other._dirty && // dirty is before bits because it's a rough estimate of bits and a faster comparison. + _bits == other._bits; + // _runs excluded because it's a cache of generated state. + } + + constexpr bool operator!=(const bitmap& other) const noexcept + { + return !(*this == other); } const_iterator begin() const @@ -144,17 +178,106 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" return const_iterator(_bits, _sz, _sz.area()); } + const std::vector& runs() const + { + // If we don't have cached runs, rebuild. + if (!_runs.has_value()) + { + // If there's only one square dirty, quick save it off and be done. + if (one()) + { + _runs.emplace({ _dirty }); + } + else + { + _runs.emplace(begin(), end()); + } + } + + // Return a reference to the runs. + return _runs.value(); + } + + // optional fill the uncovered area with bits. + void translate(const til::point delta, bool fill = false) + { + // FUTURE: PERF: GH #4015: This could use in-place walk semantics instead of a temporary. + til::bitmap other{ _sz }; + + for (auto run : *this) + { + // Offset by the delta + run += delta; + + // Intersect with the bounds of our bitmap area + // as part of it could have slid out of bounds. + run &= _rc; + + // Set it into the new bitmap. + other.set(run); + } + + // If we were asked to fill... find the uncovered region. + if (fill) + { + // Original Rect of As. + // + // X <-- origin + // A A A A + // A A A A + // A A A A + // A A A A + const auto originalRect = _rc; + + // If Delta = (2, 2) + // Translated Rect of Bs. + // + // X <-- origin + // + // + // B B B B + // B B B B + // B B B B + // B B B B + const auto translatedRect = _rc + delta; + + // Subtract the B from the A one to see what wasn't filled by the move. + // C is the overlap of A and B: + // + // X <-- origin + // A A A A 1 1 1 1 + // A A A A 1 1 1 1 + // A A C C B B subtract 2 2 + // A A C C B B ---------> 2 2 + // B B B B A - B + // B B B B + // + // 1 and 2 are the spaces to fill that are "uncovered". + const auto fillRects = originalRect - translatedRect; + for (const auto& f : fillRects) + { + other.set(f); + } + } + + // Swap us with the temporary one. + std::swap(other, *this); + } + void set(const til::point pt) { THROW_HR_IF(E_INVALIDARG, !_rc.contains(pt)); + _runs.reset(); // reset cached runs on any non-const method 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)); + _runs.reset(); // reset cached runs on any non-const method for (const auto pt : rc) { @@ -166,6 +289,8 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" void set_all() { + _runs.reset(); // reset cached runs on any non-const method + // .clear() then .resize(_size(), true) throws an assert (unsupported operation) // .assign(_size(), true) throws an assert (unsupported operation) _bits = std::vector(_sz.area(), true); @@ -174,6 +299,8 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" void reset_all() { + _runs.reset(); // reset cached runs on any non-const method + // .clear() then .resize(_size(), false) throws an assert (unsupported operation) // .assign(_size(), false) throws an assert (unsupported operation) @@ -185,6 +312,8 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" // Set fill if you want the new region (on growing) to be marked dirty. bool resize(til::size size, bool fill = false) { + _runs.reset(); // reset cached runs on any non-const method + // FYI .resize(_size(), true/false) throws an assert (unsupported operation) // Don't resize if it's not different @@ -254,14 +383,71 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" return _dirty == _rc; } + std::wstring to_string() const + { + std::wstringstream wss; + wss << std::endl + << L"Bitmap of size " << _sz.to_string() << " contains the following dirty regions:" << std::endl; + wss << L"Runs:" << std::endl; + + for (auto& item : *this) + { + wss << L"\t- " << item.to_string() << std::endl; + } + + return wss.str(); + } + private: til::rectangle _dirty; til::size _sz; til::rectangle _rc; std::vector _bits; + mutable std::optional> _runs; + #ifdef UNIT_TESTING friend class ::BitmapTests; #endif }; } + +#ifdef __WEX_COMMON_H__ +namespace WEX::TestExecution +{ + template<> + class VerifyOutputTraits<::til::bitmap> + { + public: + static WEX::Common::NoThrowString ToString(const ::til::bitmap& rect) + { + return WEX::Common::NoThrowString(rect.to_string().c_str()); + } + }; + + template<> + class VerifyCompareTraits<::til::bitmap, ::til::bitmap> + { + public: + static bool AreEqual(const ::til::bitmap& expected, const ::til::bitmap& actual) noexcept + { + return expected == actual; + } + + static bool AreSame(const ::til::bitmap& expected, const ::til::bitmap& actual) noexcept + { + return &expected == &actual; + } + + static bool IsLessThan(const ::til::bitmap& expectedLess, const ::til::bitmap& expectedGreater) = delete; + + static bool IsGreaterThan(const ::til::bitmap& expectedGreater, const ::til::bitmap& expectedLess) = delete; + + static bool IsNull(const ::til::bitmap& object) noexcept + { + return object == til::bitmap{}; + } + }; + +}; +#endif diff --git a/src/inc/til/operators.h b/src/inc/til/operators.h new file mode 100644 index 00000000000..109cd3e3576 --- /dev/null +++ b/src/inc/til/operators.h @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#include "rectangle.h" +#include "size.h" +#include "bitmap.h" + +namespace til // Terminal Implementation Library. Also: "Today I Learned" +{ + // Operators go here when they involve two headers that can't/don't include each other. + +#pragma region POINT VS SIZE + // This is a convenience and will take X vs WIDTH and Y vs HEIGHT. + _TIL_INLINEPREFIX point operator+(const point& lhs, const size& rhs) + { + return lhs + til::point{ rhs.width(), rhs.height() }; + } + + _TIL_INLINEPREFIX point operator-(const point& lhs, const size& rhs) + { + return lhs - til::point{ rhs.width(), rhs.height() }; + } + + _TIL_INLINEPREFIX point operator*(const point& lhs, const size& rhs) + { + return lhs * til::point{ rhs.width(), rhs.height() }; + } + + _TIL_INLINEPREFIX point operator/(const point& lhs, const size& rhs) + { + return lhs / til::point{ rhs.width(), rhs.height() }; + } +#pragma endregion + +#pragma region SIZE VS POINT + // This is a convenience and will take WIDTH vs X and HEIGHT vs Y. + _TIL_INLINEPREFIX size operator+(const size& lhs, const point& rhs) + { + return lhs + til::size(rhs.x(), rhs.y()); + } + + _TIL_INLINEPREFIX size operator-(const size& lhs, const point& rhs) + { + return lhs - til::size(rhs.x(), rhs.y()); + } + + _TIL_INLINEPREFIX size operator*(const size& lhs, const point& rhs) + { + return lhs * til::size(rhs.x(), rhs.y()); + } + + _TIL_INLINEPREFIX size operator/(const size& lhs, const point& rhs) + { + return lhs / til::size(rhs.x(), rhs.y()); + } +#pragma endregion +} diff --git a/src/inc/til/point.h b/src/inc/til/point.h index 88ff6bdfd96..c7643b05286 100644 --- a/src/inc/til/point.h +++ b/src/inc/til/point.h @@ -193,6 +193,11 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" } #endif + std::wstring to_string() const + { + return wil::str_printf(L"(X:%td, Y:%td)", x(), y()); + } + protected: ptrdiff_t _x; ptrdiff_t _y; @@ -212,7 +217,7 @@ namespace WEX::TestExecution public: static WEX::Common::NoThrowString ToString(const ::til::point& point) { - return WEX::Common::NoThrowString().Format(L"(X:%td, Y:%td)", point.x(), point.y()); + return WEX::Common::NoThrowString(point.to_string().c_str()); } }; diff --git a/src/inc/til/rectangle.h b/src/inc/til/rectangle.h index 853e77ed484..19f684f589f 100644 --- a/src/inc/til/rectangle.h +++ b/src/inc/til/rectangle.h @@ -212,6 +212,7 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" return const_iterator(_topLeft, _bottomRight, { _topLeft.x(), _bottomRight.y() }); } +#pragma region RECTANGLE OPERATORS // OR = union constexpr rectangle operator|(const rectangle& other) const noexcept { @@ -274,6 +275,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; + } + // - = subtract some operator-(const rectangle& other) const { @@ -405,6 +412,231 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" return result; } +#pragma endregion + +#pragma region RECTANGLE VS POINT + // ADD will translate (offset) the rectangle by the point. + rectangle operator+(const point& point) const + { + ptrdiff_t l, t, r, b; + + THROW_HR_IF(E_ABORT, !::base::CheckAdd(left(), point.x()).AssignIfValid(&l)); + THROW_HR_IF(E_ABORT, !::base::CheckAdd(top(), point.y()).AssignIfValid(&t)); + THROW_HR_IF(E_ABORT, !::base::CheckAdd(right(), point.x()).AssignIfValid(&r)); + THROW_HR_IF(E_ABORT, !::base::CheckAdd(bottom(), point.y()).AssignIfValid(&b)); + + return til::rectangle{ til::point{ l, t }, til::point{ r, b } }; + } + + rectangle& operator+=(const point& point) + { + *this = *this + point; + return *this; + } + + // SUB will translate (offset) the rectangle by the point. + rectangle operator-(const point& point) const + { + ptrdiff_t l, t, r, b; + + THROW_HR_IF(E_ABORT, !::base::CheckSub(left(), point.x()).AssignIfValid(&l)); + THROW_HR_IF(E_ABORT, !::base::CheckSub(top(), point.y()).AssignIfValid(&t)); + THROW_HR_IF(E_ABORT, !::base::CheckSub(right(), point.x()).AssignIfValid(&r)); + THROW_HR_IF(E_ABORT, !::base::CheckSub(bottom(), point.y()).AssignIfValid(&b)); + + return til::rectangle{ til::point{ l, t }, til::point{ r, b } }; + } + + rectangle& operator-=(const point& point) + { + *this = *this - point; + return *this; + } + +#pragma endregion + +#pragma region RECTANGLE VS SIZE + // ADD will grow the total area of the rectangle. The sign is the direction to grow. + rectangle operator+(const size& size) const + { + // Fetch the pieces of the rectangle. + auto l = left(); + auto r = right(); + auto t = top(); + auto b = bottom(); + + // Fetch the scale factors we're using. + const auto width = size.width(); + const auto height = size.height(); + + // Since this is the add operation versus a size, the result + // should grow the total rectangle area. + // The sign determines which edge of the rectangle moves. + // We use the magnitude as how far to move. + if (width > 0) + { + // Adding the positive makes the rectangle "grow" + // because right stretches outward (to the right). + // + // Example with adding width 3... + // |-- x = origin + // V + // x---------| x------------| + // | | | | + // | | | | + // |---------| |------------| + // BEFORE AFTER + THROW_HR_IF(E_ABORT, !base::CheckAdd(r, width).AssignIfValid(&r)); + } + else + { + // Adding the negative makes the rectangle "grow" + // because left stretches outward (to the left). + // + // Example with adding width -3... + // |-- x = origin + // V + // x---------| |--x---------| + // | | | | + // | | | | + // |---------| |------------| + // BEFORE AFTER + THROW_HR_IF(E_ABORT, !base::CheckAdd(l, width).AssignIfValid(&l)); + } + + if (height > 0) + { + // Adding the positive makes the rectangle "grow" + // because bottom stretches outward (to the down). + // + // Example with adding height 2... + // |-- x = origin + // V + // x---------| x---------| + // | | | | + // | | | | + // |---------| | | + // | | + // |---------| + // BEFORE AFTER + THROW_HR_IF(E_ABORT, !base::CheckAdd(b, height).AssignIfValid(&b)); + } + else + { + // Adding the negative makes the rectangle "grow" + // because top stretches outward (to the up). + // + // Example with adding height -2... + // |-- x = origin + // | + // | |---------| + // V | | + // x---------| x | + // | | | | + // | | | | + // |---------| |---------| + // BEFORE AFTER + THROW_HR_IF(E_ABORT, !base::CheckAdd(t, height).AssignIfValid(&t)); + } + + return rectangle{ til::point{ l, t }, til::point{ r, b } }; + } + + rectangle& operator+=(const size& size) + { + *this = *this + size; + return *this; + } + + // SUB will shrink the total area of the rectangle. The sign is the direction to shrink. + rectangle operator-(const size& size) const + { + // Fetch the pieces of the rectangle. + auto l = left(); + auto r = right(); + auto t = top(); + auto b = bottom(); + + // Fetch the scale factors we're using. + const auto width = size.width(); + const auto height = size.height(); + + // Since this is the subtract operation versus a size, the result + // should shrink the total rectangle area. + // The sign determines which edge of the rectangle moves. + // We use the magnitude as how far to move. + if (width > 0) + { + // Subtracting the positive makes the rectangle "shrink" + // because right pulls inward (to the left). + // + // Example with subtracting width 3... + // |-- x = origin + // V + // x---------| x------| + // | | | | + // | | | | + // |---------| |------| + // BEFORE AFTER + THROW_HR_IF(E_ABORT, !base::CheckSub(r, width).AssignIfValid(&r)); + } + else + { + // Subtracting the negative makes the rectangle "shrink" + // because left pulls inward (to the right). + // + // Example with subtracting width -3... + // |-- x = origin + // V + // x---------| x |------| + // | | | | + // | | | | + // |---------| |------| + // BEFORE AFTER + THROW_HR_IF(E_ABORT, !base::CheckSub(l, width).AssignIfValid(&l)); + } + + if (height > 0) + { + // Subtracting the positive makes the rectangle "shrink" + // because bottom pulls inward (to the up). + // + // Example with subtracting height 2... + // |-- x = origin + // V + // x---------| x---------| + // | | |---------| + // | | + // |---------| + // BEFORE AFTER + THROW_HR_IF(E_ABORT, !base::CheckSub(b, height).AssignIfValid(&b)); + } + else + { + // Subtracting the positive makes the rectangle "shrink" + // because top pulls inward (to the down). + // + // Example with subtracting height -2... + // |-- x = origin + // V + // x---------| x + // | | + // | | |---------| + // |---------| |---------| + // BEFORE AFTER + THROW_HR_IF(E_ABORT, !base::CheckSub(t, height).AssignIfValid(&t)); + } + + return rectangle{ til::point{ l, t }, til::point{ r, b } }; + } + + rectangle& operator-=(const size& size) + { + *this = *this - size; + return *this; + } + +#pragma endregion constexpr ptrdiff_t top() const noexcept { @@ -587,6 +819,11 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" } #endif + std::wstring to_string() const + { + return wil::str_printf(L"(L:%td, T:%td, R:%td, B:%td) [W:%td, H:%td]", left(), top(), right(), bottom(), width(), height()); + } + protected: til::point _topLeft; til::point _bottomRight; @@ -606,7 +843,7 @@ namespace WEX::TestExecution public: static WEX::Common::NoThrowString ToString(const ::til::rectangle& rect) { - return WEX::Common::NoThrowString().Format(L"(L:%td, T:%td, R:%td, B:%td) [W:%td, H:%td]", rect.left(), rect.top(), rect.right(), rect.bottom(), rect.width(), rect.height()); + return WEX::Common::NoThrowString(rect.to_string().c_str()); } }; diff --git a/src/inc/til/size.h b/src/inc/til/size.h index 905d5be98cb..e551980fbd5 100644 --- a/src/inc/til/size.h +++ b/src/inc/til/size.h @@ -220,6 +220,11 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" } #endif + std::wstring to_string() const + { + return wil::str_printf(L"[W:%td, H:%td]", width(), height()); + } + protected: ptrdiff_t _width; ptrdiff_t _height; @@ -239,7 +244,7 @@ namespace WEX::TestExecution public: static WEX::Common::NoThrowString ToString(const ::til::size& size) { - return WEX::Common::NoThrowString().Format(L"[W:%td, H:%td]", size.width(), size.height()); + return WEX::Common::NoThrowString(size.to_string().c_str()); } }; diff --git a/src/inc/til/some.h b/src/inc/til/some.h index 9620bdce777..96d523c486b 100644 --- a/src/inc/til/some.h +++ b/src/inc/til/some.h @@ -208,6 +208,21 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" { throw std::out_of_range("invalid some subscript"); } + + std::wstring to_string() const + { + std::wstringstream wss; + wss << std::endl + << L"Some contains " << size() << " of max size " << max_size() << ":" << std::endl; + wss << L"Elements:" << std::endl; + + for (auto& item : *this) + { + wss << L"\t- " << item.to_string() << std::endl; + } + + return wss.str(); + } }; } @@ -220,15 +235,7 @@ namespace WEX::TestExecution public: static WEX::Common::NoThrowString ToString(const ::til::some& some) { - auto str = WEX::Common::NoThrowString().Format(L"\r\nSome contains %d of max size %d:\r\nElements:\r\n", some.size(), some.max_size()); - - for (auto& item : some) - { - const auto itemStr = WEX::TestExecution::VerifyOutputTraits::ToString(item); - str.AppendFormat(L"\t- %ws\r\n", (const wchar_t*)itemStr); - } - - return str; + return WEX::Common::NoThrowString(some.to_string().c_str()); } }; diff --git a/src/interactivity/onecore/BgfxEngine.cpp b/src/interactivity/onecore/BgfxEngine.cpp index ab6abe8b765..bfdcfa720f3 100644 --- a/src/interactivity/onecore/BgfxEngine.cpp +++ b/src/interactivity/onecore/BgfxEngine.cpp @@ -232,7 +232,7 @@ BgfxEngine::BgfxEngine(PVOID SharedViewBase, LONG DisplayHeight, LONG DisplayWid return S_OK; } -std::vector BgfxEngine::GetDirtyArea() +std::vector BgfxEngine::GetDirtyArea() { SMALL_RECT r; r.Bottom = _displayHeight > 0 ? (SHORT)(_displayHeight - 1) : 0; diff --git a/src/interactivity/onecore/BgfxEngine.hpp b/src/interactivity/onecore/BgfxEngine.hpp index 786da48d3f6..d2fc5488bbf 100644 --- a/src/interactivity/onecore/BgfxEngine.hpp +++ b/src/interactivity/onecore/BgfxEngine.hpp @@ -69,7 +69,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT GetProposedFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo, int const iDpi) noexcept override; - std::vector GetDirtyArea() override; + std::vector GetDirtyArea() override; [[nodiscard]] HRESULT GetFontSize(_Out_ COORD* const pFontSize) noexcept override; [[nodiscard]] HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept override; diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index 0d9827a71e0..dfe4bdd3a6d 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -862,7 +862,7 @@ void Renderer::_PaintOverlay(IRenderEngine& engine, // Set it up in a Viewport helper structure and trim it the IME viewport to be within the full console viewport. Viewport viewConv = Viewport::FromInclusive(srCaView); - for (auto srDirty : engine.GetDirtyArea()) + for (SMALL_RECT srDirty : engine.GetDirtyArea()) { // Dirty is an inclusive rectangle, but oddly enough the IME was an exclusive one, so correct it. srDirty.Bottom++; diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp index 2ed766fdcbc..1345e6b23d1 100644 --- a/src/renderer/dx/DxRenderer.cpp +++ b/src/renderer/dx/DxRenderer.cpp @@ -1686,7 +1686,7 @@ float DxEngine::GetScaling() const noexcept // - // Return Value: // - Rectangle describing dirty area in characters. -[[nodiscard]] std::vector DxEngine::GetDirtyArea() +[[nodiscard]] std::vector DxEngine::GetDirtyArea() { SMALL_RECT r; r.Top = gsl::narrow(floor(_invalidRect.top / _glyphCell.cy)); diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp index 68ef13b35a1..b8521d9b1ee 100644 --- a/src/renderer/dx/DxRenderer.hpp +++ b/src/renderer/dx/DxRenderer.hpp @@ -95,7 +95,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT GetProposedFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo, int const iDpi) noexcept override; - [[nodiscard]] std::vector GetDirtyArea() override; + [[nodiscard]] std::vector GetDirtyArea() override; [[nodiscard]] HRESULT GetFontSize(_Out_ COORD* const pFontSize) noexcept override; [[nodiscard]] HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept override; diff --git a/src/renderer/gdi/gdirenderer.hpp b/src/renderer/gdi/gdirenderer.hpp index 356ce0deebd..50f2d543af0 100644 --- a/src/renderer/gdi/gdirenderer.hpp +++ b/src/renderer/gdi/gdirenderer.hpp @@ -68,7 +68,7 @@ namespace Microsoft::Console::Render _Out_ FontInfo& Font, const int iDpi) noexcept override; - [[nodiscard]] std::vector GetDirtyArea() override; + [[nodiscard]] std::vector GetDirtyArea() override; [[nodiscard]] HRESULT GetFontSize(_Out_ COORD* const pFontSize) noexcept override; [[nodiscard]] HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept override; diff --git a/src/renderer/gdi/math.cpp b/src/renderer/gdi/math.cpp index 7b2372ba083..7b5f5ccc684 100644 --- a/src/renderer/gdi/math.cpp +++ b/src/renderer/gdi/math.cpp @@ -16,7 +16,7 @@ using namespace Microsoft::Console::Render; // Return Value: // - The character dimensions of the current dirty area of the frame. // This is an Inclusive rect. -std::vector GdiEngine::GetDirtyArea() +std::vector GdiEngine::GetDirtyArea() { RECT rc = _psInvalidData.rcPaint; diff --git a/src/renderer/inc/IRenderEngine.hpp b/src/renderer/inc/IRenderEngine.hpp index 97951577b00..a55e0b64b3b 100644 --- a/src/renderer/inc/IRenderEngine.hpp +++ b/src/renderer/inc/IRenderEngine.hpp @@ -117,7 +117,7 @@ namespace Microsoft::Console::Render _Out_ FontInfo& FontInfo, const int iDpi) noexcept = 0; - virtual std::vector GetDirtyArea() = 0; + virtual std::vector GetDirtyArea() = 0; [[nodiscard]] virtual HRESULT GetFontSize(_Out_ COORD* const pFontSize) noexcept = 0; [[nodiscard]] virtual HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept = 0; [[nodiscard]] virtual HRESULT UpdateTitle(const std::wstring& newTitle) noexcept = 0; diff --git a/src/renderer/uia/UiaRenderer.cpp b/src/renderer/uia/UiaRenderer.cpp index 01235ca1cfe..68c03b7077e 100644 --- a/src/renderer/uia/UiaRenderer.cpp +++ b/src/renderer/uia/UiaRenderer.cpp @@ -426,7 +426,7 @@ UiaEngine::UiaEngine(IUiaEventDispatcher* dispatcher) : // - // Return Value: // - Rectangle describing dirty area in characters. -[[nodiscard]] std::vector UiaEngine::GetDirtyArea() +[[nodiscard]] std::vector UiaEngine::GetDirtyArea() { return { Viewport::Empty().ToInclusive() }; } diff --git a/src/renderer/uia/UiaRenderer.hpp b/src/renderer/uia/UiaRenderer.hpp index bdebb01de27..3f7ced8e33f 100644 --- a/src/renderer/uia/UiaRenderer.hpp +++ b/src/renderer/uia/UiaRenderer.hpp @@ -71,7 +71,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT GetProposedFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo, int const iDpi) noexcept override; - [[nodiscard]] std::vector GetDirtyArea() override; + [[nodiscard]] std::vector GetDirtyArea() override; [[nodiscard]] HRESULT GetFontSize(_Out_ COORD* const pFontSize) noexcept override; [[nodiscard]] HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept override; diff --git a/src/renderer/vt/XtermEngine.cpp b/src/renderer/vt/XtermEngine.cpp index 145d972fdba..8c0420e5224 100644 --- a/src/renderer/vt/XtermEngine.cpp +++ b/src/renderer/vt/XtermEngine.cpp @@ -408,21 +408,8 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe, if (dx != 0 || dy != 0) { - // Scroll the current offset - RETURN_IF_FAILED(_InvalidOffset(pcoordDelta)); - - // Add the top/bottom of the window to the invalid area - SMALL_RECT invalid = _lastViewport.ToOrigin().ToExclusive(); - - if (dy > 0) - { - invalid.Bottom = dy; - } - else if (dy < 0) - { - invalid.Top = invalid.Bottom + dy; - } - LOG_IF_FAILED(_InvalidCombine(Viewport::FromExclusive(invalid))); + // Scroll the current offset and invalidate the revealed area + _invalidMap.translate(til::point(*pcoordDelta), true); COORD invalidScrollNew; RETURN_IF_FAILED(ShortAdd(_scrollDelta.X, dx, &invalidScrollNew.X)); diff --git a/src/renderer/vt/invalidate.cpp b/src/renderer/vt/invalidate.cpp index c3042ad6e1b..597f56b0f06 100644 --- a/src/renderer/vt/invalidate.cpp +++ b/src/renderer/vt/invalidate.cpp @@ -47,12 +47,14 @@ using namespace Microsoft::Console::Render; // Return Value: // - S_OK, else an appropriate HRESULT for failing to allocate or write. [[nodiscard]] HRESULT VtEngine::Invalidate(const SMALL_RECT* const psrRegion) noexcept +try { - Viewport newInvalid = Viewport::FromExclusive(*psrRegion); - _trace.TraceInvalidate(newInvalid); - - return this->_InvalidCombine(newInvalid); + const til::rectangle rect{ Viewport::FromExclusive(*psrRegion).ToInclusive() }; + _trace.TraceInvalidate(rect); + _invalidMap.set(rect); + return S_OK; } +CATCH_RETURN(); // Routine Description: // - Notifies us that the console has changed the position of the cursor. @@ -87,10 +89,13 @@ using namespace Microsoft::Console::Render; // Return Value: // - S_OK, else an appropriate HRESULT for failing to allocate or write. [[nodiscard]] HRESULT VtEngine::InvalidateAll() noexcept +try { - _trace.TraceInvalidateAll(_lastViewport.ToOrigin()); - return this->_InvalidCombine(_lastViewport.ToOrigin()); + _trace.TraceInvalidateAll(_lastViewport.ToOrigin().ToInclusive()); + _invalidMap.set_all(); + return S_OK; } +CATCH_RETURN(); // Method Description: // - Notifies us that we're about to circle the buffer, giving us a chance to @@ -132,75 +137,3 @@ using namespace Microsoft::Console::Render; *pForcePaint = true; return S_OK; } - -// Routine Description: -// - Helper to combine the given rectangle into the invalid region to be -// updated on the next paint -// Expects EXCLUSIVE rectangles. -// Arguments: -// - invalid - A viewport containing the character region that should be -// repainted on the next frame -// Return Value: -// - S_OK, else an appropriate HRESULT for failing to allocate or write. -[[nodiscard]] HRESULT VtEngine::_InvalidCombine(const Viewport invalid) noexcept -{ - if (!_fInvalidRectUsed) - { - _invalidRect = invalid; - _fInvalidRectUsed = true; - } - else - { - _invalidRect = Viewport::Union(_invalidRect, invalid); - } - - // Ensure invalid areas remain within bounds of window. - RETURN_IF_FAILED(_InvalidRestrict()); - - return S_OK; -} - -// Routine Description: -// - Helper to adjust the invalid region by the given offset such as when a -// scroll operation occurs. -// Arguments: -// - ppt - Distances by which we should move the invalid region in response to a scroll -// Return Value: -// - S_OK, else an appropriate HRESULT for failing to allocate or write. -[[nodiscard]] HRESULT VtEngine::_InvalidOffset(const COORD* const pCoord) noexcept -{ - if (_fInvalidRectUsed) - { - try - { - Viewport newInvalid = Viewport::Offset(_invalidRect, *pCoord); - - // Add the scrolled invalid rectangle to what was left behind to get the new invalid area. - // This is the equivalent of adding in the "update rectangle" that we would get out of ScrollWindowEx/ScrollDC. - _invalidRect = Viewport::Union(_invalidRect, newInvalid); - - // Ensure invalid areas remain within bounds of window. - RETURN_IF_FAILED(_InvalidRestrict()); - } - CATCH_RETURN(); - } - - return S_OK; -} - -// Routine Description: -// - Helper to ensure the invalid region remains within the bounds of the viewport. -// Arguments: -// - -// Return Value: -// - S_OK, else an appropriate HRESULT for failing to allocate or safemath failure. -[[nodiscard]] HRESULT VtEngine::_InvalidRestrict() noexcept -{ - SMALL_RECT oldInvalid = _invalidRect.ToExclusive(); - - _lastViewport.ToOrigin().TrimToViewport(&oldInvalid); - - _invalidRect = Viewport::FromExclusive(oldInvalid); - - return S_OK; -} diff --git a/src/renderer/vt/math.cpp b/src/renderer/vt/math.cpp index f7faad04778..a5aaa7129fe 100644 --- a/src/renderer/vt/math.cpp +++ b/src/renderer/vt/math.cpp @@ -17,14 +17,9 @@ using namespace Microsoft::Console::Types; // Return Value: // - The character dimensions of the current dirty area of the frame. // This is an Inclusive rect. -std::vector VtEngine::GetDirtyArea() +std::vector VtEngine::GetDirtyArea() { - SMALL_RECT dirty = _invalidRect.ToInclusive(); - if (dirty.Top < _virtualTop) - { - dirty.Top = _virtualTop; - } - return { dirty }; + return _invalidMap.runs(); } // Routine Description: @@ -68,17 +63,26 @@ void VtEngine::_OrRect(_Inout_ SMALL_RECT* const pRectExisting, const SMALL_RECT // - true iff only the next character is invalid bool VtEngine::_WillWriteSingleChar() const { - COORD currentCursor = _lastText; - SMALL_RECT _srcInvalid = _invalidRect.ToExclusive(); - bool noScrollDelta = (_scrollDelta.X == 0 && _scrollDelta.Y == 0); + // If there is scroll delta, return false. + if (til::point{ 0, 0 } != til::point{ _scrollDelta }) + { + return false; + } + + // If there is more than one invalid char, return false. + if (!_invalidMap.one()) + { + return false; + } + + // Get the single point at which things are invalid. + const auto invalidPoint = _invalidMap.runs().front().origin(); - bool invalidIsOneChar = (_invalidRect.Width() == 1) && - (_invalidRect.Height() == 1); // Either the next character to the right or the immediately previous // character should follow this code path // (The immediate previous character would suggest a backspace) - bool invalidIsNext = (_srcInvalid.Top == _lastText.Y) && (_srcInvalid.Left == _lastText.X); - bool invalidIsLast = (_srcInvalid.Top == _lastText.Y) && (_srcInvalid.Left == (_lastText.X - 1)); + bool invalidIsNext = invalidPoint == til::point{ _lastText }; + bool invalidIsLast = invalidPoint == til::point{ _lastText.X - 1, _lastText.Y }; - return noScrollDelta && invalidIsOneChar && (invalidIsNext || invalidIsLast); + return invalidIsNext || invalidIsLast; } diff --git a/src/renderer/vt/paint.cpp b/src/renderer/vt/paint.cpp index dc292f74f3d..babc6606caa 100644 --- a/src/renderer/vt/paint.cpp +++ b/src/renderer/vt/paint.cpp @@ -26,13 +26,13 @@ using namespace Microsoft::Console::Types; } // If there's nothing to do, quick return - bool somethingToDo = _fInvalidRectUsed || + bool somethingToDo = _invalidMap.any() || (_scrollDelta.X != 0 || _scrollDelta.Y != 0) || _cursorMoved || _titleChanged; _quickReturn = !somethingToDo; - _trace.TraceStartPaint(_quickReturn, _fInvalidRectUsed, _invalidRect, _lastViewport, _scrollDelta, _cursorMoved); + _trace.TraceStartPaint(_quickReturn, _invalidMap, _lastViewport.ToInclusive(), _scrollDelta, _cursorMoved); return _quickReturn ? S_FALSE : S_OK; } @@ -50,8 +50,8 @@ using namespace Microsoft::Console::Types; { _trace.TraceEndPaint(); - _invalidRect = Viewport::Empty(); - _fInvalidRectUsed = false; + _invalidMap.reset_all(); + _scrollDelta = { 0 }; _clearedAllThisFrame = false; _cursorMoved = false; diff --git a/src/renderer/vt/state.cpp b/src/renderer/vt/state.cpp index c83ac780ddd..a50179cd506 100644 --- a/src/renderer/vt/state.cpp +++ b/src/renderer/vt/state.cpp @@ -35,8 +35,7 @@ VtEngine::VtEngine(_In_ wil::unique_hfile pipe, _LastBG(INVALID_COLOR), _lastWasBold(false), _lastViewport(initialViewport), - _invalidRect(Viewport::Empty()), - _fInvalidRectUsed(false), + _invalidMap(initialViewport.Dimensions()), _lastRealCursor({ 0 }), _lastText({ 0 }), _scrollDelta({ 0 }), @@ -317,40 +316,19 @@ CATCH_RETURN(); // buffer will have triggered it's own invalidations for what it knows is // invalid. Previously, we'd invalidate everything if the width changed, // because we couldn't be sure if lines were reflowed. + _invalidMap.resize(newView.Dimensions()); } else { if (SUCCEEDED(hr)) { + _invalidMap.resize(newView.Dimensions(), true); // resize while filling in new space with repaint requests. + // Viewport is smaller now - just update it all. if (oldView.Height() > newView.Height() || oldView.Width() > newView.Width()) { hr = InvalidateAll(); } - else - { - // At least one of the directions grew. - // First try and add everything to the right of the old viewport, - // then everything below where the old viewport ended. - if (oldView.Width() < newView.Width()) - { - short left = oldView.RightExclusive(); - short top = 0; - short right = newView.RightInclusive(); - short bottom = oldView.BottomInclusive(); - Viewport rightOfOldViewport = Viewport::FromInclusive({ left, top, right, bottom }); - hr = _InvalidCombine(rightOfOldViewport); - } - if (SUCCEEDED(hr) && oldView.Height() < newView.Height()) - { - short left = 0; - short top = oldView.BottomExclusive(); - short right = newView.RightInclusive(); - short bottom = newView.BottomInclusive(); - Viewport belowOldViewport = Viewport::FromInclusive({ left, top, right, bottom }); - hr = _InvalidCombine(belowOldViewport); - } - } } } @@ -416,7 +394,7 @@ void VtEngine::SetTestCallback(_In_ std::function - - - INLINE_TEST_METHOD_MARKUP;UNIT_TESTING;%(PreprocessorDefinitions) - - - + diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp index 8ce51bbcf1c..7ebb7a71a87 100644 --- a/src/renderer/vt/vtrenderer.hpp +++ b/src/renderer/vt/vtrenderer.hpp @@ -89,7 +89,7 @@ namespace Microsoft::Console::Render _Out_ FontInfo& Font, const int iDpi) noexcept override; - std::vector GetDirtyArea() override; + std::vector GetDirtyArea() override; [[nodiscard]] HRESULT GetFontSize(_Out_ COORD* const pFontSize) noexcept override; [[nodiscard]] HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept override; @@ -121,9 +121,9 @@ namespace Microsoft::Console::Render bool _lastWasBold; Microsoft::Console::Types::Viewport _lastViewport; - Microsoft::Console::Types::Viewport _invalidRect; - bool _fInvalidRectUsed; + til::bitmap _invalidMap; + COORD _lastRealCursor; COORD _lastText; COORD _scrollDelta; @@ -160,9 +160,6 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT _Flush() noexcept; void _OrRect(_Inout_ SMALL_RECT* const pRectExisting, const SMALL_RECT* const pRectToOr) const; - [[nodiscard]] HRESULT _InvalidCombine(const Microsoft::Console::Types::Viewport invalid) noexcept; - [[nodiscard]] HRESULT _InvalidOffset(const COORD* const ppt) noexcept; - [[nodiscard]] HRESULT _InvalidRestrict() noexcept; bool _AllIsInvalid() const; [[nodiscard]] HRESULT _StopCursorBlinking() noexcept; diff --git a/src/renderer/wddmcon/WddmConRenderer.cpp b/src/renderer/wddmcon/WddmConRenderer.cpp index 475fb85125a..5819511c0a3 100644 --- a/src/renderer/wddmcon/WddmConRenderer.cpp +++ b/src/renderer/wddmcon/WddmConRenderer.cpp @@ -355,7 +355,7 @@ bool WddmConEngine::IsInitialized() return S_OK; } -std::vector WddmConEngine::GetDirtyArea() +std::vector WddmConEngine::GetDirtyArea() { SMALL_RECT r; r.Bottom = _displayHeight > 0 ? (SHORT)(_displayHeight - 1) : 0; diff --git a/src/renderer/wddmcon/WddmConRenderer.hpp b/src/renderer/wddmcon/WddmConRenderer.hpp index 1818ae3c649..ad137131c77 100644 --- a/src/renderer/wddmcon/WddmConRenderer.hpp +++ b/src/renderer/wddmcon/WddmConRenderer.hpp @@ -61,7 +61,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT GetProposedFont(const FontInfoDesired& fiFontInfoDesired, FontInfo& fiFontInfo, int const iDpi) noexcept override; - std::vector GetDirtyArea() override; + std::vector GetDirtyArea() override; [[nodiscard]] HRESULT GetFontSize(_Out_ COORD* const pFontSize) noexcept override; [[nodiscard]] HRESULT IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept override; diff --git a/src/til/ut_til/BitmapTests.cpp b/src/til/ut_til/BitmapTests.cpp index a034c1145d1..34005e259f0 100644 --- a/src/til/ut_til/BitmapTests.cpp +++ b/src/til/ut_til/BitmapTests.cpp @@ -25,14 +25,21 @@ class BitmapTests 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) + if (bitsOn.empty()) { - dirtyExpected |= *it; + VERIFY_ARE_EQUAL(til::rectangle{}, map._dirty); } + else + { + 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); + // Check if it matches. + VERIFY_ARE_EQUAL(dirtyExpected, map._dirty); + } Log::Comment(L"Check all bits in map."); // For every point in the map... @@ -118,6 +125,486 @@ class BitmapTests } } + TEST_METHOD(Equality) + { + Log::Comment(L"0.) Defaults are equal"); + { + const til::bitmap one; + const til::bitmap two; + VERIFY_IS_TRUE(one == two); + } + + Log::Comment(L"1.) Different sizes are unequal"); + { + const til::bitmap one{ til::size{ 2, 2 } }; + const til::bitmap two{ til::size{ 3, 3 } }; + VERIFY_IS_FALSE(one == two); + } + + Log::Comment(L"2.) Same bits set are equal"); + { + til::bitmap one{ til::size{ 2, 2 } }; + til::bitmap two{ til::size{ 2, 2 } }; + one.set(til::point{ 0, 1 }); + one.set(til::point{ 1, 0 }); + two.set(til::point{ 0, 1 }); + two.set(til::point{ 1, 0 }); + VERIFY_IS_TRUE(one == two); + } + + Log::Comment(L"3.) Different bits set are not equal"); + { + til::bitmap one{ til::size{ 2, 2 } }; + til::bitmap two{ til::size{ 2, 2 } }; + one.set(til::point{ 0, 1 }); + two.set(til::point{ 1, 0 }); + VERIFY_IS_FALSE(one == two); + } + } + + TEST_METHOD(Inequality) + { + Log::Comment(L"0.) Defaults are equal"); + { + const til::bitmap one; + const til::bitmap two; + VERIFY_IS_FALSE(one != two); + } + + Log::Comment(L"1.) Different sizes are unequal"); + { + const til::bitmap one{ til::size{ 2, 2 } }; + const til::bitmap two{ til::size{ 3, 3 } }; + VERIFY_IS_TRUE(one != two); + } + + Log::Comment(L"2.) Same bits set are equal"); + { + til::bitmap one{ til::size{ 2, 2 } }; + til::bitmap two{ til::size{ 2, 2 } }; + one.set(til::point{ 0, 1 }); + one.set(til::point{ 1, 0 }); + two.set(til::point{ 0, 1 }); + two.set(til::point{ 1, 0 }); + VERIFY_IS_FALSE(one != two); + } + + Log::Comment(L"3.) Different bits set are not equal"); + { + til::bitmap one{ til::size{ 2, 2 } }; + til::bitmap two{ til::size{ 2, 2 } }; + one.set(til::point{ 0, 1 }); + two.set(til::point{ 1, 0 }); + VERIFY_IS_TRUE(one != two); + } + } + + TEST_METHOD(Translate) + { + const til::size mapSize{ 4, 4 }; + til::bitmap map{ mapSize }; + + // set the middle four bits of the map. + // 0 0 0 0 + // 0 1 1 0 + // 0 1 1 0 + // 0 0 0 0 + map.set(til::rectangle{ til::point{ 1, 1 }, til::size{ 2, 2 } }); + + Log::Comment(L"1.) Move down and right"); + { + auto actual = map; + // Move all contents + // | + // v + // | + // v --> --> + const til::point delta{ 2, 2 }; + + til::bitmap expected{ mapSize }; + // Expected: + // 0 0 0 0 0 0 0 0 0 0 0 0 + // 0 1 1 0 0 0 0 0 0 0 0 0 + // 0 1 1 0 v --> 0 0 0 0 --> 0 0 0 0 + // 0 0 0 0 v 0 1 1 0 0 0 0 1 + // ->-> + expected.set(til::point{ 3, 3 }); + + actual.translate(delta); + + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"2.) Move down"); + { + auto actual = map; + // Move all contents + // | + // v + // | + // v + const til::point delta{ 0, 2 }; + + til::bitmap expected{ mapSize }; + // Expected: + // 0 0 0 0 0 0 0 0 + // 0 1 1 0 0 0 0 0 + // 0 1 1 0 v --> 0 0 0 0 + // 0 0 0 0 v 0 1 1 0 + expected.set(til::rectangle{ til::point{ 1, 3 }, til::size{ 2, 1 } }); + + actual.translate(delta); + + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"3.) Move down and left"); + { + auto actual = map; + // Move all contents + // | + // v + // | + // v <-- <-- + const til::point delta{ -2, 2 }; + + til::bitmap expected{ mapSize }; + // Expected: + // 0 0 0 0 0 0 0 0 0 0 0 0 + // 0 1 1 0 0 0 0 0 0 0 0 0 + // 0 1 1 0 v --> 0 0 0 0 --> 0 0 0 0 + // 0 0 0 0 v 0 1 1 0 1 0 0 0 + // <-<- + expected.set(til::point{ 0, 3 }); + + actual.translate(delta); + + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"4.) Move left"); + { + auto actual = map; + // Move all contents + // <-- <-- + const til::point delta{ -2, 0 }; + + til::bitmap expected{ mapSize }; + // Expected: + // 0 0 0 0 0 0 0 0 + // 0 1 1 0 1 0 0 0 + // 0 1 1 0 --> 1 0 0 0 + // 0 0 0 0 0 0 0 0 + // <--<-- + expected.set(til::rectangle{ til::point{ 0, 1 }, til::size{ 1, 2 } }); + + actual.translate(delta); + + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"5.) Move up and left"); + { + auto actual = map; + // Move all contents + // ^ + // | + // ^ + // | <-- <-- + const til::point delta{ -2, -2 }; + + til::bitmap expected{ mapSize }; + // Expected: + // 0 0 0 0 ^ 0 1 1 0 1 0 0 0 + // 0 1 1 0 ^ 0 0 0 0 0 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 + // <-<- + expected.set(til::point{ 0, 0 }); + + actual.translate(delta); + + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"6.) Move up"); + { + auto actual = map; + // Move all contents + // ^ + // | + // ^ + // | + const til::point delta{ 0, -2 }; + + til::bitmap expected{ mapSize }; + // Expected: + // 0 0 0 0 ^ 0 1 1 0 + // 0 1 1 0 ^ 0 0 0 0 + // 0 1 1 0 --> 0 0 0 0 + // 0 0 0 0 0 0 0 0 + expected.set(til::rectangle{ til::point{ 1, 0 }, til::size{ 2, 1 } }); + + actual.translate(delta); + + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"7.) Move up and right"); + { + auto actual = map; + // Move all contents + // ^ + // | + // ^ + // | --> --> + const til::point delta{ 2, -2 }; + + til::bitmap expected{ mapSize }; + // Expected: + // 0 0 0 0 ^ 0 1 1 0 0 0 0 1 + // 0 1 1 0 ^ 0 0 0 0 0 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 + // ->-> + expected.set(til::point{ 3, 0 }); + + actual.translate(delta); + + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"8.) Move right"); + { + auto actual = map; + // Move all contents + // --> --> + const til::point delta{ 2, 0 }; + + til::bitmap expected{ mapSize }; + // Expected: + // 0 0 0 0 0 0 0 0 + // 0 1 1 0 0 0 0 1 + // 0 1 1 0 --> 0 0 0 1 + // 0 0 0 0 0 0 0 0 + // ->-> + expected.set(til::rectangle{ til::point{ 3, 1 }, til::size{ 1, 2 } }); + + actual.translate(delta); + + VERIFY_ARE_EQUAL(expected, actual); + } + } + + TEST_METHOD(TranslateWithFill) + { + const til::size mapSize{ 4, 4 }; + til::bitmap map{ mapSize }; + + // set the middle four bits of the map. + // 0 0 0 0 + // 0 1 1 0 + // 0 1 1 0 + // 0 0 0 0 + map.set(til::rectangle{ til::point{ 1, 1 }, til::size{ 2, 2 } }); + + Log::Comment(L"1.) Move down and right"); + { + auto actual = map; + // Move all contents + // | + // v + // | + // v --> --> + const til::point delta{ 2, 2 }; + + til::bitmap expected{ mapSize }; + // Expected: (F is filling uncovered value) + // 0 0 0 0 F F F F F F F F + // 0 1 1 0 F F F F F F F F + // 0 1 1 0 v --> 0 0 0 0 --> F F 0 0 + // 0 0 0 0 v 0 1 1 0 F F 0 1 + // ->-> + expected.set(til::rectangle{ til::point{ 0, 0 }, til::size{ 4, 2 } }); + expected.set(til::rectangle{ til::point{ 0, 2 }, til::size{ 2, 2 } }); + expected.set(til::point{ 3, 3 }); + + actual.translate(delta, true); + + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"2.) Move down"); + { + auto actual = map; + // Move all contents + // | + // v + // | + // v + const til::point delta{ 0, 2 }; + + til::bitmap expected{ mapSize }; + // Expected: (F is filling uncovered value) + // 0 0 0 0 F F F F + // 0 1 1 0 F F F F + // 0 1 1 0 v --> 0 0 0 0 + // 0 0 0 0 v 0 1 1 0 + expected.set(til::rectangle{ til::point{ 0, 0 }, til::size{ 4, 2 } }); + expected.set(til::rectangle{ til::point{ 1, 3 }, til::size{ 2, 1 } }); + + actual.translate(delta, true); + + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"3.) Move down and left"); + { + auto actual = map; + // Move all contents + // | + // v + // | + // v <-- <-- + const til::point delta{ -2, 2 }; + + til::bitmap expected{ mapSize }; + // Expected: (F is filling uncovered value) + // 0 0 0 0 F F F F F F F F + // 0 1 1 0 F F F F F F F F + // 0 1 1 0 v --> 0 0 0 0 --> 0 0 F F + // 0 0 0 0 v 0 1 1 0 1 0 F F + // <-<- + expected.set(til::rectangle{ til::point{ 0, 0 }, til::size{ 4, 2 } }); + expected.set(til::rectangle{ til::point{ 2, 2 }, til::size{ 2, 2 } }); + expected.set(til::point{ 0, 3 }); + + actual.translate(delta, true); + + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"4.) Move left"); + { + auto actual = map; + // Move all contents + // <-- <-- + const til::point delta{ -2, 0 }; + + til::bitmap expected{ mapSize }; + // Expected: (F is filling uncovered value) + // 0 0 0 0 0 0 F F + // 0 1 1 0 1 0 F F + // 0 1 1 0 --> 1 0 F F + // 0 0 0 0 0 0 F F + // <--<-- + expected.set(til::rectangle{ til::point{ 2, 0 }, til::size{ 2, 4 } }); + expected.set(til::rectangle{ til::point{ 0, 1 }, til::size{ 1, 2 } }); + + actual.translate(delta, true); + + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"5.) Move up and left"); + { + auto actual = map; + // Move all contents + // ^ + // | + // ^ + // | <-- <-- + const til::point delta{ -2, -2 }; + + til::bitmap expected{ mapSize }; + // Expected: (F is filling uncovered value) + // 0 0 0 0 ^ 0 1 1 0 1 0 F F + // 0 1 1 0 ^ 0 0 0 0 0 0 F F + // 0 1 1 0 --> F F F F --> F F F F + // 0 0 0 0 F F F F F F F F + // <-<- + expected.set(til::rectangle{ til::point{ 2, 0 }, til::size{ 2, 2 } }); + expected.set(til::rectangle{ til::point{ 0, 2 }, til::size{ 4, 2 } }); + expected.set(til::point{ 0, 0 }); + + actual.translate(delta, true); + + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"6.) Move up"); + { + auto actual = map; + // Move all contents + // ^ + // | + // ^ + // | + const til::point delta{ 0, -2 }; + + til::bitmap expected{ mapSize }; + // Expected: (F is filling uncovered value) + // 0 0 0 0 ^ 0 1 1 0 + // 0 1 1 0 ^ 0 0 0 0 + // 0 1 1 0 --> F F F F + // 0 0 0 0 F F F F + expected.set(til::rectangle{ til::point{ 1, 0 }, til::size{ 2, 1 } }); + expected.set(til::rectangle{ til::point{ 0, 2 }, til::size{ 4, 2 } }); + + actual.translate(delta, true); + + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"7.) Move up and right"); + { + auto actual = map; + // Move all contents + // ^ + // | + // ^ + // | --> --> + const til::point delta{ 2, -2 }; + + til::bitmap expected{ mapSize }; + // Expected: (F is filling uncovered value) + // 0 0 0 0 ^ 0 1 1 0 F F 0 1 + // 0 1 1 0 ^ 0 0 0 0 F F 0 0 + // 0 1 1 0 --> F F F F --> F F F F + // 0 0 0 0 F F F F F F F F + // ->-> + expected.set(til::point{ 3, 0 }); + expected.set(til::rectangle{ til::point{ 0, 2 }, til::size{ 4, 2 } }); + expected.set(til::rectangle{ til::point{ 0, 0 }, til::size{ 2, 2 } }); + + actual.translate(delta, true); + + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"8.) Move right"); + { + auto actual = map; + // Move all contents + // --> --> + const til::point delta{ 2, 0 }; + + til::bitmap expected{ mapSize }; + // Expected: (F is filling uncovered value) + // 0 0 0 0 F F 0 0 + // 0 1 1 0 F F 0 1 + // 0 1 1 0 --> F F 0 1 + // 0 0 0 0 F F 0 0 + // ->-> + expected.set(til::rectangle{ til::point{ 3, 1 }, til::size{ 1, 2 } }); + expected.set(til::rectangle{ til::point{ 0, 0 }, til::size{ 2, 4 } }); + + actual.translate(delta, true); + + VERIFY_ARE_EQUAL(expected, actual); + } + } + TEST_METHOD(SetReset) { const til::size sz{ 4, 4 }; @@ -133,7 +620,8 @@ class BitmapTests const til::point point{ 2, 2 }; bitmap.set(point); - til::rectangle expectedSet{ point }; + std::vector expectedSet; + expectedSet.emplace_back(til::rectangle{ point }); // Run through every bit. Only the one we set should be true. Log::Comment(L"Only the bit we set should be true."); @@ -142,13 +630,14 @@ class BitmapTests Log::Comment(L"Setting all should mean they're all true."); bitmap.set_all(); - expectedSet = til::rectangle{ bitmap._rc }; + expectedSet.clear(); + expectedSet.emplace_back(til::rectangle{ bitmap._rc }); _checkBits(expectedSet, bitmap); Log::Comment(L"Now reset them all."); bitmap.reset_all(); - expectedSet = {}; + expectedSet.clear(); _checkBits(expectedSet, bitmap); til::rectangle totalZone{ sz }; @@ -160,13 +649,14 @@ class BitmapTests til::rectangle setZone{ til::point{ 0, 0 }, til::size{ 2, 3 } }; bitmap.set(setZone); - expectedSet = setZone; + expectedSet.clear(); + expectedSet.emplace_back(setZone); _checkBits(expectedSet, bitmap); Log::Comment(L"Reset all."); bitmap.reset_all(); - expectedSet = {}; + expectedSet.clear(); _checkBits(expectedSet, bitmap); } @@ -361,7 +851,7 @@ class BitmapTests VERIFY_IS_FALSE(bitmap.all()); } - TEST_METHOD(Iterate) + TEST_METHOD(Runs) { // This map --> Those runs // 1 1 0 1 A A _ B @@ -370,8 +860,6 @@ class BitmapTests // 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 @@ -421,7 +909,7 @@ class BitmapTests Log::Comment(L"Run the iterator and collect the runs."); til::some actual; - for (auto run : map) + for (auto run : map.runs()) { actual.push_back(run); } @@ -434,12 +922,67 @@ class BitmapTests expected.clear(); actual.clear(); - for (auto run : map) + for (auto run : map.runs()) { actual.push_back(run); } Log::Comment(L"Verify they're empty."); VERIFY_ARE_EQUAL(expected, actual); + + Log::Comment(L"Set point and validate runs updated."); + const til::point setPoint{ 2, 2 }; + expected.push_back(til::rectangle{ setPoint }); + map.set(setPoint); + + for (auto run : map.runs()) + { + actual.push_back(run); + } + VERIFY_ARE_EQUAL(expected, actual); + + Log::Comment(L"Set rectangle and validate runs updated."); + const til::rectangle setRect{ setPoint, til::size{ 2, 2 } }; + expected.clear(); + expected.push_back(til::rectangle{ til::point{ 2, 2 }, til::size{ 2, 1 } }); + expected.push_back(til::rectangle{ til::point{ 2, 3 }, til::size{ 2, 1 } }); + map.set(setRect); + + actual.clear(); + for (auto run : map.runs()) + { + actual.push_back(run); + } + VERIFY_ARE_EQUAL(expected, actual); + + Log::Comment(L"Set all and validate runs updated."); + expected.clear(); + expected.push_back(til::rectangle{ til::point{ 0, 0 }, til::size{ 4, 1 } }); + expected.push_back(til::rectangle{ til::point{ 0, 1 }, til::size{ 4, 1 } }); + expected.push_back(til::rectangle{ til::point{ 0, 2 }, til::size{ 4, 1 } }); + expected.push_back(til::rectangle{ til::point{ 0, 3 }, til::size{ 4, 1 } }); + map.set_all(); + + actual.clear(); + for (auto run : map.runs()) + { + actual.push_back(run); + } + VERIFY_ARE_EQUAL(expected, actual); + + Log::Comment(L"Resize and validate runs updated."); + const til::size newSize{ 3, 3 }; + expected.clear(); + expected.push_back(til::rectangle{ til::point{ 0, 0 }, til::size{ 3, 1 } }); + expected.push_back(til::rectangle{ til::point{ 0, 1 }, til::size{ 3, 1 } }); + expected.push_back(til::rectangle{ til::point{ 0, 2 }, til::size{ 3, 1 } }); + map.resize(newSize); + + actual.clear(); + for (auto run : map.runs()) + { + actual.push_back(run); + } + VERIFY_ARE_EQUAL(expected, actual); } }; diff --git a/src/til/ut_til/OperatorTests.cpp b/src/til/ut_til/OperatorTests.cpp new file mode 100644 index 00000000000..191c09e2484 --- /dev/null +++ b/src/til/ut_til/OperatorTests.cpp @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "til/operators.h" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +class OperatorTests +{ + TEST_CLASS(OperatorTests); + + TEST_METHOD(PointAddSize) + { + const til::point pt{ 5, 10 }; + const til::size sz{ 2, 4 }; + const til::point expected{ 5 + 2, 10 + 4 }; + const auto actual = pt + sz; + VERIFY_ARE_EQUAL(expected, actual); + } + + TEST_METHOD(PointSubSize) + { + const til::point pt{ 5, 10 }; + const til::size sz{ 2, 4 }; + const til::point expected{ 5 - 2, 10 - 4 }; + const auto actual = pt - sz; + VERIFY_ARE_EQUAL(expected, actual); + } + + TEST_METHOD(PointMulSize) + { + const til::point pt{ 5, 10 }; + const til::size sz{ 2, 4 }; + const til::point expected{ 5 * 2, 10 * 4 }; + const auto actual = pt * sz; + VERIFY_ARE_EQUAL(expected, actual); + } + + TEST_METHOD(PointDivSize) + { + const til::point pt{ 5, 10 }; + const til::size sz{ 2, 4 }; + const til::point expected{ 5 / 2, 10 / 4 }; + const auto actual = pt / sz; + VERIFY_ARE_EQUAL(expected, actual); + } + + TEST_METHOD(SizeAddPoint) + { + const til::size pt{ 5, 10 }; + const til::point sz{ 2, 4 }; + const til::size expected{ 5 + 2, 10 + 4 }; + const auto actual = pt + sz; + VERIFY_ARE_EQUAL(expected, actual); + } + + TEST_METHOD(SizeSubPoint) + { + const til::size pt{ 5, 10 }; + const til::point sz{ 2, 4 }; + const til::size expected{ 5 - 2, 10 - 4 }; + const auto actual = pt - sz; + VERIFY_ARE_EQUAL(expected, actual); + } + + TEST_METHOD(SizeMulPoint) + { + const til::size pt{ 5, 10 }; + const til::point sz{ 2, 4 }; + const til::size expected{ 5 * 2, 10 * 4 }; + const auto actual = pt * sz; + VERIFY_ARE_EQUAL(expected, actual); + } + + TEST_METHOD(SizeDivPoint) + { + const til::size pt{ 5, 10 }; + const til::point sz{ 2, 4 }; + const til::size expected{ 5 / 2, 10 / 4 }; + const auto actual = pt / sz; + VERIFY_ARE_EQUAL(expected, actual); + } +}; diff --git a/src/til/ut_til/RectangleTests.cpp b/src/til/ut_til/RectangleTests.cpp index 47e17da9430..82aa5a69a75 100644 --- a/src/til/ut_til/RectangleTests.cpp +++ b/src/til/ut_til/RectangleTests.cpp @@ -440,6 +440,16 @@ class RectangleTests VERIFY_ARE_EQUAL(expected, actual); } + TEST_METHOD(OrUnionInplace) + { + til::rectangle one{ 4, 6, 10, 14 }; + const til::rectangle two{ 5, 2, 13, 10 }; + + const til::rectangle expected{ 4, 2, 13, 14 }; + one |= two; + VERIFY_ARE_EQUAL(expected, one); + } + TEST_METHOD(AndIntersect) { const til::rectangle one{ 4, 6, 10, 14 }; @@ -450,6 +460,16 @@ class RectangleTests VERIFY_ARE_EQUAL(expected, actual); } + TEST_METHOD(AndIntersectInplace) + { + til::rectangle one{ 4, 6, 10, 14 }; + const til::rectangle two{ 5, 2, 13, 10 }; + + const til::rectangle expected{ 5, 6, 10, 10 }; + one &= two; + VERIFY_ARE_EQUAL(expected, one); + } + TEST_METHOD(MinusSubtractSame) { const til::rectangle original{ 0, 0, 10, 10 }; @@ -585,6 +605,198 @@ class RectangleTests VERIFY_ARE_EQUAL(expected, actual); } + TEST_METHOD(AdditionPoint) + { + const til::rectangle start{ 10, 20, 30, 40 }; + const til::point pt{ 3, 7 }; + const til::rectangle expected{ 10 + 3, 20 + 7, 30 + 3, 40 + 7 }; + const auto actual = start + pt; + VERIFY_ARE_EQUAL(expected, actual); + } + + TEST_METHOD(AdditionPointInplace) + { + til::rectangle start{ 10, 20, 30, 40 }; + const til::point pt{ 3, 7 }; + const til::rectangle expected{ 10 + 3, 20 + 7, 30 + 3, 40 + 7 }; + start += pt; + VERIFY_ARE_EQUAL(expected, start); + } + + TEST_METHOD(SubtractionPoint) + { + const til::rectangle start{ 10, 20, 30, 40 }; + const til::point pt{ 3, 7 }; + const til::rectangle expected{ 10 - 3, 20 - 7, 30 - 3, 40 - 7 }; + const auto actual = start - pt; + VERIFY_ARE_EQUAL(expected, actual); + } + + TEST_METHOD(SubtractionPointInplace) + { + til::rectangle start{ 10, 20, 30, 40 }; + const til::point pt{ 3, 7 }; + const til::rectangle expected{ 10 - 3, 20 - 7, 30 - 3, 40 - 7 }; + start -= pt; + VERIFY_ARE_EQUAL(expected, start); + } + + TEST_METHOD(AdditionSize) + { + const til::rectangle start{ 10, 20, 30, 40 }; + + Log::Comment(L"1.) Add size to bottom and right"); + { + const til::size scale{ 3, 7 }; + const til::rectangle expected{ 10, 20, 33, 47 }; + const auto actual = start + scale; + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"2.) Add size to top and left"); + { + const til::size scale{ -3, -7 }; + const til::rectangle expected{ 7, 13, 30, 40 }; + const auto actual = start + scale; + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"3.) Add size to bottom and left"); + { + const til::size scale{ -3, 7 }; + const til::rectangle expected{ 7, 20, 30, 47 }; + const auto actual = start + scale; + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"4.) Add size to top and right"); + { + const til::size scale{ 3, -7 }; + const til::rectangle expected{ 10, 13, 33, 40 }; + const auto actual = start + scale; + VERIFY_ARE_EQUAL(expected, actual); + } + } + + TEST_METHOD(AdditionSizeInplace) + { + const til::rectangle start{ 10, 20, 30, 40 }; + + Log::Comment(L"1.) Add size to bottom and right"); + { + auto actual = start; + const til::size scale{ 3, 7 }; + const til::rectangle expected{ 10, 20, 33, 47 }; + actual += scale; + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"2.) Add size to top and left"); + { + auto actual = start; + const til::size scale{ -3, -7 }; + const til::rectangle expected{ 7, 13, 30, 40 }; + actual += scale; + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"3.) Add size to bottom and left"); + { + auto actual = start; + const til::size scale{ -3, 7 }; + const til::rectangle expected{ 7, 20, 30, 47 }; + actual += scale; + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"4.) Add size to top and right"); + { + auto actual = start; + const til::size scale{ 3, -7 }; + const til::rectangle expected{ 10, 13, 33, 40 }; + actual += scale; + VERIFY_ARE_EQUAL(expected, actual); + } + } + + TEST_METHOD(SubtractionSize) + { + const til::rectangle start{ 10, 20, 30, 40 }; + + Log::Comment(L"1.) Subtract size from bottom and right"); + { + const til::size scale{ 3, 7 }; + const til::rectangle expected{ 10, 20, 27, 33 }; + const auto actual = start - scale; + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"2.) Subtract size from top and left"); + { + const til::size scale{ -3, -7 }; + const til::rectangle expected{ 13, 27, 30, 40 }; + const auto actual = start - scale; + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"3.) Subtract size from bottom and left"); + { + const til::size scale{ -3, 7 }; + const til::rectangle expected{ 13, 20, 30, 33 }; + const auto actual = start - scale; + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"4.) Subtract size from top and right"); + { + const til::size scale{ 3, -6 }; + const til::rectangle expected{ 10, 26, 27, 40 }; + const auto actual = start - scale; + VERIFY_ARE_EQUAL(expected, actual); + } + } + + TEST_METHOD(SubtractionSizeInplace) + { + const til::rectangle start{ 10, 20, 30, 40 }; + + Log::Comment(L"1.) Subtract size from bottom and right"); + { + auto actual = start; + const til::size scale{ 3, 7 }; + const til::rectangle expected{ 10, 20, 27, 33 }; + actual -= scale; + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"2.) Subtract size from top and left"); + { + auto actual = start; + const til::size scale{ -3, -7 }; + const til::rectangle expected{ 13, 27, 30, 40 }; + actual -= scale; + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"3.) Subtract size from bottom and left"); + { + auto actual = start; + const til::size scale{ -3, 7 }; + const til::rectangle expected{ 13, 20, 30, 33 }; + actual -= scale; + VERIFY_ARE_EQUAL(expected, actual); + } + + Log::Comment(L"4.) Subtract size from top and right"); + { + auto actual = start; + const til::size scale{ 3, -6 }; + const til::rectangle expected{ 10, 26, 27, 40 }; + actual -= scale; + VERIFY_ARE_EQUAL(expected, actual); + } + } + TEST_METHOD(Top) { const til::rectangle rc{ 5, 10, 15, 20 }; diff --git a/src/til/ut_til/sources b/src/til/ut_til/sources index 2e7861cdfb5..af602729cd6 100644 --- a/src/til/ut_til/sources +++ b/src/til/ut_til/sources @@ -14,8 +14,14 @@ DLLDEF = SOURCES = \ $(SOURCES) \ + BitmapTests.cpp \ ColorTests.cpp \ + OperatorTests.cpp \ + PointTests.cpp \ + RectangleTests.cpp \ + SizeTests.cpp \ SomeTests.cpp \ + u8u16convertTests.cpp \ DefaultResource.rc \ INCLUDES = \ diff --git a/src/til/ut_til/til.unit.tests.vcxproj b/src/til/ut_til/til.unit.tests.vcxproj index 644322da1c1..d2e91bf250d 100644 --- a/src/til/ut_til/til.unit.tests.vcxproj +++ b/src/til/ut_til/til.unit.tests.vcxproj @@ -11,6 +11,7 @@ + diff --git a/src/til/ut_til/til.unit.tests.vcxproj.filters b/src/til/ut_til/til.unit.tests.vcxproj.filters index e07ae69b990..21bf4896239 100644 --- a/src/til/ut_til/til.unit.tests.vcxproj.filters +++ b/src/til/ut_til/til.unit.tests.vcxproj.filters @@ -12,6 +12,7 @@ +