/ src / modules / alwaysontop / AlwaysOnTop / FrameDrawer.cpp
FrameDrawer.cpp
  1  #include "pch.h"
  2  #include "FrameDrawer.h"
  3  
  4  #include <dwmapi.h>
  5  
  6  #include <ScalingUtils.h>
  7  
  8  namespace
  9  {
 10      size_t D2DRectUHash(D2D1_SIZE_U rect)
 11      {
 12          using pod_repr_t = uint64_t;
 13          static_assert(sizeof(D2D1_SIZE_U) == sizeof(pod_repr_t));
 14          std::hash<pod_repr_t> hasher{};
 15          return hasher(*reinterpret_cast<const pod_repr_t*>(&rect));
 16      }
 17  }
 18  
 19  std::unique_ptr<FrameDrawer> FrameDrawer::Create(HWND window)
 20  {
 21      auto self = std::make_unique<FrameDrawer>(window);
 22      if (self->Init())
 23      {
 24          return self;
 25      }
 26  
 27      return nullptr;
 28  }
 29  
 30  FrameDrawer::FrameDrawer(HWND window) :
 31      m_window(window)
 32  {
 33  }
 34  
 35  bool FrameDrawer::CreateRenderTargets(const RECT& clientRect)
 36  {
 37      HRESULT hr;
 38  
 39      constexpr float DPI = 96.f; // Always using the default in DPI-aware mode
 40      const auto renderTargetProperties = D2D1::RenderTargetProperties(
 41          D2D1_RENDER_TARGET_TYPE_DEFAULT,
 42          D2D1::PixelFormat(DXGI_FORMAT_UNKNOWN, D2D1_ALPHA_MODE_PREMULTIPLIED),
 43          DPI,
 44          DPI);
 45  
 46      const auto renderTargetSize = D2D1::SizeU(clientRect.right - clientRect.left, clientRect.bottom - clientRect.top);
 47      const auto rectHash = D2DRectUHash(renderTargetSize);
 48      if (m_renderTarget && rectHash == m_renderTargetSizeHash)
 49      {
 50          // Already at the desired size -> do nothing
 51          return true;
 52      }
 53  
 54      m_renderTarget = nullptr;
 55  
 56      const auto hwndRenderTargetProperties = D2D1::HwndRenderTargetProperties(m_window, renderTargetSize, D2D1_PRESENT_OPTIONS_NONE);
 57  
 58      hr = GetD2DFactory()->CreateHwndRenderTarget(renderTargetProperties, hwndRenderTargetProperties, m_renderTarget.put());
 59  
 60      if (!SUCCEEDED(hr) || !m_renderTarget)
 61      {
 62          return false;
 63      }
 64  
 65      m_renderTarget->SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
 66      m_renderTargetSizeHash = rectHash;
 67  
 68      return true;
 69  }
 70  
 71  bool FrameDrawer::Init()
 72  {
 73      RECT clientRect;
 74      if (!SUCCEEDED(DwmGetWindowAttribute(m_window, DWMWA_EXTENDED_FRAME_BOUNDS, &clientRect, sizeof(clientRect))))
 75      {
 76          return false;
 77      }
 78  
 79      return CreateRenderTargets(clientRect);
 80  }
 81  
 82  void FrameDrawer::Hide()
 83  {
 84      ShowWindow(m_window, SW_HIDE);
 85  }
 86  
 87  void FrameDrawer::Show()
 88  {
 89      ShowWindow(m_window, SW_SHOWNA);
 90      Render();
 91  }
 92  
 93  void FrameDrawer::SetBorderRect(RECT windowRect, COLORREF rgb, float alpha, int thickness, float radius)
 94  {
 95      auto newSceneRect = DrawableRect{
 96          .borderColor = ConvertColor(rgb, alpha),
 97          .thickness = thickness,
 98      };
 99  
100      if (radius != 0)
101      {
102          newSceneRect.roundedRect = ConvertRect(windowRect, thickness, radius);
103      }
104      else
105      {
106          newSceneRect.rect = ConvertRect(windowRect, thickness);
107      }
108      
109      const bool colorUpdated = std::memcmp(&m_sceneRect.borderColor, &newSceneRect.borderColor, sizeof(newSceneRect.borderColor));
110      const bool thicknessUpdated = m_sceneRect.thickness != newSceneRect.thickness;
111      const bool cornersUpdated = m_sceneRect.rect.has_value() != newSceneRect.rect.has_value() || m_sceneRect.roundedRect.has_value() != newSceneRect.roundedRect.has_value();
112      const bool needsRedraw = colorUpdated || thicknessUpdated || cornersUpdated;
113  
114      RECT clientRect;
115      if (!SUCCEEDED(DwmGetWindowAttribute(m_window, DWMWA_EXTENDED_FRAME_BOUNDS, &clientRect, sizeof(clientRect))))
116      {
117          return;
118      }
119  
120      m_sceneRect = std::move(newSceneRect);
121  
122      const auto renderTargetSize = D2D1::SizeU(clientRect.right - clientRect.left, clientRect.bottom - clientRect.top);
123  
124      const auto rectHash = D2DRectUHash(renderTargetSize);
125  
126      const bool atTheDesiredSize = (rectHash == m_renderTargetSizeHash) && m_renderTarget;
127      if (!atTheDesiredSize)
128      {
129          const bool resizeOk = m_renderTarget && SUCCEEDED(m_renderTarget->Resize(renderTargetSize));
130          if (!resizeOk)
131          {
132              if (!CreateRenderTargets(clientRect))
133              {
134                  Logger::error(L"Failed to create render targets");
135              }
136          }
137          else
138          {
139              m_renderTargetSizeHash = rectHash;
140          }
141      }
142  
143      if (colorUpdated)
144      {
145          m_borderBrush = nullptr;
146          if (m_renderTarget)
147          {
148              m_renderTarget->CreateSolidColorBrush(m_sceneRect.borderColor, m_borderBrush.put());
149          }
150      }
151  
152      if (!atTheDesiredSize || needsRedraw)
153      {
154          Render();
155      }
156  }
157  
158  ID2D1Factory* FrameDrawer::GetD2DFactory()
159  {
160      static auto pD2DFactory = [] {
161          ID2D1Factory* res = nullptr;
162          D2D1CreateFactory(D2D1_FACTORY_TYPE_MULTI_THREADED, &res);
163          return res;
164      }();
165      return pD2DFactory;
166  }
167  
168  IDWriteFactory* FrameDrawer::GetWriteFactory()
169  {
170      static auto pDWriteFactory = [] {
171          IUnknown* res = nullptr;
172          DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), &res);
173          return reinterpret_cast<IDWriteFactory*>(res);
174      }();
175      return pDWriteFactory;
176  }
177  
178  D2D1_COLOR_F FrameDrawer::ConvertColor(COLORREF color, float alpha)
179  {
180      return D2D1::ColorF(GetRValue(color) / 255.f,
181                          GetGValue(color) / 255.f,
182                          GetBValue(color) / 255.f,
183                          alpha);
184  }
185  
186  D2D1_ROUNDED_RECT FrameDrawer::ConvertRect(RECT rect, int thickness, float radius)
187  {
188      float halfThickness = thickness / 2.0f;
189  
190      // 1 is needed to eliminate the gap between border and window
191      auto d2d1Rect = D2D1::RectF(static_cast<float>(rect.left) + halfThickness + 1, 
192          static_cast<float>(rect.top) + halfThickness + 1, 
193          static_cast<float>(rect.right) - halfThickness - 1, 
194          static_cast<float>(rect.bottom) - halfThickness - 1);
195      return D2D1::RoundedRect(d2d1Rect, radius, radius);
196  }
197  
198  D2D1_RECT_F FrameDrawer::ConvertRect(RECT rect, int thickness)
199  {
200      float halfThickness = thickness / 2.0f;
201  
202      // 1 is needed to eliminate the gap between border and window
203      return D2D1::RectF(static_cast<float>(rect.left) + halfThickness + 1,
204          static_cast<float>(rect.top) + halfThickness + 1,
205          static_cast<float>(rect.right) - halfThickness - 1,
206          static_cast<float>(rect.bottom) - halfThickness - 1);
207  }
208  
209  void FrameDrawer::Render()
210  {
211      if (!m_renderTarget || !m_borderBrush)
212      {
213          return;
214      }
215  
216      m_renderTarget->BeginDraw();
217  
218      m_renderTarget->Clear(D2D1::ColorF(0.f, 0.f, 0.f, 0.f));
219  
220      // The border stroke is centered on the line.
221  
222      if (m_sceneRect.roundedRect)
223      {
224          m_renderTarget->DrawRoundedRectangle(m_sceneRect.roundedRect.value(), m_borderBrush.get(), static_cast<float>(m_sceneRect.thickness));
225      }
226      else if (m_sceneRect.rect)
227      {
228          m_renderTarget->DrawRectangle(m_sceneRect.rect.value(), m_borderBrush.get(), static_cast<float>(m_sceneRect.thickness));
229      }
230      
231      m_renderTarget->EndDraw();
232  }