From d201ae4335484a158cf54decf60c30f417d7c990 Mon Sep 17 00:00:00 2001 From: Seraphima Zykova Date: Fri, 1 Jul 2022 17:56:45 +0200 Subject: [PATCH] [AlwaysOnTop] Round corners on Windows 11 (#19109) * check window corners * draw rounded rectangle * draw rounded corners * switch between rounded and not rounded rects * added enabled corners setting * update corner --- .../AlwaysOnTop/AlwaysOnTop.vcxproj | 2 + .../AlwaysOnTop/AlwaysOnTop.vcxproj.filters | 6 +++ .../alwaysontop/AlwaysOnTop/FrameDrawer.cpp | 51 ++++++++++++++----- .../alwaysontop/AlwaysOnTop/FrameDrawer.h | 8 +-- .../alwaysontop/AlwaysOnTop/Settings.cpp | 11 ++++ .../alwaysontop/AlwaysOnTop/Settings.h | 1 + .../AlwaysOnTop/SettingsConstants.h | 3 +- .../alwaysontop/AlwaysOnTop/WindowBorder.cpp | 17 ++++++- .../AlwaysOnTop/WindowCornersUtil.cpp | 39 ++++++++++++++ .../AlwaysOnTop/WindowCornersUtil.h | 4 ++ .../AlwaysOnTopProperties.cs | 13 +++-- .../ViewModels/AlwaysOnTopViewModel.cs | 34 ++++++++++++- .../Settings.UI/Strings/en-us/Resources.resw | 7 ++- .../Settings.UI/Views/AlwaysOnTopPage.xaml | 7 +++ 14 files changed, 175 insertions(+), 28 deletions(-) create mode 100644 src/modules/alwaysontop/AlwaysOnTop/WindowCornersUtil.cpp create mode 100644 src/modules/alwaysontop/AlwaysOnTop/WindowCornersUtil.h diff --git a/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.vcxproj b/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.vcxproj index f06126b3fb..b7091defa9 100644 --- a/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.vcxproj +++ b/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.vcxproj @@ -136,6 +136,7 @@ + @@ -150,6 +151,7 @@ + diff --git a/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.vcxproj.filters b/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.vcxproj.filters index 8eeaaded4f..d0ec584675 100644 --- a/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.vcxproj.filters +++ b/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.vcxproj.filters @@ -42,6 +42,9 @@ Source Files + + Source Files + @@ -83,5 +86,8 @@ Header Files + + Header Files + \ No newline at end of file diff --git a/src/modules/alwaysontop/AlwaysOnTop/FrameDrawer.cpp b/src/modules/alwaysontop/AlwaysOnTop/FrameDrawer.cpp index b22e8644f1..ab0833ca86 100644 --- a/src/modules/alwaysontop/AlwaysOnTop/FrameDrawer.cpp +++ b/src/modules/alwaysontop/AlwaysOnTop/FrameDrawer.cpp @@ -86,17 +86,26 @@ void FrameDrawer::Show() Render(); } -void FrameDrawer::SetBorderRect(RECT windowRect, COLORREF color, int thickness) +void FrameDrawer::SetBorderRect(RECT windowRect, COLORREF color, int thickness, int radius) { - const auto newSceneRect = DrawableRect{ - .rect = ConvertRect(windowRect), + auto newSceneRect = DrawableRect{ .borderColor = ConvertColor(color), - .thickness = thickness + .thickness = thickness, }; + if (radius != 0) + { + newSceneRect.roundedRect = ConvertRect(windowRect, thickness, radius); + } + else + { + newSceneRect.rect = ConvertRect(windowRect, thickness); + } + const bool colorUpdated = std::memcmp(&m_sceneRect.borderColor, &newSceneRect.borderColor, sizeof(newSceneRect.borderColor)); const bool thicknessUpdated = m_sceneRect.thickness != newSceneRect.thickness; - const bool needsRedraw = colorUpdated || thicknessUpdated; + const bool cornersUpdated = m_sceneRect.rect.has_value() != newSceneRect.rect.has_value() || m_sceneRect.roundedRect.has_value() != newSceneRect.roundedRect.has_value(); + const bool needsRedraw = colorUpdated || thicknessUpdated || cornersUpdated; RECT clientRect; if (!SUCCEEDED(DwmGetWindowAttribute(m_window, DWMWA_EXTENDED_FRAME_BOUNDS, &clientRect, sizeof(clientRect)))) @@ -104,7 +113,7 @@ void FrameDrawer::SetBorderRect(RECT windowRect, COLORREF color, int thickness) return; } - m_sceneRect = newSceneRect; + m_sceneRect = std::move(newSceneRect); const auto renderTargetSize = D2D1::SizeU(clientRect.right - clientRect.left, clientRect.bottom - clientRect.top); @@ -170,24 +179,38 @@ D2D1_COLOR_F FrameDrawer::ConvertColor(COLORREF color) 1.f); } -D2D1_RECT_F FrameDrawer::ConvertRect(RECT rect) +D2D1_ROUNDED_RECT FrameDrawer::ConvertRect(RECT rect, int thickness, int radius) { - return D2D1::RectF((float)rect.left, (float)rect.top, (float)rect.right, (float)rect.bottom); + auto d2d1Rect = D2D1::RectF((float)rect.left + thickness, (float)rect.top + thickness, (float)rect.right - thickness, (float)rect.bottom - thickness); + return D2D1::RoundedRect(d2d1Rect, (float)radius, (float)radius); +} + +D2D1_RECT_F FrameDrawer::ConvertRect(RECT rect, int thickness) +{ + return D2D1::RectF((float)rect.left + thickness, (float)rect.top + thickness, (float)rect.right - thickness, (float)rect.bottom - thickness); } void FrameDrawer::Render() { - if (!m_renderTarget) + if (!m_renderTarget || !m_borderBrush) + { return; + } + m_renderTarget->BeginDraw(); m_renderTarget->Clear(D2D1::ColorF(0.f, 0.f, 0.f, 0.f)); - if (m_borderBrush) - { - // The border stroke is centered on the line. - m_renderTarget->DrawRectangle(m_sceneRect.rect, m_borderBrush.get(), static_cast(m_sceneRect.thickness * 2)); - } + // The border stroke is centered on the line. + if (m_sceneRect.roundedRect) + { + m_renderTarget->DrawRoundedRectangle(m_sceneRect.roundedRect.value(), m_borderBrush.get(), static_cast(m_sceneRect.thickness * 2)); + } + else if (m_sceneRect.rect) + { + m_renderTarget->DrawRectangle(m_sceneRect.rect.value(), m_borderBrush.get(), static_cast(m_sceneRect.thickness * 2)); + } + m_renderTarget->EndDraw(); } \ No newline at end of file diff --git a/src/modules/alwaysontop/AlwaysOnTop/FrameDrawer.h b/src/modules/alwaysontop/AlwaysOnTop/FrameDrawer.h index 19f44487e6..d3fb27d411 100644 --- a/src/modules/alwaysontop/AlwaysOnTop/FrameDrawer.h +++ b/src/modules/alwaysontop/AlwaysOnTop/FrameDrawer.h @@ -18,14 +18,15 @@ public: void Show(); void Hide(); - void SetBorderRect(RECT windowRect, COLORREF color, int thickness); + void SetBorderRect(RECT windowRect, COLORREF color, int thickness, int radius); private: bool CreateRenderTargets(const RECT& clientRect); struct DrawableRect { - D2D1_RECT_F rect; + std::optional rect; + std::optional roundedRect; D2D1_COLOR_F borderColor; int thickness; }; @@ -33,7 +34,8 @@ private: static ID2D1Factory* GetD2DFactory(); static IDWriteFactory* GetWriteFactory(); static D2D1_COLOR_F ConvertColor(COLORREF color); - static D2D1_RECT_F ConvertRect(RECT rect); + static D2D1_ROUNDED_RECT ConvertRect(RECT rect, int thickness, int radius); + static D2D1_RECT_F ConvertRect(RECT rect, int thickness); void Render(); HWND m_window = nullptr; diff --git a/src/modules/alwaysontop/AlwaysOnTop/Settings.cpp b/src/modules/alwaysontop/AlwaysOnTop/Settings.cpp index 8122f0cf86..f91ce253a8 100644 --- a/src/modules/alwaysontop/AlwaysOnTop/Settings.cpp +++ b/src/modules/alwaysontop/AlwaysOnTop/Settings.cpp @@ -20,6 +20,7 @@ namespace NonLocalizable const static wchar_t* BlockInGameModeID = L"do-not-activate-on-game-mode"; const static wchar_t* ExcludedAppsID = L"excluded-apps"; const static wchar_t* FrameAccentColor = L"frame-accent-color"; + const static wchar_t* RoundCornersEnabledID = L"round-corners-enabled"; } // TODO: move to common utils @@ -153,6 +154,16 @@ void AlwaysOnTopSettings::LoadSettings() } } + if (const auto jsonVal = values.get_bool_value(NonLocalizable::RoundCornersEnabledID)) + { + auto val = *jsonVal; + if (m_settings.roundCornersEnabled != val) + { + m_settings.roundCornersEnabled = val; + NotifyObservers(SettingId::RoundCornersEnabled); + } + } + if (auto jsonVal = values.get_string_value(NonLocalizable::ExcludedAppsID)) { std::wstring apps = std::move(*jsonVal); diff --git a/src/modules/alwaysontop/AlwaysOnTop/Settings.h b/src/modules/alwaysontop/AlwaysOnTop/Settings.h index 95a9192126..7adf839423 100644 --- a/src/modules/alwaysontop/AlwaysOnTop/Settings.h +++ b/src/modules/alwaysontop/AlwaysOnTop/Settings.h @@ -17,6 +17,7 @@ struct Settings PowerToysSettings::HotkeyObject hotkey = PowerToysSettings::HotkeyObject::from_settings(true, true, false, false, 84); // win + ctrl + T bool enableFrame = true; bool enableSound = true; + bool roundCornersEnabled = true; bool blockInGameMode = true; bool frameAccentColor = true; int frameThickness = 15; diff --git a/src/modules/alwaysontop/AlwaysOnTop/SettingsConstants.h b/src/modules/alwaysontop/AlwaysOnTop/SettingsConstants.h index 87bd5d6e91..f6e9cc6cb1 100644 --- a/src/modules/alwaysontop/AlwaysOnTop/SettingsConstants.h +++ b/src/modules/alwaysontop/AlwaysOnTop/SettingsConstants.h @@ -9,5 +9,6 @@ enum class SettingId FrameColor, BlockInGameMode, ExcludeApps, - FrameAccentColor + FrameAccentColor, + RoundCornersEnabled }; \ No newline at end of file diff --git a/src/modules/alwaysontop/AlwaysOnTop/WindowBorder.cpp b/src/modules/alwaysontop/AlwaysOnTop/WindowBorder.cpp index 02001bd0af..f4232b1c49 100644 --- a/src/modules/alwaysontop/AlwaysOnTop/WindowBorder.cpp +++ b/src/modules/alwaysontop/AlwaysOnTop/WindowBorder.cpp @@ -6,6 +6,7 @@ #include #include +#include // Non-Localizable strings namespace NonLocalizable @@ -31,7 +32,7 @@ std::optional GetFrameRect(HWND window) } WindowBorder::WindowBorder(HWND window) : - SettingsObserver({ SettingId::FrameColor, SettingId::FrameThickness, SettingId::FrameAccentColor }), + SettingsObserver({ SettingId::FrameColor, SettingId::FrameThickness, SettingId::FrameAccentColor, SettingId::RoundCornersEnabled }), m_window(nullptr), m_trackingWindow(window), m_frameDrawer(nullptr) @@ -187,7 +188,13 @@ void WindowBorder::UpdateBorderProperties() const color = AlwaysOnTopSettings::settings().frameColor; } - m_frameDrawer->SetBorderRect(frameRect, color, AlwaysOnTopSettings::settings().frameThickness); + int cornerRadius = 0; + if (AlwaysOnTopSettings::settings().roundCornersEnabled) + { + cornerRadius = WindowBordersUtils::AreCornersRounded(m_trackingWindow) ? 8 : 0; + } + + m_frameDrawer->SetBorderRect(frameRect, color, AlwaysOnTopSettings::settings().frameThickness, cornerRadius); } LRESULT WindowBorder::WndProc(UINT message, WPARAM wparam, LPARAM lparam) noexcept @@ -263,6 +270,12 @@ void WindowBorder::SettingsUpdate(SettingId id) UpdateBorderProperties(); } break; + + case SettingId::RoundCornersEnabled: + { + UpdateBorderProperties(); + } + break; default: break; } diff --git a/src/modules/alwaysontop/AlwaysOnTop/WindowCornersUtil.cpp b/src/modules/alwaysontop/AlwaysOnTop/WindowCornersUtil.cpp new file mode 100644 index 0000000000..9a594d211f --- /dev/null +++ b/src/modules/alwaysontop/AlwaysOnTop/WindowCornersUtil.cpp @@ -0,0 +1,39 @@ +#include "pch.h" +#include "WindowCornersUtil.h" + +#include + +#include + +// Placeholder enums since dwmapi.h doesn't have these until SDK 22000. +// TODO: Remove once SDK targets 22000 or above. +enum DWMWINDOWATTRIBUTE_CUSTOM +{ + DWMWA_WINDOW_CORNER_PREFERENCE = 33 +}; + +enum DWM_WINDOW_CORNER_PREFERENCE +{ + DWMWCP_DEFAULT = 0, + DWMWCP_DONOTROUND = 1, + DWMWCP_ROUND = 2, + DWMWCP_ROUNDSMALL = 3 +}; + +bool WindowBordersUtils::AreCornersRounded(HWND window) noexcept +{ + int cornerPreference = DWMWCP_DEFAULT; + auto res = DwmGetWindowAttribute(window, DWMWA_WINDOW_CORNER_PREFERENCE, &cornerPreference, sizeof(cornerPreference)); + if (res != S_OK) + { + // no need to spam with error log if arg is invalid (on Windows 10) + if (res != E_INVALIDARG) + { + Logger::error(L"Get corner preference error: {}", get_last_error_or_default(res)); + } + + return false; + } + + return cornerPreference != DWM_WINDOW_CORNER_PREFERENCE::DWMWCP_DONOTROUND; +} diff --git a/src/modules/alwaysontop/AlwaysOnTop/WindowCornersUtil.h b/src/modules/alwaysontop/AlwaysOnTop/WindowCornersUtil.h new file mode 100644 index 0000000000..db48178532 --- /dev/null +++ b/src/modules/alwaysontop/AlwaysOnTop/WindowCornersUtil.h @@ -0,0 +1,4 @@ +namespace WindowBordersUtils +{ + bool AreCornersRounded(HWND window) noexcept; +} \ No newline at end of file diff --git a/src/settings-ui/Settings.UI.Library/AlwaysOnTopProperties.cs b/src/settings-ui/Settings.UI.Library/AlwaysOnTopProperties.cs index a4cf4c0b95..d92d1f0c67 100644 --- a/src/settings-ui/Settings.UI.Library/AlwaysOnTopProperties.cs +++ b/src/settings-ui/Settings.UI.Library/AlwaysOnTopProperties.cs @@ -14,9 +14,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library public const bool DefaultFrameEnabled = true; public const int DefaultFrameThickness = 15; public const string DefaultFrameColor = "#0099cc"; + public const bool DefaultFrameAccentColor = true; public const bool DefaultSoundEnabled = true; public const bool DefaultDoNotActivateOnGameMode = true; - public const bool DefaultFrameAccentColor = true; + public const bool DefaultRoundCornersEnabled = true; public AlwaysOnTopProperties() { @@ -24,10 +25,11 @@ namespace Microsoft.PowerToys.Settings.UI.Library FrameEnabled = new BoolProperty(DefaultFrameEnabled); FrameThickness = new IntProperty(DefaultFrameThickness); FrameColor = new StringProperty(DefaultFrameColor); + FrameAccentColor = new BoolProperty(DefaultFrameAccentColor); SoundEnabled = new BoolProperty(DefaultSoundEnabled); DoNotActivateOnGameMode = new BoolProperty(DefaultDoNotActivateOnGameMode); + RoundCornersEnabled = new BoolProperty(DefaultRoundCornersEnabled); ExcludedApps = new StringProperty(); - FrameAccentColor = new BoolProperty(DefaultFrameAccentColor); } [JsonPropertyName("hotkey")] @@ -42,6 +44,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonPropertyName("frame-color")] public StringProperty FrameColor { get; set; } + [JsonPropertyName("frame-accent-color")] + public BoolProperty FrameAccentColor { get; set; } + [JsonPropertyName("sound-enabled")] public BoolProperty SoundEnabled { get; set; } @@ -51,8 +56,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonPropertyName("excluded-apps")] public StringProperty ExcludedApps { get; set; } - [JsonPropertyName("frame-accent-color")] - public BoolProperty FrameAccentColor { get; set; } + [JsonPropertyName("round-corners-enabled")] + public BoolProperty RoundCornersEnabled { get; set; } public string ToJsonString() { diff --git a/src/settings-ui/Settings.UI.Library/ViewModels/AlwaysOnTopViewModel.cs b/src/settings-ui/Settings.UI.Library/ViewModels/AlwaysOnTopViewModel.cs index 4fac7c85c9..9cdd21092a 100644 --- a/src/settings-ui/Settings.UI.Library/ViewModels/AlwaysOnTopViewModel.cs +++ b/src/settings-ui/Settings.UI.Library/ViewModels/AlwaysOnTopViewModel.cs @@ -8,6 +8,7 @@ using System.Runtime.CompilerServices; using System.Text.Json; using Microsoft.PowerToys.Settings.UI.Library.Helpers; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; +using Microsoft.PowerToys.Settings.UI.Library.Utilities; namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels { @@ -51,10 +52,12 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels _frameEnabled = Settings.Properties.FrameEnabled.Value; _frameThickness = Settings.Properties.FrameThickness.Value; _frameColor = Settings.Properties.FrameColor.Value; + _frameAccentColor = Settings.Properties.FrameAccentColor.Value; _soundEnabled = Settings.Properties.SoundEnabled.Value; _doNotActivateOnGameMode = Settings.Properties.DoNotActivateOnGameMode.Value; + _roundCornersEnabled = Settings.Properties.RoundCornersEnabled.Value; _excludedApps = Settings.Properties.ExcludedApps.Value; - _frameAccentColor = Settings.Properties.FrameAccentColor.Value; + _windows11 = Helper.Windows11(); // set the callback functions value to hangle outgoing IPC message. SendConfigMSG = ipcMSGCallBackFunc; @@ -186,6 +189,21 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels } } + public bool RoundCornersEnabled + { + get => _roundCornersEnabled; + + set + { + if (value != _roundCornersEnabled) + { + _roundCornersEnabled = value; + Settings.Properties.RoundCornersEnabled.Value = value; + NotifyPropertyChanged(); + } + } + } + public string ExcludedApps { get => _excludedApps; @@ -216,6 +234,16 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels } } + public bool Windows11 + { + get => _windows11; + + set + { + _windows11 = value; + } + } + public void NotifyPropertyChanged([CallerMemberName] string propertyName = null) { OnPropertyChanged(propertyName); @@ -227,9 +255,11 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels private bool _frameEnabled; private int _frameThickness; private string _frameColor; + private bool _frameAccentColor; private bool _soundEnabled; private bool _doNotActivateOnGameMode; + private bool _roundCornersEnabled; private string _excludedApps; - private bool _frameAccentColor; + private bool _windows11; } } diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw index 53f8b38a77..6f5eb64fba 100644 --- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw @@ -1996,7 +1996,7 @@ From there, simply click on one of the supported files in the File Explorer and Geometric Code File type, do not translate - + Only .gcode files with embedded thumbnails are supported @@ -2125,4 +2125,7 @@ From there, simply click on one of the supported files in the File Explorer and Disable round corners when window is snapped - + + Enable round corners + + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/Views/AlwaysOnTopPage.xaml b/src/settings-ui/Settings.UI/Views/AlwaysOnTopPage.xaml index 236327e65d..053079e3e3 100644 --- a/src/settings-ui/Settings.UI/Views/AlwaysOnTopPage.xaml +++ b/src/settings-ui/Settings.UI/Views/AlwaysOnTopPage.xaml @@ -12,6 +12,7 @@ + + + +