/ src / modules / alwaysontop / AlwaysOnTop / WindowBorder.cpp
WindowBorder.cpp
  1  #include "pch.h"
  2  #include "WindowBorder.h"
  3  
  4  #include <dwmapi.h>
  5  #include "winrt/Windows.Foundation.h"
  6  
  7  #include <FrameDrawer.h>
  8  #include <ScalingUtils.h>
  9  #include <Settings.h>
 10  #include <WindowCornersUtil.h>
 11  
 12  // Non-Localizable strings
 13  namespace NonLocalizable
 14  {
 15      const wchar_t ToolWindowClassName[] = L"AlwaysOnTop_Border";
 16  }
 17  
 18  std::optional<RECT> GetFrameRect(HWND window)
 19  {
 20      RECT rect;
 21      if (!SUCCEEDED(DwmGetWindowAttribute(window, DWMWA_EXTENDED_FRAME_BOUNDS, &rect, sizeof(rect))))
 22      {
 23          return std::nullopt;
 24      }
 25  
 26      int border = AlwaysOnTopSettings::settings().frameThickness;
 27      rect.top -= border;
 28      rect.left -= border;
 29      rect.right += border;
 30      rect.bottom += border;
 31  
 32      return rect;
 33  }
 34  
 35  WindowBorder::WindowBorder(HWND window) :
 36      SettingsObserver({ SettingId::FrameColor, SettingId::FrameThickness, SettingId::FrameAccentColor, SettingId::FrameOpacity, SettingId::RoundCornersEnabled }),
 37      m_window(nullptr),
 38      m_trackingWindow(window),
 39      m_frameDrawer(nullptr)
 40  {
 41  }
 42  
 43  WindowBorder::~WindowBorder()
 44  {
 45      if (m_frameDrawer)
 46      {
 47          m_frameDrawer->Hide();
 48          m_frameDrawer = nullptr;
 49      }
 50  
 51      if (m_window)
 52      {
 53          SetWindowLongPtrW(m_window, GWLP_USERDATA, 0);
 54          DestroyWindow(m_window);
 55      }
 56  }
 57  
 58  std::unique_ptr<WindowBorder> WindowBorder::Create(HWND window, HINSTANCE hinstance)
 59  {
 60      auto self = std::unique_ptr<WindowBorder>(new WindowBorder(window));
 61      if (self->Init(hinstance))
 62      {
 63          return self;
 64      }
 65  
 66      return nullptr;
 67  }
 68  
 69  namespace
 70  {
 71      constexpr uint32_t REFRESH_BORDER_TIMER_ID = 123;
 72      constexpr uint32_t REFRESH_BORDER_INTERVAL = 100;
 73  }
 74  
 75  bool WindowBorder::Init(HINSTANCE hinstance)
 76  {
 77      if (!m_trackingWindow)
 78      {
 79          return false;
 80      }
 81  
 82      auto windowRectOpt = GetFrameRect(m_trackingWindow);
 83      if (!windowRectOpt.has_value())
 84      {
 85          return false;
 86      }
 87  
 88      RECT windowRect = windowRectOpt.value();
 89  
 90      WNDCLASSEXW wcex{};
 91      wcex.cbSize = sizeof(WNDCLASSEX);
 92      wcex.lpfnWndProc = s_WndProc;
 93      wcex.hInstance = hinstance;
 94      wcex.lpszClassName = NonLocalizable::ToolWindowClassName;
 95      wcex.hCursor = LoadCursorW(nullptr, IDC_ARROW);
 96      RegisterClassExW(&wcex);
 97  
 98      m_window = CreateWindowExW(WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_TOOLWINDOW
 99          , NonLocalizable::ToolWindowClassName
100          , L""
101          , WS_POPUP | WS_DISABLED
102          , windowRect.left
103          , windowRect.top
104          , windowRect.right - windowRect.left
105          , windowRect.bottom - windowRect.top
106          , nullptr
107          , nullptr
108          , hinstance
109          , this);
110  
111      if (!m_window)
112      {
113          return false;
114      }
115  
116      // make window transparent
117      int const pos = -GetSystemMetrics(SM_CXVIRTUALSCREEN) - 8;
118      if (wil::unique_hrgn hrgn{ CreateRectRgn(pos, 0, (pos + 1), 1) })
119      {
120          DWM_BLURBEHIND bh = { DWM_BB_ENABLE | DWM_BB_BLURREGION, TRUE, hrgn.get(), FALSE };
121          DwmEnableBlurBehindWindow(m_window, &bh);
122      }
123  
124      if (!SetLayeredWindowAttributes(m_window, RGB(0, 0, 0), 0, LWA_COLORKEY))
125      {
126          return false;
127      }
128  
129      if (!SetLayeredWindowAttributes(m_window, 0, 255, LWA_ALPHA))
130      {
131          return false;
132      }
133  
134      // set position of the border-window behind the tracking window
135      // helps to prevent border overlapping (happens after turning borders off and on)
136      SetWindowPos(m_trackingWindow
137          , m_window
138          , windowRect.left
139          , windowRect.top
140          , windowRect.right - windowRect.left
141          , windowRect.bottom - windowRect.top
142          , SWP_NOMOVE | SWP_NOSIZE);
143  
144      BOOL val = TRUE;
145      DwmSetWindowAttribute(m_window, DWMWA_EXCLUDED_FROM_PEEK, &val, sizeof(val));
146  
147      m_frameDrawer = FrameDrawer::Create(m_window);
148      if (!m_frameDrawer)
149      {
150          return false;
151      }
152  
153      m_frameDrawer->Show();
154      UpdateBorderPosition();
155      UpdateBorderProperties();
156      m_timer_id = SetTimer(m_window, REFRESH_BORDER_TIMER_ID, REFRESH_BORDER_INTERVAL, nullptr);
157  
158      return true;
159  }
160  
161  void WindowBorder::UpdateBorderPosition() const
162  {
163      if (!m_trackingWindow)
164      {
165          return;
166      }
167  
168      auto rectOpt = GetFrameRect(m_trackingWindow);
169      if (!rectOpt.has_value())
170      {
171          m_frameDrawer->Hide();
172          return;
173      }
174  
175      RECT rect = rectOpt.value();
176      SetWindowPos(m_window, m_trackingWindow, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, SWP_NOREDRAW | SWP_NOACTIVATE);
177  }
178  
179  void WindowBorder::UpdateBorderProperties() const
180  {
181      if (!m_trackingWindow || !m_frameDrawer)
182      {
183          return;
184      }
185  
186      auto windowRectOpt = GetFrameRect(m_trackingWindow);
187      if (!windowRectOpt.has_value())
188      {
189          return;
190      }
191  
192      const RECT windowRect = windowRectOpt.value();
193      SetWindowPos(m_window, m_trackingWindow, windowRect.left, windowRect.top, windowRect.right - windowRect.left, windowRect.bottom - windowRect.top, SWP_NOREDRAW | SWP_NOACTIVATE);
194  
195      RECT frameRect{ 0, 0, windowRect.right - windowRect.left, windowRect.bottom - windowRect.top };
196  
197      COLORREF color;
198      if (AlwaysOnTopSettings::settings().frameAccentColor)
199      {
200          winrt::Windows::UI::ViewManagement::UISettings settings;
201          auto accentValue = settings.GetColorValue(winrt::Windows::UI::ViewManagement::UIColorType::Accent);
202          color = RGB(accentValue.R, accentValue.G, accentValue.B);
203      }
204      else
205      {
206          color = AlwaysOnTopSettings::settings().frameColor;
207      }
208  
209      float opacity = AlwaysOnTopSettings::settings().frameOpacity / 100.0f;
210      float scalingFactor = ScalingUtils::ScalingFactor(m_trackingWindow);
211      float thickness = AlwaysOnTopSettings::settings().frameThickness * scalingFactor;
212      float cornerRadius = 0.0;
213      if (AlwaysOnTopSettings::settings().roundCornersEnabled)
214      {
215          cornerRadius = WindowCornerUtils::CornersRadius(m_trackingWindow) * scalingFactor;
216      }
217      
218      m_frameDrawer->SetBorderRect(frameRect, color, opacity, static_cast<int>(thickness), cornerRadius);
219  }
220  
221  LRESULT WindowBorder::WndProc(UINT message, WPARAM wparam, LPARAM lparam) noexcept
222  {
223      switch (message)
224      {
225      case WM_TIMER:
226      {
227          switch (wparam)
228          {
229          case REFRESH_BORDER_TIMER_ID:
230              KillTimer(m_window, m_timer_id);
231              m_timer_id = SetTimer(m_window, REFRESH_BORDER_TIMER_ID, REFRESH_BORDER_INTERVAL, nullptr);
232              UpdateBorderPosition();
233              UpdateBorderProperties();
234              break;
235          }
236          break;
237      }
238      case WM_NCDESTROY:
239      {
240          KillTimer(m_window, m_timer_id);
241          ::DefWindowProc(m_window, message, wparam, lparam);
242          SetWindowLongPtr(m_window, GWLP_USERDATA, 0);
243      }
244      break;
245  
246      case WM_ERASEBKGND:
247          return TRUE;
248  
249      // Prevent from beeping if the border was clicked
250      case WM_SETCURSOR:
251      {
252          HCURSOR hCursor = LoadCursorW(nullptr, IDC_ARROW);
253          if (hCursor)
254          {
255              SetCursor(hCursor);
256          }
257  
258          return TRUE;
259      }
260  
261      default:
262      {
263          return DefWindowProc(m_window, message, wparam, lparam);
264      }
265      }
266      return FALSE;
267  }
268  
269  void WindowBorder::SettingsUpdate(SettingId id)
270  {
271      if (!AlwaysOnTopSettings::settings().enableFrame)
272      {
273          return;
274      }
275  
276      auto windowRectOpt = GetFrameRect(m_trackingWindow);
277      if (!windowRectOpt.has_value())
278      {
279          return;
280      }
281  
282      switch (id)
283      {
284      case SettingId::FrameThickness:
285      {
286          UpdateBorderPosition();
287          UpdateBorderProperties();
288      }
289      break;
290  
291      case SettingId::FrameColor:
292      case SettingId::FrameAccentColor:
293      case SettingId::FrameOpacity:
294      case SettingId::RoundCornersEnabled:
295      {
296          UpdateBorderProperties();
297      }
298      break;
299  
300      default:
301          break;
302      }
303  }