Add `til::property` and other winrt helpers (#15029)

## Summary of the Pull Request

This was a fever dream I had last July. What if, instead of `WINRT_PROPERTY` magic macros everywhere, we had actual templated versions you could debug into. 

So instead of 

```c++
WINRT_PROPERTY(bool, Deleted, false);
WINRT_PROPERTY(OriginTag, Origin, OriginTag::None);
WINRT_PROPERTY(guid, Updates);
```

you'd do 

```c++
til::property<bool> Deleted{ false };
til::property<OriginTag> Origin{ OriginTag::None };
til::property<guid> Updates;
```

.... and then I just kinda kept doing that. So I did that for `til::event`.

**AND THEN LAST WEEK**

Raymond Chen was like: ["this is a good idea"](https://devblogs.microsoft.com/oldnewthing/20230317-00/?p=107946)

So here it is. 

## Validation Steps Performed
Added some simple tests.

Co-authored-by: Leonard Hecker <lhecker@microsoft.com>
This commit is contained in:
Mike Griese 2023-05-03 12:41:36 -05:00 committed by GitHub
parent 23d45a7e3a
commit ae7595b8e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 372 additions and 9 deletions

View File

@ -100,6 +100,7 @@ TLDR
tokenizes
tonos
toolset
truthiness
tshe
ubuntu
uiatextrange

View File

@ -65,13 +65,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation
TextColor SelectionColor::AsTextColor() const noexcept
{
if (_IsIndex16)
if (IsIndex16())
{
return { _Color.r, false };
return { Color().r, false };
}
else
{
return { static_cast<COLORREF>(_Color) };
return { static_cast<COLORREF>(Color()) };
}
}

View File

@ -52,8 +52,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
{
TextColor AsTextColor() const noexcept;
WINRT_PROPERTY(til::color, Color);
WINRT_PROPERTY(bool, IsIndex16);
til::property<til::color> Color;
til::property<bool> IsIndex16;
};
struct ControlCore : ControlCoreT<ControlCore>

View File

@ -70,6 +70,7 @@ TRACELOGGING_DECLARE_PROVIDER(g_hTerminalControlProvider);
#include "til.h"
#include <til/mutex.h>
#include <til/winrt.h>
#include "ThrottledFunc.h"

View File

@ -46,6 +46,7 @@ Licensed under the MIT license.
// Manually include til after we include Windows.Foundation to give it winrt superpowers
#include "til.h"
#include <til/winrt.h>
#include "ThrottledFunc.h"

View File

@ -7,6 +7,7 @@
#include "../../inc/ControlProperties.h"
#include <winrt/Microsoft.Terminal.Core.h>
#include <til/winrt.h>
using namespace winrt::Microsoft::Terminal::Core;
@ -17,16 +18,17 @@ namespace TerminalCoreUnitTests
// Color Table is special because it's an array
std::array<winrt::Microsoft::Terminal::Core::Color, COLOR_TABLE_SIZE> _ColorTable;
#define SETTINGS_GEN(type, name, ...) WINRT_PROPERTY(type, name, __VA_ARGS__);
public:
#define SETTINGS_GEN(type, name, ...) til::property<type> name{ __VA_ARGS__ };
CORE_SETTINGS(SETTINGS_GEN)
CORE_APPEARANCE_SETTINGS(SETTINGS_GEN)
#undef SETTINGS_GEN
public:
MockTermSettings(int32_t historySize, int32_t initialRows, int32_t initialCols) :
_HistorySize(historySize),
_InitialRows(initialRows),
_InitialCols(initialCols)
HistorySize(historySize),
InitialRows(initialRows),
InitialCols(initialCols)
{
}

View File

@ -0,0 +1,246 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include <WexTestClass.h>
#include <DefaultSettings.h>
#include "../renderer/inc/DummyRenderer.hpp"
#include "../renderer/base/Renderer.hpp"
#include "../renderer/dx/DxRenderer.hpp"
#include "../cascadia/TerminalCore/Terminal.hpp"
#include "MockTermSettings.h"
#include "consoletaeftemplates.hpp"
#include "../../inc/TestUtils.h"
#include <til/winrt.h>
using namespace winrt::Microsoft::Terminal::Core;
using namespace Microsoft::Terminal::Core;
using namespace Microsoft::Console::Render;
using namespace ::Microsoft::Console::Types;
using namespace WEX::Common;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
namespace TerminalCoreUnitTests
{
class TilWinRtHelpersTests;
};
using namespace TerminalCoreUnitTests;
class TerminalCoreUnitTests::TilWinRtHelpersTests final
{
TEST_CLASS(TilWinRtHelpersTests);
TEST_METHOD(TestPropertySimple);
TEST_METHOD(TestPropertyHString);
TEST_METHOD(TestTruthiness);
TEST_METHOD(TestSimpleConstProperties);
TEST_METHOD(TestComposedConstProperties);
TEST_METHOD(TestEvent);
TEST_METHOD(TestTypedEvent);
};
void TilWinRtHelpersTests::TestPropertySimple()
{
til::property<int> Foo;
til::property<int> Bar(11);
VERIFY_ARE_EQUAL(11, Bar());
Foo(42);
VERIFY_ARE_EQUAL(42, Foo());
Foo(Foo() - 5); // 37
VERIFY_ARE_EQUAL(37, Foo());
Foo(Foo() + Bar()); // 48
VERIFY_ARE_EQUAL(48, Foo());
}
void TilWinRtHelpersTests::TestPropertyHString()
{
til::property<winrt::hstring> Foo{ L"Foo" };
VERIFY_ARE_EQUAL(L"Foo", Foo());
Foo(L"bar");
VERIFY_ARE_EQUAL(L"bar", Foo());
}
void TilWinRtHelpersTests::TestTruthiness()
{
til::property<bool> Foo{ false };
til::property<int> Bar(0);
til::property<winrt::hstring> EmptyString;
til::property<winrt::hstring> FullString{ L"Full" };
VERIFY_IS_FALSE(Foo());
VERIFY_IS_FALSE(Foo);
VERIFY_IS_FALSE(Bar());
VERIFY_IS_FALSE(Bar);
VERIFY_IS_FALSE(EmptyString);
VERIFY_IS_FALSE(!EmptyString().empty());
Foo(true);
VERIFY_IS_TRUE(Foo());
VERIFY_IS_TRUE(Foo);
Bar(11);
VERIFY_IS_TRUE(Bar());
VERIFY_IS_TRUE(Bar);
VERIFY_IS_TRUE(FullString);
VERIFY_IS_TRUE(!FullString().empty());
}
void TilWinRtHelpersTests::TestSimpleConstProperties()
{
struct InnerType
{
int first{ 1 };
int second{ 2 };
};
struct Helper
{
til::property<int> Foo{ 0 };
til::property<struct InnerType> Composed;
til::property<winrt::hstring> MyString;
};
struct Helper changeMe;
const struct Helper noTouching;
VERIFY_ARE_EQUAL(0, changeMe.Foo());
VERIFY_ARE_EQUAL(1, changeMe.Composed().first);
VERIFY_ARE_EQUAL(2, changeMe.Composed().second);
VERIFY_ARE_EQUAL(L"", changeMe.MyString());
VERIFY_ARE_EQUAL(0, noTouching.Foo());
VERIFY_ARE_EQUAL(1, noTouching.Composed().first);
VERIFY_ARE_EQUAL(2, noTouching.Composed().second);
VERIFY_ARE_EQUAL(L"", noTouching.MyString());
changeMe.Foo(42);
VERIFY_ARE_EQUAL(42, changeMe.Foo());
// noTouching.Foo = 123; // will not compile
// None of this compiles.
// Composed() doesn't return an l-value, it returns an _int_
//
// changeMe.Composed().first = 5;
// VERIFY_ARE_EQUAL(5, changeMe.Composed().first);
// noTouching.Composed().first = 0x0f; // will not compile
changeMe.MyString(L"Foo");
VERIFY_ARE_EQUAL(L"Foo", changeMe.MyString());
// noTouching.MyString = L"Bar"; // will not compile
}
void TilWinRtHelpersTests::TestComposedConstProperties()
{
// This is an intentionally obtuse test, to show a weird edge case you
// should avoid.
//
// In this sample, `Helper` has a `property` of a raw struct
// `InnerType`, which itself is composed of two `property`s. This is not
// something that will actually occur in practice. In practice, the things
// inside the `property` will be WinRT types (or primitive types), and
// things that contain properties will THEMSELVES be WinRT types.
//
// But if you do it like this, you can't call
//
// changeMe.Composed().first(5);
//
// Or any variation of that, without ~ unexpected ~ behavior. This demonstrates that.
struct InnerType
{
til::property<int> first{ 3 };
til::property<int> second{ 2 };
};
struct Helper
{
til::property<int> Foo{ 0 };
til::property<struct InnerType> Composed;
til::property<winrt::hstring> MyString;
};
struct Helper changeMe;
const struct Helper noTouching;
VERIFY_ARE_EQUAL(0, changeMe.Foo());
VERIFY_ARE_EQUAL(3, changeMe.Composed().first);
VERIFY_ARE_EQUAL(2, changeMe.Composed().second);
VERIFY_ARE_EQUAL(L"", changeMe.MyString());
VERIFY_ARE_EQUAL(0, noTouching.Foo());
VERIFY_ARE_EQUAL(3, noTouching.Composed().first);
VERIFY_ARE_EQUAL(2, noTouching.Composed().second);
VERIFY_ARE_EQUAL(L"", noTouching.MyString());
changeMe.Foo(42);
VERIFY_ARE_EQUAL(42, changeMe.Foo());
// noTouching.Foo = 123; // will not compile
// This test was authored to work through a potential foot gun.
// If you have property::operator() return `T`, then
// changeMe.Composed().first = 5;
//
// Roughly translates to:
// auto copy = changeMe.Composed();
// copy.first(5);
//
// Which rather seems like a foot gun.
changeMe.Composed().first(5);
VERIFY_ARE_EQUAL(3, changeMe.Composed().first());
// IN PRACTICE, this shouldn't ever occur. Composed would be a WinRT type,
// and you'd get a ref to it, rather than a copy.
changeMe.MyString(L"Foo");
VERIFY_ARE_EQUAL(L"Foo", changeMe.MyString());
}
void TilWinRtHelpersTests::TestEvent()
{
bool handledOne = false;
bool handledTwo = false;
auto handler = [&](const int& v) -> void {
VERIFY_ARE_EQUAL(42, v);
handledOne = true;
};
til::event<winrt::delegate<void(int)>> MyEvent;
MyEvent(handler);
MyEvent([&](int) { handledTwo = true; });
MyEvent.raise(42);
VERIFY_ARE_EQUAL(true, handledOne);
VERIFY_ARE_EQUAL(true, handledTwo);
}
void TilWinRtHelpersTests::TestTypedEvent()
{
bool handledOne = false;
bool handledTwo = false;
auto handler = [&](const winrt::hstring sender, const int& v) -> void {
VERIFY_ARE_EQUAL(L"sure", sender);
VERIFY_ARE_EQUAL(42, v);
handledOne = true;
};
til::typed_event<winrt::hstring, int> MyEvent;
MyEvent(handler);
MyEvent([&](winrt::hstring, int) { handledTwo = true; });
MyEvent.raise(L"sure", 42);
VERIFY_ARE_EQUAL(true, handledOne);
VERIFY_ARE_EQUAL(true, handledTwo);
}

View File

@ -26,6 +26,7 @@
<ClCompile Include="ConptyRoundtripTests.cpp" />
<ClCompile Include="TerminalBufferTests.cpp" />
<ClCompile Include="ScrollTest.cpp" />
<ClCompile Include="TilWinRtHelpersTests.cpp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\buffer\out\lib\bufferout.vcxproj">

111
src/inc/til/winrt.h Normal file
View File

@ -0,0 +1,111 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
namespace til // Terminal Implementation Library. Also: "Today I Learned"
{
template<typename T>
struct property
{
explicit constexpr property(auto&&... args) :
_value{ std::forward<decltype(args)>(args)... } {}
property& operator=(const property& other) = default;
T operator()() const
{
return _value;
}
void operator()(auto&& arg)
{
_value = std::forward<decltype(arg)>(arg);
}
operator bool() const noexcept
{
if constexpr (std::is_same_v<T, winrt::hstring>)
{
return !_value.empty();
}
else
{
return _value;
}
}
bool operator==(const property& other) const noexcept
{
return _value == other._value;
}
bool operator!=(const property& other) const noexcept
{
return _value != other._value;
}
bool operator==(const T& other) const noexcept
{
return _value == other;
}
bool operator!=(const T& other) const noexcept
{
return _value != other;
}
private:
T _value;
};
#ifdef WINRT_Windows_Foundation_H
template<typename ArgsT>
struct event
{
event<ArgsT>() = default;
winrt::event_token operator()(const ArgsT& handler) { return _handlers.add(handler); }
void operator()(const winrt::event_token& token) { _handlers.remove(token); }
operator bool() const noexcept { return _handlers; }
template<typename... Arg>
void raise(auto&&... args)
{
_handlers(std::forward<decltype(args)>(args)...);
}
winrt::event<ArgsT> _handlers;
};
template<typename SenderT, typename ArgsT>
struct typed_event
{
typed_event<SenderT, ArgsT>() = default;
winrt::event_token operator()(const winrt::Windows::Foundation::TypedEventHandler<SenderT, ArgsT>& handler) { return _handlers.add(handler); }
void operator()(const winrt::event_token& token) { _handlers.remove(token); }
operator bool() const noexcept { return _handlers; }
template<typename... Arg>
void raise(Arg const&... args)
{
_handlers(std::forward<decltype(args)>(args)...);
}
winrt::event<winrt::Windows::Foundation::TypedEventHandler<SenderT, ArgsT>> _handlers;
};
#endif
#ifdef WINRT_Windows_UI_Xaml_DataH
using property_changed_event = event<winrt::Windows::UI::Xaml::Data::PropertyChangedEventHandler>;
// Making a til::observable_property unfortunately doesn't seem feasible.
// It's gonna just result in more macros, which no one wants.
//
// 1. We don't know who the sender is, or would require `this` to always be
// the first parameter to one of these observable_property's.
//
// 2. We don't know what our own name is. We need to actually raise an event
// with the name of the variable as the parameter. Only way to do that is
// with something like
//
// til::observable<int> Foo(this, L"Foo", 42)
//
// which then kinda implies the creation of:
//
// #define OBSERVABLE(type, name, ...) til::observable_property<type> name{ this, L## #name, this.PropertyChanged, __VA_ARGS__ };
//
// Which is just silly
#endif
}