From af135759094c6dc9558b926435bb1a9f597bf30f Mon Sep 17 00:00:00 2001
From: Michael Maltsev <4129781+m417z@users.noreply.github.com>
Date: Tue, 19 Dec 2023 21:25:48 +0200
Subject: [PATCH] Windows 11 Start Menu Styler v1.0 (#438)
---
mods/windows-11-start-menu-styler.wh.cpp | 1573 ++++++++++++++++++++++
1 file changed, 1573 insertions(+)
create mode 100644 mods/windows-11-start-menu-styler.wh.cpp
diff --git a/mods/windows-11-start-menu-styler.wh.cpp b/mods/windows-11-start-menu-styler.wh.cpp
new file mode 100644
index 000000000..064524116
--- /dev/null
+++ b/mods/windows-11-start-menu-styler.wh.cpp
@@ -0,0 +1,1573 @@
+// ==WindhawkMod==
+// @id windows-11-start-menu-styler
+// @name Windows 11 Start Menu Styler
+// @description An advanced mod to override style attributes of the start menu control elements
+// @version 1.0
+// @author m417z
+// @github https://github.com/m417z
+// @twitter https://twitter.com/m417z
+// @homepage https://m417z.com/
+// @include StartMenuExperienceHost.exe
+// @architecture x86-64
+// @compilerOptions -lcomctl32 -lole32 -loleaut32 -lruntimeobject -Wl,--export-all-symbols
+// ==/WindhawkMod==
+
+// Source code is published under The GNU General Public License v3.0.
+//
+// For bug reports and feature requests, please open an issue here:
+// https://github.com/ramensoftware/windhawk-mods/issues
+//
+// For pull requests, development takes place here:
+// https://github.com/m417z/my-windhawk-mods
+
+// ==WindhawkModReadme==
+/*
+# Windows 11 Start Menu Styler
+
+An advanced mod to override style attributes of the start menu control elements.
+
+**Note**: This mod requires Windhawk v1.4 or later.
+
+Also check out the **Windows 11 Taskbar Styler** mod.
+
+The settings have two sections: control styles and resource variables. Control
+styles allow to override styles, such as size and color, for the target
+elements. Resource variables allow to override predefined variables. For a more
+detailed explanation and examples, refer to the sections below.
+
+The start menu's XAML resources can help find out which elements and resource
+variables can be customized. To the best of my knowledge, there are no public
+tools that are able to decode the resource files of recent Windows versions, but
+here are XAML resources which were obtained via other means for your
+convenience:
+[StartResources.xbf](https://gist.github.com/m417z/a7e4e2c7b451ee79c62c51ca2dba7349).
+
+The [UWPSpy](https://ramensoftware.com/uwpspy) tool can be used to inspect the
+start menu control elements in real time, and experiment with various styles.
+
+## Control styles
+
+Each entry has a target control and a list of styles.
+
+The target control is written as `Control` or `Control#Name`, i.e. the target
+control tag name, such as `StartMenu:StartInnerFrame` or `Rectangle`, optionally
+followed by `#` and the target control's `x:Name` attribute. The target control
+can also include:
+* Child control index, for example: `Control#Name[2]` will only match the
+ relevant control that's also the second child among all of its parent's child
+ controls.
+* Control properties, for example:
+ `Control#Name[Property1=Value1][Property2=Value2]`.
+* Parent controls, separated by `>`, for example: `ParentControl#ParentName >
+ Control#Name`.
+* Visual state group name, for example: `Control#Name@VisualStateGroupName`. It
+ can be specified for the target control or for a parent control, but can be
+ specified only once per target. The visual state group can be used in styles
+ as specified below.
+
+**Note**: The target is evaluated only once. If, for example, the index or the
+properties of a control change, the target conditions aren't evaluated again.
+
+Each style is written as `Style=Value`, for example: `Height=5`. The `:=` syntax
+can be used to use XAML syntax, for example: `Fill:=`. In addition, a visual state can be specified as following:
+`Style@VisualState=Value`, in which case the style will only apply when the
+visual state group specified in the target matches the specified visual state.
+
+## Resource variables
+
+Some variables, such as size and padding for various controls, are defined as
+resource variables.
+
+## Implementation notes
+
+The VisualTreeWatcher implementation is based on the
+[ExplorerTAP](https://github.com/TranslucentTB/TranslucentTB/tree/develop/ExplorerTAP)
+code from the **TranslucentTB** project.
+*/
+// ==/WindhawkModReadme==
+
+// ==WindhawkModSettings==
+/*
+- controlStyles:
+ - - target: Border#AcrylicBorder
+ $name: Target
+ - styles: [CornerRadius=0]
+ $name: Styles
+ $name: Control styles
+- resourceVariables:
+ - - variableKey: ""
+ $name: Variable key
+ - value: ""
+ $name: Value
+ $name: Resource variables
+*/
+// ==/WindhawkModSettings==
+
+#include
+#include
+
+#include
+
+#undef GetCurrentTime
+
+#include
+
+std::atomic g_targetThreadId = 0;
+
+void ApplyCustomizations(InstanceHandle handle,
+ winrt::Windows::UI::Xaml::FrameworkElement element);
+void CleanupCustomizations(InstanceHandle handle);
+
+HMODULE GetCurrentModuleHandle() {
+ HMODULE module;
+ if (!GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
+ GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
+ L"", &module)) {
+ return nullptr;
+ }
+
+ return module;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// clang-format off
+
+#pragma region winrt_hpp
+
+#include
+#include
+#include
+
+// forward declare namespaces we alias
+namespace winrt {
+ namespace Microsoft::UI::Xaml::Controls {}
+ namespace TranslucentTB::Xaml::Models::Primitives {}
+ namespace Windows {
+ namespace Foundation::Collections {}
+ namespace UI::Xaml {
+ namespace Controls {}
+ namespace Hosting {}
+ }
+ }
+}
+
+// alias some long namespaces for convenience
+// namespace mux = winrt::Microsoft::UI::Xaml;
+// namespace muxc = winrt::Microsoft::UI::Xaml::Controls;
+// namespace txmp = winrt::TranslucentTB::Xaml::Models::Primitives;
+namespace wf = winrt::Windows::Foundation;
+// namespace wfc = wf::Collections;
+namespace wux = winrt::Windows::UI::Xaml;
+// namespace wuxc = wux::Controls;
+namespace wuxh = wux::Hosting;
+
+#pragma endregion // winrt_hpp
+
+#pragma region visualtreewatcher_hpp
+
+#include
+
+class VisualTreeWatcher : public winrt::implements
+{
+public:
+ VisualTreeWatcher(winrt::com_ptr site);
+
+ VisualTreeWatcher(const VisualTreeWatcher&) = delete;
+ VisualTreeWatcher& operator=(const VisualTreeWatcher&) = delete;
+
+ VisualTreeWatcher(VisualTreeWatcher&&) = delete;
+ VisualTreeWatcher& operator=(VisualTreeWatcher&&) = delete;
+
+ ~VisualTreeWatcher();
+
+ void UnadviseVisualTreeChange();
+
+private:
+ HRESULT STDMETHODCALLTYPE OnVisualTreeChange(ParentChildRelation relation, VisualElement element, VisualMutationType mutationType) override;
+ HRESULT STDMETHODCALLTYPE OnElementStateChanged(InstanceHandle element, VisualElementState elementState, LPCWSTR context) noexcept override;
+
+ template
+ T FromHandle(InstanceHandle handle)
+ {
+ wf::IInspectable obj;
+ winrt::check_hresult(m_XamlDiagnostics->GetIInspectableFromHandle(handle, reinterpret_cast<::IInspectable**>(winrt::put_abi(obj))));
+
+ return obj.as();
+ }
+
+ winrt::com_ptr m_XamlDiagnostics = nullptr;
+};
+
+#pragma endregion // visualtreewatcher_hpp
+
+#pragma region visualtreewatcher_cpp
+
+#include
+
+VisualTreeWatcher::VisualTreeWatcher(winrt::com_ptr site) :
+ m_XamlDiagnostics(site.as())
+{
+ Wh_Log(L"Constructing VisualTreeWatcher");
+ winrt::check_hresult(m_XamlDiagnostics.as()->AdviseVisualTreeChange(this));
+}
+
+VisualTreeWatcher::~VisualTreeWatcher()
+{
+ Wh_Log(L"Destructing VisualTreeWatcher");
+}
+
+void VisualTreeWatcher::UnadviseVisualTreeChange()
+{
+ Wh_Log(L"UnadviseVisualTreeChange VisualTreeWatcher");
+ winrt::check_hresult(m_XamlDiagnostics.as()->UnadviseVisualTreeChange(this));
+}
+
+HRESULT VisualTreeWatcher::OnVisualTreeChange(ParentChildRelation, VisualElement element, VisualMutationType mutationType) try
+{
+ if (GetCurrentThreadId() != g_targetThreadId)
+ {
+ return S_OK;
+ }
+
+ Wh_Log(L"========================================");
+
+ switch (mutationType)
+ {
+ case Add:
+ Wh_Log(L"Mutation type: Add");
+ break;
+
+ case Remove:
+ Wh_Log(L"Mutation type: Remove");
+ break;
+
+ default:
+ Wh_Log(L"Mutation type: %d", static_cast(mutationType));
+ break;
+ }
+
+ Wh_Log(L"Element type: %s", element.Type);
+
+ if (mutationType == Add)
+ {
+ const auto inspectable = FromHandle(element.Handle);
+
+ auto frameworkElement = inspectable.try_as();
+ if (!frameworkElement)
+ {
+ const auto desktopXamlSource = FromHandle(element.Handle);
+ frameworkElement = desktopXamlSource.Content().try_as();
+ }
+
+ if (frameworkElement)
+ {
+ Wh_Log(L"FrameworkElement name: %s", frameworkElement.Name().c_str());
+ ApplyCustomizations(element.Handle, frameworkElement);
+ }
+ }
+ else if (mutationType == Remove)
+ {
+ CleanupCustomizations(element.Handle);
+ }
+
+ return S_OK;
+}
+catch (...)
+{
+ HRESULT hr = winrt::to_hresult();
+ Wh_Log(L"Error %08X", hr);
+
+ // Returning an error prevents (some?) further messages, always return
+ // success.
+ // return hr;
+ return S_OK;
+}
+
+HRESULT VisualTreeWatcher::OnElementStateChanged(InstanceHandle, VisualElementState, LPCWSTR) noexcept
+{
+ return S_OK;
+}
+
+#pragma endregion // visualtreewatcher_cpp
+
+#pragma region tap_hpp
+
+#include
+
+// TODO: weak_ref might be better here.
+winrt::com_ptr g_visualTreeWatcher;
+
+// {C85D8CC7-5463-40E8-A432-F5916B6427E5}
+static constexpr CLSID CLSID_WindhawkTAP = { 0xc85d8cc7, 0x5463, 0x40e8, { 0xa4, 0x32, 0xf5, 0x91, 0x6b, 0x64, 0x27, 0xe5 } };
+
+class WindhawkTAP : public winrt::implements
+{
+public:
+ HRESULT STDMETHODCALLTYPE SetSite(IUnknown *pUnkSite) override;
+ HRESULT STDMETHODCALLTYPE GetSite(REFIID riid, void **ppvSite) noexcept override;
+
+private:
+ winrt::com_ptr site;
+};
+
+#pragma endregion // tap_hpp
+
+#pragma region tap_cpp
+
+HRESULT WindhawkTAP::SetSite(IUnknown *pUnkSite) try
+{
+ // Only ever 1 VTW at once.
+ if (g_visualTreeWatcher)
+ {
+ g_visualTreeWatcher->UnadviseVisualTreeChange();
+ g_visualTreeWatcher = nullptr;
+ }
+
+ site.copy_from(pUnkSite);
+
+ if (site)
+ {
+ // Decrease refcount increased by InitializeXamlDiagnosticsEx.
+ FreeLibrary(GetCurrentModuleHandle());
+
+ g_visualTreeWatcher = winrt::make_self(site);
+ }
+
+ return S_OK;
+}
+catch (...)
+{
+ return winrt::to_hresult();
+}
+
+HRESULT WindhawkTAP::GetSite(REFIID riid, void **ppvSite) noexcept
+{
+ return site.as(riid, ppvSite);
+}
+
+#pragma endregion // tap_cpp
+
+#pragma region simplefactory_hpp
+
+#include
+
+template
+struct SimpleFactory : winrt::implements, IClassFactory, winrt::non_agile>
+{
+ HRESULT STDMETHODCALLTYPE CreateInstance(IUnknown* pUnkOuter, REFIID riid, void** ppvObject) override try
+ {
+ if (!pUnkOuter)
+ {
+ *ppvObject = nullptr;
+ return winrt::make().as(riid, ppvObject);
+ }
+ else
+ {
+ return CLASS_E_NOAGGREGATION;
+ }
+ }
+ catch (...)
+ {
+ return winrt::to_hresult();
+ }
+
+ HRESULT STDMETHODCALLTYPE LockServer(BOOL) noexcept override
+ {
+ return S_OK;
+ }
+};
+
+#pragma endregion // simplefactory_hpp
+
+#pragma region module_cpp
+
+#include
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdll-attribute-on-redeclaration"
+
+__declspec(dllexport)
+_Use_decl_annotations_ STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv) try
+{
+ if (rclsid == CLSID_WindhawkTAP)
+ {
+ *ppv = nullptr;
+ return winrt::make>().as(riid, ppv);
+ }
+ else
+ {
+ return CLASS_E_CLASSNOTAVAILABLE;
+ }
+}
+catch (...)
+{
+ return winrt::to_hresult();
+}
+
+__declspec(dllexport)
+_Use_decl_annotations_ STDAPI DllCanUnloadNow(void)
+{
+ if (winrt::get_module_lock())
+ {
+ return S_FALSE;
+ }
+ else
+ {
+ return S_OK;
+ }
+}
+
+#pragma clang diagnostic pop
+
+#pragma endregion // module_cpp
+
+#pragma region api_cpp
+
+using PFN_INITIALIZE_XAML_DIAGNOSTICS_EX = decltype(&InitializeXamlDiagnosticsEx);
+
+HRESULT InjectWindhawkTAP() noexcept
+{
+ HMODULE module = GetCurrentModuleHandle();
+ if (!module)
+ {
+ return HRESULT_FROM_WIN32(GetLastError());
+ }
+
+ WCHAR location[MAX_PATH];
+ switch (GetModuleFileName(module, location, ARRAYSIZE(location)))
+ {
+ case 0:
+ case ARRAYSIZE(location):
+ return HRESULT_FROM_WIN32(GetLastError());
+ }
+
+ const HMODULE wux(LoadLibraryEx(L"Windows.UI.Xaml.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32));
+ if (!wux) [[unlikely]]
+ {
+ return HRESULT_FROM_WIN32(GetLastError());
+ }
+
+ const auto ixde = reinterpret_cast(GetProcAddress(wux, "InitializeXamlDiagnosticsEx"));
+ if (!ixde) [[unlikely]]
+ {
+ return HRESULT_FROM_WIN32(GetLastError());
+ }
+
+ const HRESULT hr2 = ixde(L"VisualDiagConnection1", GetCurrentProcessId(), nullptr, location, CLSID_WindhawkTAP, nullptr);
+ if (FAILED(hr2)) [[unlikely]]
+ {
+ return hr2;
+ }
+
+ return S_OK;
+}
+
+#pragma endregion // api_cpp
+
+// clang-format on
+////////////////////////////////////////////////////////////////////////////////
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+using namespace winrt::Windows::UI::Xaml;
+
+// https://stackoverflow.com/a/51274008
+template
+struct deleter_from_fn {
+ template
+ constexpr void operator()(T* arg) const {
+ fn(arg);
+ }
+};
+using string_setting_unique_ptr =
+ std::unique_ptr>;
+
+using PropertyKeyValue =
+ std::pair;
+
+struct ElementMatcher {
+ std::wstring type;
+ std::wstring name;
+ std::wstring visualStateGroup;
+ int oneBasedIndex = 0;
+ std::vector> propertyValuesStr;
+ std::vector propertyValues;
+};
+
+struct StyleRule {
+ std::wstring name;
+ std::wstring visualState;
+ std::wstring value;
+ bool isXamlValue = false;
+};
+
+// Property -> visual state -> value.
+using PropertyOverrides = std::unordered_map<
+ DependencyProperty,
+ std::unordered_map>;
+
+struct ElementCustomizationRules {
+ ElementMatcher elementMatcher;
+ std::vector parentElementMatchers;
+ PropertyOverrides propertyOverrides;
+};
+
+bool g_elementsCustomizationRulesLoaded;
+std::vector g_elementsCustomizationRules;
+
+struct ElementPropertyCustomizationState {
+ std::optional originalValue;
+ std::optional customValue;
+ int64_t propertyChangedToken = 0;
+};
+
+struct ElementCustomizationState {
+ winrt::weak_ref element;
+ std::unordered_map
+ propertyCustomizationStates;
+ winrt::weak_ref visualStateGroup;
+ winrt::event_token visualStateGroupCurrentStateChangedToken;
+};
+
+std::unordered_map
+ g_elementsCustomizationState;
+
+bool g_elementPropertyModifying;
+
+// https://stackoverflow.com/a/12835139
+VisualStateGroup GetVisualStateGroup(FrameworkElement element,
+ std::wstring_view stateGroupName) {
+ auto list = VisualStateManager::GetVisualStateGroups(element);
+
+ for (const auto& v : list) {
+ if (v.Name() == stateGroupName) {
+ return v;
+ }
+ }
+
+ return nullptr;
+}
+
+bool TestElementMatcher(FrameworkElement element,
+ const ElementMatcher& matcher,
+ VisualStateGroup* visualStateGroup) {
+ if (!matcher.type.empty() &&
+ matcher.type != winrt::get_class_name(element)) {
+ return false;
+ }
+
+ if (!matcher.name.empty() && matcher.name != element.Name()) {
+ return false;
+ }
+
+ if (matcher.oneBasedIndex) {
+ auto parent = Media::VisualTreeHelper::GetParent(element);
+ if (!parent) {
+ return false;
+ }
+
+ int index = matcher.oneBasedIndex - 1;
+ if (index < 0 ||
+ index >= Media::VisualTreeHelper::GetChildrenCount(parent) ||
+ Media::VisualTreeHelper::GetChild(parent, index) != element) {
+ return false;
+ }
+ }
+
+ auto elementDo = element.as();
+
+ for (const auto& propertyValue : matcher.propertyValues) {
+ const auto value = elementDo.ReadLocalValue(propertyValue.first);
+ const auto className = winrt::get_class_name(value);
+ const auto expectedClassName =
+ winrt::get_class_name(propertyValue.second);
+ if (className != expectedClassName) {
+ Wh_Log(L"Different property class: %s vs. %s", className.c_str(),
+ expectedClassName.c_str());
+ return false;
+ }
+
+ if (className == L"Windows.Foundation.IReference`1") {
+ if (winrt::unbox_value(propertyValue.second) ==
+ winrt::unbox_value(value)) {
+ continue;
+ }
+
+ return false;
+ }
+
+ if (className == L"Windows.Foundation.IReference`1") {
+ if (winrt::unbox_value(propertyValue.second) ==
+ winrt::unbox_value(value)) {
+ continue;
+ }
+
+ return false;
+ }
+
+ if (className == L"Windows.Foundation.IReference`1") {
+ if (winrt::unbox_value(propertyValue.second) ==
+ winrt::unbox_value(value)) {
+ continue;
+ }
+
+ return false;
+ }
+
+ Wh_Log(L"Unsupported property class: %s", className.c_str());
+ return false;
+ }
+
+ if (!matcher.visualStateGroup.empty() && visualStateGroup) {
+ *visualStateGroup =
+ GetVisualStateGroup(element, matcher.visualStateGroup);
+ }
+
+ return true;
+}
+
+const ElementCustomizationRules* FindElementCustomizationRules(
+ FrameworkElement element,
+ VisualStateGroup* visualStateGroup) {
+ for (const auto& override : g_elementsCustomizationRules) {
+ if (!TestElementMatcher(element, override.elementMatcher,
+ visualStateGroup)) {
+ continue;
+ }
+
+ auto parentElementIter = element;
+ bool parentElementMatchFailed = false;
+
+ for (const auto& matcher : override.parentElementMatchers) {
+ // Using parentElementIter.Parent() was sometimes returning null.
+ parentElementIter =
+ Media::VisualTreeHelper::GetParent(parentElementIter)
+ .try_as();
+ if (!parentElementIter) {
+ parentElementMatchFailed = true;
+ break;
+ }
+
+ if (!TestElementMatcher(parentElementIter, matcher,
+ visualStateGroup)) {
+ parentElementMatchFailed = true;
+ break;
+ }
+ }
+
+ if (!parentElementMatchFailed) {
+ return &override;
+ }
+ }
+
+ return nullptr;
+}
+
+void ProcessAllStylesFromSettings();
+void ProcessResourceVariablesFromSettings();
+
+void ApplyCustomizations(InstanceHandle handle, FrameworkElement element) {
+ if (!g_elementsCustomizationRulesLoaded) {
+ ProcessAllStylesFromSettings();
+ ProcessResourceVariablesFromSettings();
+ g_elementsCustomizationRulesLoaded = true;
+ }
+
+ VisualStateGroup visualStateGroup;
+ auto rules = FindElementCustomizationRules(element, &visualStateGroup);
+ if (!rules) {
+ return;
+ }
+
+ Wh_Log(L"Applying styles");
+
+ auto& elementCustomizationState = g_elementsCustomizationState[handle];
+
+ {
+ auto oldElement = elementCustomizationState.element.get();
+ if (oldElement) {
+ auto oldElementDo = oldElement.as();
+ for (const auto& [property, state] :
+ elementCustomizationState.propertyCustomizationStates) {
+ oldElementDo.UnregisterPropertyChangedCallback(
+ property, state.propertyChangedToken);
+
+ if (state.originalValue) {
+ oldElement.SetValue(property, *state.originalValue);
+ }
+ }
+ }
+
+ auto oldVisualStateGroup =
+ elementCustomizationState.visualStateGroup.get();
+ if (oldVisualStateGroup) {
+ oldVisualStateGroup.CurrentStateChanged(
+ elementCustomizationState
+ .visualStateGroupCurrentStateChangedToken);
+ }
+ }
+
+ elementCustomizationState = {
+ .element = element,
+ };
+
+ auto elementDo = element.as();
+
+ VisualState currentVisualState(
+ visualStateGroup ? visualStateGroup.CurrentState() : nullptr);
+
+ std::wstring currentVisualStateName(
+ currentVisualState ? currentVisualState.Name() : L"");
+
+ for (auto& [property, valuesPerVisualState] : rules->propertyOverrides) {
+ const auto [propertyCustomizationStatesIt, inserted] =
+ elementCustomizationState.propertyCustomizationStates.insert(
+ {property, {}});
+ if (!inserted) {
+ continue;
+ }
+
+ auto& propertyCustomizationState =
+ propertyCustomizationStatesIt->second;
+
+ auto it = valuesPerVisualState.find(currentVisualStateName);
+ if (it == valuesPerVisualState.end() &&
+ !currentVisualStateName.empty()) {
+ it = valuesPerVisualState.find(L"");
+ }
+
+ if (it != valuesPerVisualState.end()) {
+ propertyCustomizationState.originalValue =
+ element.ReadLocalValue(property);
+ propertyCustomizationState.customValue = it->second;
+ element.SetValue(property, it->second);
+ }
+
+ propertyCustomizationState.propertyChangedToken =
+ elementDo.RegisterPropertyChangedCallback(
+ property,
+ [&propertyCustomizationState](DependencyObject sender,
+ DependencyProperty property) {
+ if (g_elementPropertyModifying) {
+ return;
+ }
+
+ auto element = sender.try_as();
+ if (!element) {
+ return;
+ }
+
+ if (!propertyCustomizationState.customValue) {
+ return;
+ }
+
+ Wh_Log(L"Re-applying style for %s",
+ winrt::get_class_name(element).c_str());
+
+ auto localValue = element.ReadLocalValue(property);
+
+ if (*propertyCustomizationState.customValue != localValue) {
+ propertyCustomizationState.originalValue = localValue;
+ }
+
+ g_elementPropertyModifying = true;
+ element.SetValue(property,
+ *propertyCustomizationState.customValue);
+ g_elementPropertyModifying = false;
+ });
+ }
+
+ if (visualStateGroup) {
+ elementCustomizationState.visualStateGroup = visualStateGroup;
+
+ elementCustomizationState.visualStateGroupCurrentStateChangedToken =
+ visualStateGroup.CurrentStateChanged(
+ [rules, &elementCustomizationState](
+ winrt::Windows::Foundation::IInspectable const& sender,
+ VisualStateChangedEventArgs const& e) {
+ auto element = elementCustomizationState.element.get();
+
+ Wh_Log(L"Re-applying all styles for %s",
+ winrt::get_class_name(element).c_str());
+
+ g_elementPropertyModifying = true;
+
+ auto& propertyCustomizationStates =
+ elementCustomizationState.propertyCustomizationStates;
+
+ for (auto& [property, valuesPerVisualState] :
+ rules->propertyOverrides) {
+ auto& propertyCustomizationState =
+ propertyCustomizationStates.at(property);
+
+ auto newState = e.NewState();
+ auto newStateName =
+ std::wstring{newState ? newState.Name() : L""};
+ auto it = valuesPerVisualState.find(newStateName);
+ if (it == valuesPerVisualState.end()) {
+ it = valuesPerVisualState.find(L"");
+ if (it != valuesPerVisualState.end()) {
+ auto oldState = e.OldState();
+ auto oldStateName = std::wstring{
+ oldState ? oldState.Name() : L""};
+ if (!valuesPerVisualState.contains(
+ oldStateName)) {
+ continue;
+ }
+ }
+ }
+
+ if (it != valuesPerVisualState.end()) {
+ if (!propertyCustomizationState.originalValue) {
+ propertyCustomizationState.originalValue =
+ element.ReadLocalValue(property);
+ }
+
+ propertyCustomizationState.customValue = it->second;
+ element.SetValue(property, it->second);
+ } else {
+ if (propertyCustomizationState.originalValue) {
+ if (*propertyCustomizationState.originalValue ==
+ DependencyProperty::UnsetValue()) {
+ element.ClearValue(property);
+ } else {
+ element.SetValue(property,
+ *propertyCustomizationState
+ .originalValue);
+ }
+
+ propertyCustomizationState.originalValue
+ .reset();
+ }
+
+ propertyCustomizationState.customValue.reset();
+ }
+ }
+
+ g_elementPropertyModifying = false;
+ });
+ }
+}
+
+void CleanupCustomizations(InstanceHandle handle) {
+ auto it = g_elementsCustomizationState.find(handle);
+ if (it == g_elementsCustomizationState.end()) {
+ return;
+ }
+
+ auto& [k, v] = *it;
+
+ auto oldElement = v.element.get();
+ if (oldElement) {
+ auto oldElementDo = oldElement.as();
+ for (const auto& [property, state] : v.propertyCustomizationStates) {
+ oldElementDo.UnregisterPropertyChangedCallback(
+ property, state.propertyChangedToken);
+ }
+ }
+
+ auto oldVisualStateGroup = v.visualStateGroup.get();
+ if (oldVisualStateGroup) {
+ oldVisualStateGroup.CurrentStateChanged(
+ v.visualStateGroupCurrentStateChangedToken);
+ }
+
+ g_elementsCustomizationState.erase(it);
+}
+
+// https://stackoverflow.com/a/5665377
+std::wstring EscapeXmlAttribute(std::wstring_view data) {
+ std::wstring buffer;
+ buffer.reserve(data.size());
+ for (size_t pos = 0; pos != data.size(); ++pos) {
+ switch (data[pos]) {
+ case '&':
+ buffer.append(L"&");
+ break;
+ case '\"':
+ buffer.append(L""");
+ break;
+ // case '\'':
+ // buffer.append(L"'");
+ // break;
+ case '<':
+ buffer.append(L"<");
+ break;
+ case '>':
+ buffer.append(L">");
+ break;
+ default:
+ buffer.append(&data[pos], 1);
+ break;
+ }
+ }
+
+ return buffer;
+}
+
+// https://stackoverflow.com/a/54364173
+std::wstring_view TrimStringView(std::wstring_view s) {
+ s.remove_prefix(std::min(s.find_first_not_of(L" \t\r\v\n"), s.size()));
+ s.remove_suffix(
+ std::min(s.size() - s.find_last_not_of(L" \t\r\v\n") - 1, s.size()));
+ return s;
+}
+
+// https://stackoverflow.com/a/46931770
+std::vector SplitStringView(std::wstring_view s,
+ std::wstring_view delimiter) {
+ size_t pos_start = 0, pos_end, delim_len = delimiter.length();
+ std::wstring_view token;
+ std::vector res;
+
+ while ((pos_end = s.find(delimiter, pos_start)) !=
+ std::wstring_view::npos) {
+ token = s.substr(pos_start, pos_end - pos_start);
+ pos_start = pos_end + delim_len;
+ res.push_back(token);
+ }
+
+ res.push_back(s.substr(pos_start));
+ return res;
+}
+
+std::wstring AdjustTypeName(std::wstring_view type) {
+ static const std::vector>
+ adjustments = {
+ {L"StartMenu.", L"StartMenu:"},
+ {L"Microsoft.UI.Xaml.Control.", L"muxc:"},
+ };
+
+ for (const auto& adjustment : adjustments) {
+ if (type.starts_with(adjustment.first)) {
+ auto result = std::wstring{adjustment.second};
+ result += type.substr(adjustment.first.size());
+ return result;
+ }
+ }
+
+ return std::wstring{type};
+}
+
+void ResolveTypeAndStyles(ElementMatcher* elementMatcher,
+ std::vector styleRules = {},
+ PropertyOverrides* propertyOverrides = nullptr) {
+ std::wstring xaml =
+ LR"(
+
+)";
+
+ Wh_Log(L"======================================== XAML:");
+ std::wstringstream ss(xaml);
+ std::wstring line;
+ while (std::getline(ss, line, L'\n')) {
+ Wh_Log(L"%s", line.c_str());
+ }
+ Wh_Log(L"========================================");
+
+ auto resourceDictionary =
+ Markup::XamlReader::Load(xaml).as();
+
+ auto [styleKey, styleInspectable] = resourceDictionary.First().Current();
+ auto style = styleInspectable.as