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:
parent
23d45a7e3a
commit
ae7595b8e1
|
@ -100,6 +100,7 @@ TLDR
|
|||
tokenizes
|
||||
tonos
|
||||
toolset
|
||||
truthiness
|
||||
tshe
|
||||
ubuntu
|
||||
uiatextrange
|
||||
|
|
|
@ -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()) };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -70,6 +70,7 @@ TRACELOGGING_DECLARE_PROVIDER(g_hTerminalControlProvider);
|
|||
|
||||
#include "til.h"
|
||||
#include <til/mutex.h>
|
||||
#include <til/winrt.h>
|
||||
|
||||
#include "ThrottledFunc.h"
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue