Flutter Windows Embedder
host_window.cc
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
6 
7 #include <dwmapi.h>
8 
13 
14 namespace {
15 
16 constexpr wchar_t kWindowClassName[] = L"FLUTTER_HOST_WINDOW";
17 
18 // Clamps |size| to the size of the virtual screen. Both the parameter and
19 // return size are in physical coordinates.
20 flutter::Size ClampToVirtualScreen(flutter::Size size) {
21  double const virtual_screen_width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
22  double const virtual_screen_height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
23 
24  return flutter::Size(std::clamp(size.width(), 0.0, virtual_screen_width),
25  std::clamp(size.height(), 0.0, virtual_screen_height));
26 }
27 
28 void EnableTransparentWindowBackground(HWND hwnd,
29  flutter::WindowsProcTable const& win32) {
30  enum ACCENT_STATE { ACCENT_DISABLED = 0 };
31 
32  struct ACCENT_POLICY {
33  ACCENT_STATE AccentState;
34  DWORD AccentFlags;
35  DWORD GradientColor;
36  DWORD AnimationId;
37  };
38 
39  // Set the accent policy to disable window composition.
40  ACCENT_POLICY accent = {ACCENT_DISABLED, 2, static_cast<DWORD>(0), 0};
42  .Attrib =
43  flutter::WindowsProcTable::WINDOWCOMPOSITIONATTRIB::WCA_ACCENT_POLICY,
44  .pvData = &accent,
45  .cbData = sizeof(accent)};
46  win32.SetWindowCompositionAttribute(hwnd, &data);
47 
48  // Extend the frame into the client area and set the window's system
49  // backdrop type for visual effects.
50  MARGINS const margins = {-1};
51  win32.DwmExtendFrameIntoClientArea(hwnd, &margins);
52  INT effect_value = 1;
53  win32.DwmSetWindowAttribute(hwnd, DWMWA_SYSTEMBACKDROP_TYPE, &effect_value,
54  sizeof(BOOL));
55 }
56 
57 // Retrieves the calling thread's last-error code message as a string,
58 // or a fallback message if the error message cannot be formatted.
59 std::string GetLastErrorAsString() {
60  LPWSTR message_buffer = nullptr;
61 
62  if (DWORD const size = FormatMessage(
63  FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
64  FORMAT_MESSAGE_IGNORE_INSERTS,
65  nullptr, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
66  reinterpret_cast<LPTSTR>(&message_buffer), 0, nullptr)) {
67  std::wstring const wide_message(message_buffer, size);
68  LocalFree(message_buffer);
69  message_buffer = nullptr;
70 
71  if (int const buffer_size =
72  WideCharToMultiByte(CP_UTF8, 0, wide_message.c_str(), -1, nullptr,
73  0, nullptr, nullptr)) {
74  std::string message(buffer_size, 0);
75  WideCharToMultiByte(CP_UTF8, 0, wide_message.c_str(), -1, &message[0],
76  buffer_size, nullptr, nullptr);
77  return message;
78  }
79  }
80 
81  if (message_buffer) {
82  LocalFree(message_buffer);
83  }
84  std::ostringstream oss;
85  oss << "Format message failed with 0x" << std::hex << std::setfill('0')
86  << std::setw(8) << GetLastError();
87  return oss.str();
88 }
89 
90 // Calculates the required window size, in physical coordinates, to
91 // accommodate the given |client_size|, in logical coordinates, constrained by
92 // optional |smallest| and |biggest|, for a window with the specified
93 // |window_style| and |extended_window_style|. If |owner_hwnd| is not null, the
94 // DPI of the display with the largest area of intersection with |owner_hwnd| is
95 // used for the calculation; otherwise, the primary display's DPI is used. The
96 // resulting size includes window borders, non-client areas, and drop shadows.
97 // On error, returns std::nullopt and logs an error message.
98 std::optional<flutter::Size> GetWindowSizeForClientSize(
99  flutter::WindowsProcTable const& win32,
100  flutter::Size const& client_size,
101  std::optional<flutter::Size> smallest,
102  std::optional<flutter::Size> biggest,
103  DWORD window_style,
104  DWORD extended_window_style,
105  HWND owner_hwnd) {
106  UINT const dpi = flutter::GetDpiForHWND(owner_hwnd);
107  double const scale_factor =
108  static_cast<double>(dpi) / USER_DEFAULT_SCREEN_DPI;
109  RECT rect = {
110  .right = static_cast<LONG>(client_size.width() * scale_factor),
111  .bottom = static_cast<LONG>(client_size.height() * scale_factor)};
112 
113  if (!win32.AdjustWindowRectExForDpi(&rect, window_style, FALSE,
114  extended_window_style, dpi)) {
115  FML_LOG(ERROR) << "Failed to run AdjustWindowRectExForDpi: "
116  << GetLastErrorAsString();
117  return std::nullopt;
118  }
119 
120  double width = static_cast<double>(rect.right - rect.left);
121  double height = static_cast<double>(rect.bottom - rect.top);
122 
123  // Apply size constraints
124  double const non_client_width = width - (client_size.width() * scale_factor);
125  double const non_client_height =
126  height - (client_size.height() * scale_factor);
127  if (smallest) {
128  flutter::Size min_physical_size = ClampToVirtualScreen(
129  flutter::Size(smallest->width() * scale_factor + non_client_width,
130  smallest->height() * scale_factor + non_client_height));
131  width = std::max(width, min_physical_size.width());
132  height = std::max(height, min_physical_size.height());
133  }
134  if (biggest) {
135  flutter::Size max_physical_size = ClampToVirtualScreen(
136  flutter::Size(biggest->width() * scale_factor + non_client_width,
137  biggest->height() * scale_factor + non_client_height));
138  width = std::min(width, max_physical_size.width());
139  height = std::min(height, max_physical_size.height());
140  }
141 
142  return flutter::Size{width, height};
143 }
144 
145 // Checks whether the window class of name |class_name| is registered for the
146 // current application.
147 bool IsClassRegistered(LPCWSTR class_name) {
148  WNDCLASSEX window_class = {};
149  return GetClassInfoEx(GetModuleHandle(nullptr), class_name, &window_class) !=
150  0;
151 }
152 
153 // Window attribute that enables dark mode window decorations.
154 //
155 // Redefined in case the developer's machine has a Windows SDK older than
156 // version 10.0.22000.0.
157 // See:
158 // https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
159 #ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
160 #define DWMWA_USE_IMMERSIVE_DARK_MODE 20
161 #endif
162 
163 // Updates the window frame's theme to match the system theme.
164 void UpdateTheme(HWND window) {
165  // Registry key for app theme preference.
166  const wchar_t kGetPreferredBrightnessRegKey[] =
167  L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
168  const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
169 
170  // A value of 0 indicates apps should use dark mode. A non-zero or missing
171  // value indicates apps should use light mode.
172  DWORD light_mode;
173  DWORD light_mode_size = sizeof(light_mode);
174  LSTATUS const result =
175  RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
176  kGetPreferredBrightnessRegValue, RRF_RT_REG_DWORD, nullptr,
177  &light_mode, &light_mode_size);
178 
179  if (result == ERROR_SUCCESS) {
180  BOOL enable_dark_mode = light_mode == 0;
181  DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
182  &enable_dark_mode, sizeof(enable_dark_mode));
183  }
184 }
185 
186 // Inserts |content| into the window tree.
187 void SetChildContent(HWND content, HWND window) {
188  SetParent(content, window);
189  RECT client_rect;
190  GetClientRect(window, &client_rect);
191  MoveWindow(content, client_rect.left, client_rect.top,
192  client_rect.right - client_rect.left,
193  client_rect.bottom - client_rect.top, true);
194 }
195 
196 } // namespace
197 
198 namespace flutter {
199 
200 std::unique_ptr<HostWindow> HostWindow::CreateRegularWindow(
201  WindowManager* window_manager,
202  FlutterWindowsEngine* engine,
203  const WindowSizing& content_size) {
204  DWORD window_style = WS_OVERLAPPEDWINDOW;
205  DWORD extended_window_style = 0;
206  std::optional<Size> smallest = std::nullopt;
207  std::optional<Size> biggest = std::nullopt;
208 
209  if (content_size.has_view_constraints) {
210  smallest = Size(content_size.view_min_width, content_size.view_min_height);
211  if (content_size.view_max_width > 0 && content_size.view_max_height > 0) {
212  biggest = Size(content_size.view_max_width, content_size.view_max_height);
213  }
214  }
215 
216  // TODO(knopp): What about windows sized to content?
217  FML_CHECK(content_size.has_preferred_view_size);
218 
219  // Calculate the screen space window rectangle for the new window.
220  // Default positioning values (CW_USEDEFAULT) are used
221  // if the window has no owner.
222  Rect const initial_window_rect = [&]() -> Rect {
223  std::optional<Size> const window_size = GetWindowSizeForClientSize(
224  *engine->windows_proc_table(),
225  Size(content_size.preferred_view_width,
226  content_size.preferred_view_height),
227  smallest, biggest, window_style, extended_window_style, nullptr);
228  return {{CW_USEDEFAULT, CW_USEDEFAULT},
229  window_size ? *window_size : Size{CW_USEDEFAULT, CW_USEDEFAULT}};
230  }();
231 
232  // Set up the view.
233  auto view_window = std::make_unique<FlutterWindow>(
234  initial_window_rect.width(), initial_window_rect.height(),
235  engine->windows_proc_table());
236 
237  std::unique_ptr<FlutterWindowsView> view =
238  engine->CreateView(std::move(view_window));
239  if (view == nullptr) {
240  FML_LOG(ERROR) << "Failed to create view";
241  return nullptr;
242  }
243 
244  std::unique_ptr<FlutterWindowsViewController> view_controller =
245  std::make_unique<FlutterWindowsViewController>(nullptr, std::move(view));
246  FML_CHECK(engine->running());
247  // The Windows embedder listens to accessibility updates using the
248  // view's HWND. The embedder's accessibility features may be stale if
249  // the app was in headless mode.
250  engine->UpdateAccessibilityFeatures();
251 
252  // Register the window class.
253  if (!IsClassRegistered(kWindowClassName)) {
254  auto const idi_app_icon = 101;
255  WNDCLASSEX window_class = {};
256  window_class.cbSize = sizeof(WNDCLASSEX);
257  window_class.style = CS_HREDRAW | CS_VREDRAW;
258  window_class.lpfnWndProc = HostWindow::WndProc;
259  window_class.hInstance = GetModuleHandle(nullptr);
260  window_class.hIcon =
261  LoadIcon(window_class.hInstance, MAKEINTRESOURCE(idi_app_icon));
262  if (!window_class.hIcon) {
263  window_class.hIcon = LoadIcon(nullptr, IDI_APPLICATION);
264  }
265  window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
266  window_class.lpszClassName = kWindowClassName;
267 
268  if (!RegisterClassEx(&window_class)) {
269  FML_LOG(ERROR) << "Cannot register window class " << kWindowClassName
270  << ": " << GetLastErrorAsString();
271  return nullptr;
272  }
273  }
274 
275  // Create the native window.
276  HWND hwnd = CreateWindowEx(
277  extended_window_style, kWindowClassName, L"", window_style,
278  initial_window_rect.left(), initial_window_rect.top(),
279  initial_window_rect.width(), initial_window_rect.height(), nullptr,
280  nullptr, GetModuleHandle(nullptr), engine->windows_proc_table().get());
281  if (!hwnd) {
282  FML_LOG(ERROR) << "Cannot create window: " << GetLastErrorAsString();
283  return nullptr;
284  }
285 
286  // Adjust the window position so its origin aligns with the top-left corner
287  // of the window frame, not the window rectangle (which includes the
288  // drop-shadow). This adjustment must be done post-creation since the frame
289  // rectangle is only available after the window has been created.
290  RECT frame_rect;
291  DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &frame_rect,
292  sizeof(frame_rect));
293  RECT window_rect;
294  GetWindowRect(hwnd, &window_rect);
295  LONG const left_dropshadow_width = frame_rect.left - window_rect.left;
296  LONG const top_dropshadow_height = window_rect.top - frame_rect.top;
297  SetWindowPos(hwnd, nullptr, window_rect.left - left_dropshadow_width,
298  window_rect.top - top_dropshadow_height, 0, 0,
299  SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
300 
301  UpdateTheme(hwnd);
302 
303  SetChildContent(view_controller->view()->GetWindowHandle(), hwnd);
304 
305  // TODO(loicsharma): Hide the window until the first frame is rendered.
306  // Single window apps use the engine's next frame callback to show the
307  // window. This doesn't work for multi window apps as the engine cannot have
308  // multiple next frame callbacks. If multiple windows are created, only the
309  // last one will be shown.
310  ShowWindow(hwnd, SW_SHOWNORMAL);
311  return std::unique_ptr<HostWindow>(new HostWindow(
312  window_manager, engine, WindowArchetype::kRegular,
313  std::move(view_controller), BoxConstraints(smallest, biggest), hwnd));
314 }
315 
316 HostWindow::HostWindow(
317  WindowManager* window_manager,
318  FlutterWindowsEngine* engine,
319  WindowArchetype archetype,
320  std::unique_ptr<FlutterWindowsViewController> view_controller,
321  const BoxConstraints& box_constraints,
322  HWND hwnd)
323  : window_manager_(window_manager),
324  engine_(engine),
325  archetype_(archetype),
326  view_controller_(std::move(view_controller)),
327  window_handle_(hwnd),
328  box_constraints_(box_constraints) {
329  SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));
330 }
331 
333  if (view_controller_) {
334  // Unregister the window class. Fail silently if other windows are still
335  // using the class, as only the last window can successfully unregister it.
336  if (!UnregisterClass(kWindowClassName, GetModuleHandle(nullptr))) {
337  // Clear the error state after the failed unregistration.
338  SetLastError(ERROR_SUCCESS);
339  }
340  }
341 }
342 
344  return reinterpret_cast<HostWindow*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
345 }
346 
348  return window_handle_;
349 }
350 
351 void HostWindow::FocusViewOf(HostWindow* window) {
352  auto child_content = window->view_controller_->view()->GetWindowHandle();
353  if (window != nullptr && child_content != nullptr) {
354  SetFocus(child_content);
355  }
356 };
357 
358 LRESULT HostWindow::WndProc(HWND hwnd,
359  UINT message,
360  WPARAM wparam,
361  LPARAM lparam) {
362  if (message == WM_NCCREATE) {
363  auto* const create_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
364  auto* const windows_proc_table =
365  static_cast<WindowsProcTable*>(create_struct->lpCreateParams);
366  windows_proc_table->EnableNonClientDpiScaling(hwnd);
367  EnableTransparentWindowBackground(hwnd, *windows_proc_table);
368  } else if (HostWindow* const window = GetThisFromHandle(hwnd)) {
369  return window->HandleMessage(hwnd, message, wparam, lparam);
370  }
371 
372  return DefWindowProc(hwnd, message, wparam, lparam);
373 }
374 
375 LRESULT HostWindow::HandleMessage(HWND hwnd,
376  UINT message,
377  WPARAM wparam,
378  LPARAM lparam) {
379  auto result = engine_->window_proc_delegate_manager()->OnTopLevelWindowProc(
380  window_handle_, message, wparam, lparam);
381  if (result) {
382  return *result;
383  }
384 
385  switch (message) {
386  case WM_DPICHANGED: {
387  auto* const new_scaled_window_rect = reinterpret_cast<RECT*>(lparam);
388  LONG const width =
389  new_scaled_window_rect->right - new_scaled_window_rect->left;
390  LONG const height =
391  new_scaled_window_rect->bottom - new_scaled_window_rect->top;
392  SetWindowPos(hwnd, nullptr, new_scaled_window_rect->left,
393  new_scaled_window_rect->top, width, height,
394  SWP_NOZORDER | SWP_NOACTIVATE);
395  return 0;
396  }
397 
398  case WM_GETMINMAXINFO: {
399  RECT window_rect;
400  GetWindowRect(hwnd, &window_rect);
401  RECT client_rect;
402  GetClientRect(hwnd, &client_rect);
403  LONG const non_client_width = (window_rect.right - window_rect.left) -
404  (client_rect.right - client_rect.left);
405  LONG const non_client_height = (window_rect.bottom - window_rect.top) -
406  (client_rect.bottom - client_rect.top);
407 
408  UINT const dpi = flutter::GetDpiForHWND(hwnd);
409  double const scale_factor =
410  static_cast<double>(dpi) / USER_DEFAULT_SCREEN_DPI;
411 
412  MINMAXINFO* info = reinterpret_cast<MINMAXINFO*>(lparam);
413  Size const min_physical_size = ClampToVirtualScreen(Size(
414  box_constraints_.smallest().width() * scale_factor + non_client_width,
415  box_constraints_.smallest().height() * scale_factor +
416  non_client_height));
417 
418  info->ptMinTrackSize.x = min_physical_size.width();
419  info->ptMinTrackSize.y = min_physical_size.height();
420  Size const max_physical_size = ClampToVirtualScreen(Size(
421  box_constraints_.biggest().width() * scale_factor + non_client_width,
422  box_constraints_.biggest().height() * scale_factor +
423  non_client_height));
424 
425  info->ptMaxTrackSize.x = max_physical_size.width();
426  info->ptMaxTrackSize.y = max_physical_size.height();
427  return 0;
428  }
429 
430  case WM_SIZE: {
431  auto child_content = view_controller_->view()->GetWindowHandle();
432  if (child_content != nullptr) {
433  // Resize and reposition the child content window.
434  RECT client_rect;
435  GetClientRect(hwnd, &client_rect);
436  MoveWindow(child_content, client_rect.left, client_rect.top,
437  client_rect.right - client_rect.left,
438  client_rect.bottom - client_rect.top, TRUE);
439  }
440  return 0;
441  }
442 
443  case WM_ACTIVATE:
444  FocusViewOf(this);
445  return 0;
446 
447  case WM_MOUSEACTIVATE:
448  FocusViewOf(this);
449  return MA_ACTIVATE;
450 
451  case WM_DWMCOLORIZATIONCOLORCHANGED:
452  UpdateTheme(hwnd);
453  return 0;
454 
455  default:
456  break;
457  }
458 
459  if (!view_controller_) {
460  return 0;
461  }
462 
463  return DefWindowProc(hwnd, message, wparam, lparam);
464 }
465 
467  WINDOWINFO window_info = {.cbSize = sizeof(WINDOWINFO)};
468  GetWindowInfo(window_handle_, &window_info);
469 
470  std::optional<Size> smallest, biggest;
471  if (size.has_view_constraints) {
472  smallest = Size(size.view_min_width, size.view_min_height);
473  if (size.view_max_width > 0 && size.view_max_height > 0) {
474  biggest = Size(size.view_max_width, size.view_max_height);
475  }
476  }
477 
478  box_constraints_ = BoxConstraints(smallest, biggest);
479 
480  if (size.has_preferred_view_size) {
481  std::optional<Size> const window_size = GetWindowSizeForClientSize(
482  *engine_->windows_proc_table(),
484  box_constraints_.smallest(), box_constraints_.biggest(),
485  window_info.dwStyle, window_info.dwExStyle, nullptr);
486 
487  if (window_size) {
488  SetWindowPos(window_handle_, NULL, 0, 0, window_size->width(),
489  window_size->height(),
490  SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
491  }
492  }
493 }
494 
495 } // namespace flutter
Size smallest() const
Definition: geometry.h:97
Size biggest() const
Definition: geometry.h:96
std::shared_ptr< WindowsProcTable > windows_proc_table()
WindowProcDelegateManager * window_proc_delegate_manager()
std::unique_ptr< FlutterWindowsView > CreateView(std::unique_ptr< WindowBindingHandler > window)
HWND GetWindowHandle() const
Definition: host_window.cc:347
void SetContentSize(const WindowSizing &size)
Definition: host_window.cc:466
static std::unique_ptr< HostWindow > CreateRegularWindow(WindowManager *controller, FlutterWindowsEngine *engine, const WindowSizing &content_size)
Definition: host_window.cc:200
static HostWindow * GetThisFromHandle(HWND hwnd)
Definition: host_window.cc:343
double top() const
Definition: geometry.h:67
double height() const
Definition: geometry.h:71
double left() const
Definition: geometry.h:66
double width() const
Definition: geometry.h:70
double height() const
Definition: geometry.h:45
double width() const
Definition: geometry.h:44
std::optional< LRESULT > OnTopLevelWindowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) const
virtual BOOL AdjustWindowRectExForDpi(LPRECT lpRect, DWORD dwStyle, BOOL bMenu, DWORD dwExStyle, UINT dpi) const
virtual HRESULT DwmExtendFrameIntoClientArea(HWND hwnd, const MARGINS *pMarInset) const
virtual HRESULT DwmSetWindowAttribute(HWND hwnd, DWORD dwAttribute, LPCVOID pvAttribute, DWORD cbAttribute) const
virtual BOOL SetWindowCompositionAttribute(HWND hwnd, WINDOWCOMPOSITIONATTRIBDATA *data) const
#define DWMWA_USE_IMMERSIVE_DARK_MODE
Definition: host_window.cc:160
union flutter::testing::@90::KeyboardChange::@0 content
Win32Message message
UINT GetDpiForHWND(HWND hwnd)
Definition: dpi_utils.cc:130
WindowArchetype
Definition: windowing.h:13