diff --git a/src/inc/til.h b/src/inc/til.h index fce071cf1..1519f9245 100644 --- a/src/inc/til.h +++ b/src/inc/til.h @@ -6,6 +6,7 @@ #include "til/at.h" #include "til/color.h" #include "til/some.h" +#include "til/size.h" #include "til/u8u16convert.h" namespace til // Terminal Implementation Library. Also: "Today I Learned" diff --git a/src/inc/til/size.h b/src/inc/til/size.h new file mode 100644 index 000000000..f996c9325 --- /dev/null +++ b/src/inc/til/size.h @@ -0,0 +1,265 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#pragma once + +#ifdef UNIT_TESTING +class SizeTests; +#endif + +namespace til // Terminal Implementation Library. Also: "Today I Learned" +{ + class size + { + public: + constexpr size() noexcept : + size(0, 0) + { + } + + // On 64-bit processors, int and ptrdiff_t are different fundamental types. + // On 32-bit processors, they're the same which makes this a double-definition + // with the `ptrdiff_t` one below. +#if defined(_M_AMD64) || defined(_M_ARM64) + constexpr size(int width, int height) noexcept : + size(static_cast(width), static_cast(height)) + { + } +#endif + + size(size_t width, size_t height) + { + THROW_HR_IF(E_ABORT, !base::MakeCheckedNum(width).AssignIfValid(&_width)); + THROW_HR_IF(E_ABORT, !base::MakeCheckedNum(height).AssignIfValid(&_height)); + } + + constexpr size(ptrdiff_t width, ptrdiff_t height) noexcept : + _width(width), + _height(height) + { + } + + // This template will convert to size from anything that has an X and a Y field that appear convertable to an integer value + template + constexpr size(const TOther& other, std::enable_if_t().X)> && std::is_integral_v().Y)>, int> /*sentinel*/ = 0) : + size(static_cast(other.X), static_cast(other.Y)) + { + } + + // This template will convert to size from anything that has a cx and a cy field that appear convertable to an integer value + template + constexpr size(const TOther& other, std::enable_if_t().cx)> && std::is_integral_v().cy)>, int> /*sentinel*/ = 0) : + size(static_cast(other.cx), static_cast(other.cy)) + { + } + + constexpr bool operator==(const size& other) const noexcept + { + return _width == other._width && + _height == other._height; + } + + constexpr bool operator!=(const size& other) const noexcept + { + return !(*this == other); + } + + size operator+(const size& other) const + { + ptrdiff_t width; + THROW_HR_IF(E_ABORT, !base::CheckAdd(_width, other._width).AssignIfValid(&width)); + + ptrdiff_t height; + THROW_HR_IF(E_ABORT, !base::CheckAdd(_height, other._height).AssignIfValid(&height)); + + return size{ width, height }; + } + + size operator-(const size& other) const + { + ptrdiff_t width; + THROW_HR_IF(E_ABORT, !base::CheckSub(_width, other._width).AssignIfValid(&width)); + + ptrdiff_t height; + THROW_HR_IF(E_ABORT, !base::CheckSub(_height, other._height).AssignIfValid(&height)); + + return size{ width, height }; + } + + size operator*(const size& other) const + { + ptrdiff_t width; + THROW_HR_IF(E_ABORT, !base::CheckMul(_width, other._width).AssignIfValid(&width)); + + ptrdiff_t height; + THROW_HR_IF(E_ABORT, !base::CheckMul(_height, other._height).AssignIfValid(&height)); + + return size{ width, height }; + } + + size operator/(const size& other) const + { + ptrdiff_t width; + THROW_HR_IF(E_ABORT, !base::CheckDiv(_width, other._width).AssignIfValid(&width)); + + ptrdiff_t height; + THROW_HR_IF(E_ABORT, !base::CheckDiv(_height, other._height).AssignIfValid(&height)); + + return size{ width, height }; + } + + size divide_ceil(const size& other) const + { + // Divide normally to get the floor. + const size floor = *this / other; + + ptrdiff_t adjWidth = 0; + ptrdiff_t adjHeight = 0; + + // Check for width remainder, anything not 0. + // If we multiply the floored number with the other, it will equal + // the old width if there was no remainder. + if (other._width * floor._width != _width) + { + // If there was any remainder, + // Grow the magnitude by 1 in the + // direction of the sign. + if (floor.width() >= 0) + { + ++adjWidth; + } + else + { + --adjWidth; + } + } + + // Check for height remainder, anything not 0. + // If we multiply the floored number with the other, it will equal + // the old width if there was no remainder. + if (other._height * floor._height != _height) + { + // If there was any remainder, + // Grow the magnitude by 1 in the + // direction of the sign. + if (_height >= 0) + { + ++adjHeight; + } + else + { + --adjHeight; + } + } + + return floor + size{ adjWidth, adjHeight }; + } + + constexpr ptrdiff_t width() const noexcept + { + return _width; + } + + template + T width() const + { + T ret; + THROW_HR_IF(E_ABORT, !base::MakeCheckedNum(width()).AssignIfValid(&ret)); + return ret; + } + + constexpr ptrdiff_t height() const noexcept + { + return _height; + } + + template + T height() const + { + T ret; + THROW_HR_IF(E_ABORT, !base::MakeCheckedNum(height()).AssignIfValid(&ret)); + return ret; + } + + ptrdiff_t area() const + { + ptrdiff_t result; + THROW_HR_IF(E_ABORT, !base::CheckMul(_width, _height).AssignIfValid(&result)); + return result; + } + +#ifdef _WINCONTYPES_ + operator COORD() const + { + COORD ret; + THROW_HR_IF(E_ABORT, !base::MakeCheckedNum(_width).AssignIfValid(&ret.X)); + THROW_HR_IF(E_ABORT, !base::MakeCheckedNum(_height).AssignIfValid(&ret.Y)); + return ret; + } +#endif + +#ifdef _WINDEF_ + operator SIZE() const + { + SIZE ret; + THROW_HR_IF(E_ABORT, !base::MakeCheckedNum(_width).AssignIfValid(&ret.cx)); + THROW_HR_IF(E_ABORT, !base::MakeCheckedNum(_height).AssignIfValid(&ret.cy)); + return ret; + } +#endif + +#ifdef DCOMMON_H_INCLUDED + constexpr operator D2D1_SIZE_F() const noexcept + { + return D2D1_SIZE_F{ gsl::narrow_cast(_width), gsl::narrow_cast(_height) }; + } +#endif + + protected: + ptrdiff_t _width; + ptrdiff_t _height; + +#ifdef UNIT_TESTING + friend class ::SizeTests; +#endif + }; +}; + +#ifdef __WEX_COMMON_H__ +namespace WEX::TestExecution +{ + template<> + class VerifyOutputTraits<::til::size> + { + public: + static WEX::Common::NoThrowString ToString(const ::til::size& size) + { + return WEX::Common::NoThrowString().Format(L"[W:%td, H:%td]", size.width(), size.height()); + } + }; + + template<> + class VerifyCompareTraits<::til::size, ::til::size> + { + public: + static bool AreEqual(const ::til::size& expected, const ::til::size& actual) noexcept + { + return expected == actual; + } + + static bool AreSame(const ::til::size& expected, const ::til::size& actual) noexcept + { + return &expected == &actual; + } + + static bool IsLessThan(const ::til::size& expectedLess, const ::til::size& expectedGreater) = delete; + + static bool IsGreaterThan(const ::til::size& expectedGreater, const ::til::size& expectedLess) = delete; + + static bool IsNull(const ::til::size& object) noexcept + { + return object == til::size{}; + } + }; +}; +#endif diff --git a/src/til/precomp.h b/src/til/precomp.h index 6e68de7f6..2375e15ec 100644 --- a/src/til/precomp.h +++ b/src/til/precomp.h @@ -25,7 +25,17 @@ Abstract: // Windows Header Files: #include +#define BLOCK_TIL // This includes support libraries from the CRT, STL, WIL, and GSL #include "LibraryIncludes.h" +#include "WexTestClass.h" + +// Include DirectX common structures so we can test them. +// (before TIL so its support lights up) +#include + +// Include TIL after Wex to get test comparators. +#include "til.h" + // clang-format on diff --git a/src/til/ut_til/SizeTests.cpp b/src/til/ut_til/SizeTests.cpp new file mode 100644 index 000000000..09b573698 --- /dev/null +++ b/src/til/ut_til/SizeTests.cpp @@ -0,0 +1,494 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "til/size.h" + +using namespace WEX::Common; +using namespace WEX::Logging; +using namespace WEX::TestExecution; + +class SizeTests +{ + TEST_CLASS(SizeTests); + + TEST_METHOD(DefaultConstruct) + { + const til::size sz; + VERIFY_ARE_EQUAL(0, sz._width); + VERIFY_ARE_EQUAL(0, sz._height); + } + + TEST_METHOD(RawConstruct) + { + const til::size sz{ 5, 10 }; + VERIFY_ARE_EQUAL(5, sz._width); + VERIFY_ARE_EQUAL(10, sz._height); + } + + TEST_METHOD(UnsignedConstruct) + { + Log::Comment(L"0.) Normal unsigned construct."); + { + const size_t width = 5; + const size_t height = 10; + + const til::size sz{ width, height }; + VERIFY_ARE_EQUAL(5, sz._width); + VERIFY_ARE_EQUAL(10, sz._height); + } + + Log::Comment(L"1.) Unsigned construct overflow on width."); + { + constexpr size_t width = std::numeric_limits().max(); + const size_t height = 10; + + auto fn = [&]() { + til::size sz{ width, height }; + }; + + VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + } + + Log::Comment(L"2.) Unsigned construct overflow on height."); + { + constexpr size_t height = std::numeric_limits().max(); + const size_t width = 10; + + auto fn = [&]() { + til::size sz{ width, height }; + }; + + VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + } + } + + TEST_METHOD(SignedConstruct) + { + const ptrdiff_t width = -5; + const ptrdiff_t height = -10; + + const til::size sz{ width, height }; + VERIFY_ARE_EQUAL(width, sz._width); + VERIFY_ARE_EQUAL(height, sz._height); + } + + TEST_METHOD(CoordConstruct) + { + COORD coord{ -5, 10 }; + + const til::size sz{ coord }; + VERIFY_ARE_EQUAL(coord.X, sz._width); + VERIFY_ARE_EQUAL(coord.Y, sz._height); + } + + TEST_METHOD(SizeConstruct) + { + SIZE size{ 5, -10 }; + + const til::size sz{ size }; + VERIFY_ARE_EQUAL(size.cx, sz._width); + VERIFY_ARE_EQUAL(size.cy, sz._height); + } + + TEST_METHOD(Equality) + { + Log::Comment(L"0.) Equal."); + { + const til::size s1{ 5, 10 }; + const til::size s2{ 5, 10 }; + VERIFY_IS_TRUE(s1 == s2); + } + + Log::Comment(L"1.) Left Width changed."); + { + const til::size s1{ 4, 10 }; + const til::size s2{ 5, 10 }; + VERIFY_IS_FALSE(s1 == s2); + } + + Log::Comment(L"2.) Right Width changed."); + { + const til::size s1{ 5, 10 }; + const til::size s2{ 6, 10 }; + VERIFY_IS_FALSE(s1 == s2); + } + + Log::Comment(L"3.) Left Height changed."); + { + const til::size s1{ 5, 9 }; + const til::size s2{ 5, 10 }; + VERIFY_IS_FALSE(s1 == s2); + } + + Log::Comment(L"4.) Right Height changed."); + { + const til::size s1{ 5, 10 }; + const til::size s2{ 5, 11 }; + VERIFY_IS_FALSE(s1 == s2); + } + } + + TEST_METHOD(Inequality) + { + Log::Comment(L"0.) Equal."); + { + const til::size s1{ 5, 10 }; + const til::size s2{ 5, 10 }; + VERIFY_IS_FALSE(s1 != s2); + } + + Log::Comment(L"1.) Left Width changed."); + { + const til::size s1{ 4, 10 }; + const til::size s2{ 5, 10 }; + VERIFY_IS_TRUE(s1 != s2); + } + + Log::Comment(L"2.) Right Width changed."); + { + const til::size s1{ 5, 10 }; + const til::size s2{ 6, 10 }; + VERIFY_IS_TRUE(s1 != s2); + } + + Log::Comment(L"3.) Left Height changed."); + { + const til::size s1{ 5, 9 }; + const til::size s2{ 5, 10 }; + VERIFY_IS_TRUE(s1 != s2); + } + + Log::Comment(L"4.) Right Height changed."); + { + const til::size s1{ 5, 10 }; + const til::size s2{ 5, 11 }; + VERIFY_IS_TRUE(s1 != s2); + } + } + + TEST_METHOD(Addition) + { + Log::Comment(L"0.) Addition of two things that should be in bounds."); + { + const til::size sz{ 5, 10 }; + const til::size sz2{ 23, 47 }; + + const til::size expected{ sz.width() + sz2.width(), sz.height() + sz2.height() }; + + VERIFY_ARE_EQUAL(expected, sz + sz2); + } + + Log::Comment(L"1.) Addition results in value that is too large (width)."); + { + constexpr ptrdiff_t bigSize = std::numeric_limits().max(); + const til::size sz{ bigSize, static_cast(0) }; + const til::size sz2{ 1, 1 }; + + auto fn = [&]() { + sz + sz2; + }; + + VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + } + + Log::Comment(L"2.) Addition results in value that is too large (height)."); + { + constexpr ptrdiff_t bigSize = std::numeric_limits().max(); + const til::size sz{ static_cast(0), bigSize }; + const til::size sz2{ 1, 1 }; + + auto fn = [&]() { + sz + sz2; + }; + + VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + } + } + + TEST_METHOD(Subtraction) + { + Log::Comment(L"0.) Subtraction of two things that should be in bounds."); + { + const til::size sz{ 5, 10 }; + const til::size sz2{ 23, 47 }; + + const til::size expected{ sz.width() - sz2.width(), sz.height() - sz2.height() }; + + VERIFY_ARE_EQUAL(expected, sz - sz2); + } + + Log::Comment(L"1.) Subtraction results in value that is too small (width)."); + { + constexpr ptrdiff_t bigSize = std::numeric_limits().max(); + const til::size sz{ bigSize, static_cast(0) }; + const til::size sz2{ -2, -2 }; + + auto fn = [&]() { + sz2 - sz; + }; + + VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + } + + Log::Comment(L"2.) Subtraction results in value that is too small (height)."); + { + constexpr ptrdiff_t bigSize = std::numeric_limits().max(); + const til::size sz{ static_cast(0), bigSize }; + const til::size sz2{ -2, -2 }; + + auto fn = [&]() { + sz2 - sz; + }; + + VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + } + } + + TEST_METHOD(Multiplication) + { + Log::Comment(L"0.) Multiplication of two things that should be in bounds."); + { + const til::size sz{ 5, 10 }; + const til::size sz2{ 23, 47 }; + + const til::size expected{ sz.width() * sz2.width(), sz.height() * sz2.height() }; + + VERIFY_ARE_EQUAL(expected, sz * sz2); + } + + Log::Comment(L"1.) Multiplication results in value that is too large (width)."); + { + constexpr ptrdiff_t bigSize = std::numeric_limits().max(); + const til::size sz{ bigSize, static_cast(0) }; + const til::size sz2{ 10, 10 }; + + auto fn = [&]() { + sz* sz2; + }; + + VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + } + + Log::Comment(L"2.) Multiplication results in value that is too large (height)."); + { + constexpr ptrdiff_t bigSize = std::numeric_limits().max(); + const til::size sz{ static_cast(0), bigSize }; + const til::size sz2{ 10, 10 }; + + auto fn = [&]() { + sz* sz2; + }; + + VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + } + } + + TEST_METHOD(Division) + { + Log::Comment(L"0.) Division of two things that should be in bounds."); + { + const til::size sz{ 555, 510 }; + const til::size sz2{ 23, 47 }; + + const til::size expected{ sz.width() / sz2.width(), sz.height() / sz2.height() }; + + VERIFY_ARE_EQUAL(expected, sz / sz2); + } + + Log::Comment(L"1.) Division by zero"); + { + constexpr ptrdiff_t bigSize = std::numeric_limits().max(); + const til::size sz{ bigSize, static_cast(0) }; + const til::size sz2{ 1, 1 }; + + auto fn = [&]() { + sz2 / sz; + }; + + VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + } + } + + TEST_METHOD(DivisionRoundingUp) + { + Log::Comment(L"1.) Division rounding up with positive result."); + { + const til::size sz{ 10, 5 }; + const til::size divisor{ 3, 2 }; + + // 10 / 3 is 3.333, rounded up is 4. + // 5 / 2 is 2.5, rounded up is 3. + const til::size expected{ 4, 3 }; + + VERIFY_ARE_EQUAL(expected, sz.divide_ceil(divisor)); + } + + Log::Comment(L"2.) Division rounding larger(up) with negative result."); + { + const til::size sz{ -10, -5 }; + const til::size divisor{ 3, 2 }; + + // -10 / 3 is -3.333, rounded up is -4. + // -5 / 2 is -2.5, rounded up is -3. + const til::size expected{ -4, -3 }; + + VERIFY_ARE_EQUAL(expected, sz.divide_ceil(divisor)); + } + } + + TEST_METHOD(Width) + { + const til::size sz{ 5, 10 }; + VERIFY_ARE_EQUAL(sz._width, sz.width()); + } + + TEST_METHOD(WidthCast) + { + const til::size sz{ 5, 10 }; + VERIFY_ARE_EQUAL(static_cast(sz._width), sz.width()); + } + + TEST_METHOD(Height) + { + const til::size sz{ 5, 10 }; + VERIFY_ARE_EQUAL(sz._height, sz.height()); + } + + TEST_METHOD(HeightCast) + { + const til::size sz{ 5, 10 }; + VERIFY_ARE_EQUAL(static_cast(sz._height), sz.height()); + } + + TEST_METHOD(Area) + { + Log::Comment(L"0.) Area of two things that should be in bounds."); + { + const til::size sz{ 5, 10 }; + VERIFY_ARE_EQUAL(sz._width * sz._height, sz.area()); + } + + Log::Comment(L"1.) Area is out of bounds on multiplication."); + { + constexpr ptrdiff_t bigSize = std::numeric_limits().max(); + const til::size sz{ bigSize, bigSize }; + + auto fn = [&]() { + sz.area(); + }; + + VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + } + } + + TEST_METHOD(CastToCoord) + { + Log::Comment(L"0.) Typical situation."); + { + const til::size sz{ 5, 10 }; + COORD val = sz; + VERIFY_ARE_EQUAL(5, val.X); + VERIFY_ARE_EQUAL(10, val.Y); + } + + Log::Comment(L"1.) Overflow on width."); + { + constexpr ptrdiff_t width = std::numeric_limits().max(); + const ptrdiff_t height = 10; + const til::size sz{ width, height }; + + auto fn = [&]() { + COORD val = sz; + }; + + VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + } + + Log::Comment(L"2.) Overflow on height."); + { + constexpr ptrdiff_t height = std::numeric_limits().max(); + const ptrdiff_t width = 10; + const til::size sz{ width, height }; + + auto fn = [&]() { + COORD val = sz; + }; + + VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + } + } + + TEST_METHOD(CastToSize) + { + Log::Comment(L"0.) Typical situation."); + { + const til::size sz{ 5, 10 }; + SIZE val = sz; + VERIFY_ARE_EQUAL(5, val.cx); + VERIFY_ARE_EQUAL(10, val.cy); + } + + Log::Comment(L"1.) Fit max width into SIZE (may overflow)."); + { + constexpr ptrdiff_t width = std::numeric_limits().max(); + const ptrdiff_t height = 10; + const til::size sz{ width, height }; + + // On some platforms, ptrdiff_t will fit inside cx/cy + const bool overflowExpected = width > std::numeric_limits().max(); + + if (overflowExpected) + { + auto fn = [&]() { + SIZE val = sz; + }; + + VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + } + else + { + SIZE val = sz; + VERIFY_ARE_EQUAL(width, val.cx); + } + } + + Log::Comment(L"2.) Fit max height into SIZE (may overflow)."); + { + constexpr ptrdiff_t height = std::numeric_limits().max(); + const ptrdiff_t width = 10; + const til::size sz{ width, height }; + + // On some platforms, ptrdiff_t will fit inside cx/cy + const bool overflowExpected = height > std::numeric_limits().max(); + + if (overflowExpected) + { + auto fn = [&]() { + SIZE val = sz; + }; + + VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; }); + } + else + { + SIZE val = sz; + VERIFY_ARE_EQUAL(height, val.cy); + } + } + } + + TEST_METHOD(CastToD2D1SizeF) + { + Log::Comment(L"0.) Typical situation."); + { + const til::size sz{ 5, 10 }; + D2D1_SIZE_F val = sz; + VERIFY_ARE_EQUAL(5, val.width); + VERIFY_ARE_EQUAL(10, val.height); + } + + // All ptrdiff_ts fit into a float, so there's no exception tests. + } +}; diff --git a/src/til/ut_til/SomeTests.cpp b/src/til/ut_til/SomeTests.cpp index 981a726cd..e7a1c0258 100644 --- a/src/til/ut_til/SomeTests.cpp +++ b/src/til/ut_til/SomeTests.cpp @@ -2,7 +2,6 @@ // Licensed under the MIT license. #include "precomp.h" -#include "WexTestClass.h" using namespace WEX::Common; using namespace WEX::Logging; diff --git a/src/til/ut_til/til.unit.tests.vcxproj b/src/til/ut_til/til.unit.tests.vcxproj index bfa70c72f..750931c19 100644 --- a/src/til/ut_til/til.unit.tests.vcxproj +++ b/src/til/ut_til/til.unit.tests.vcxproj @@ -10,6 +10,7 @@ + diff --git a/src/til/ut_til/til.unit.tests.vcxproj.filters b/src/til/ut_til/til.unit.tests.vcxproj.filters new file mode 100644 index 000000000..c78750fac --- /dev/null +++ b/src/til/ut_til/til.unit.tests.vcxproj.filters @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/ConsoleTypes.natvis b/tools/ConsoleTypes.natvis index 31340c96d..8d09a1a67 100644 --- a/tools/ConsoleTypes.natvis +++ b/tools/ConsoleTypes.natvis @@ -80,7 +80,11 @@ {{↓ wch:{_charData} mod:{_activeModifierKeys} repeat:{_repeatCount} vk:{_virtualKeyCode} vsc:{_virtualScanCode}} {{↑ wch:{_charData} mod:{_activeModifierKeys} repeat:{_repeatCount} vk:{_virtualKeyCode} vsc:{_virtualScanCode}} - + + + {{W: {_width,d} x H: {_height,d} -> A: {_width * _height, d}}} + + {{RGB: {(int)r,d}, {(int)g,d}, {(int)b,d}; α: {(int)a,d}}}