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"( + type)); + xaml += L"\">\n"; + + for (const auto& [property, value] : elementMatcher->propertyValuesStr) { + xaml += L" \n"; + } + + for (const auto& rule : styleRules) { + xaml += L" \n"; + } else { + xaml += + L">\n" + L" \n"; + xaml += rule.value; + xaml += + L"\n" + L" \n" + L" \n"; + } + } + + 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