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 
10 
11 #include <dwmapi.h>
12 
20 
21 namespace {
22 
23 constexpr wchar_t kWindowClassName[] = L"FLUTTER_HOST_WINDOW";
24 
25 // Clamps |size| to the size of the virtual screen. Both the parameter and
26 // return size are in physical coordinates.
27 flutter::Size ClampToVirtualScreen(flutter::Size size) {
28  double const virtual_screen_width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
29  double const virtual_screen_height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
30 
31  return flutter::Size(std::clamp(size.width(), 0.0, virtual_screen_width),
32  std::clamp(size.height(), 0.0, virtual_screen_height));
33 }
34 
35 // Retrieves the calling thread's last-error code message as a string,
36 // or a fallback message if the error message cannot be formatted.
37 std::string GetLastErrorAsString() {
38  LPWSTR message_buffer = nullptr;
39 
40  if (DWORD const size = FormatMessage(
41  FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
42  FORMAT_MESSAGE_IGNORE_INSERTS,
43  nullptr, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
44  reinterpret_cast<LPTSTR>(&message_buffer), 0, nullptr)) {
45  std::wstring const wide_message(message_buffer, size);
46  LocalFree(message_buffer);
47  message_buffer = nullptr;
48 
49  if (int const buffer_size =
50  WideCharToMultiByte(CP_UTF8, 0, wide_message.c_str(), -1, nullptr,
51  0, nullptr, nullptr)) {
52  std::string message(buffer_size, 0);
53  WideCharToMultiByte(CP_UTF8, 0, wide_message.c_str(), -1, &message[0],
54  buffer_size, nullptr, nullptr);
55  return message;
56  }
57  }
58 
59  if (message_buffer) {
60  LocalFree(message_buffer);
61  }
62  std::ostringstream oss;
63  oss << "Format message failed with 0x" << std::hex << std::setfill('0')
64  << std::setw(8) << GetLastError();
65  return oss.str();
66 }
67 
68 // Checks whether the window class of name |class_name| is registered for the
69 // current application.
70 bool IsClassRegistered(LPCWSTR class_name) {
71  WNDCLASSEX window_class = {};
72  return GetClassInfoEx(GetModuleHandle(nullptr), class_name, &window_class) !=
73  0;
74 }
75 
76 // Window attribute that enables dark mode window decorations.
77 //
78 // Redefined in case the developer's machine has a Windows SDK older than
79 // version 10.0.22000.0.
80 // See:
81 // https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
82 #ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
83 #define DWMWA_USE_IMMERSIVE_DARK_MODE 20
84 #endif
85 
86 // Updates the window frame's theme to match the system theme.
87 void UpdateTheme(HWND window) {
88  // Registry key for app theme preference.
89  const wchar_t kGetPreferredBrightnessRegKey[] =
90  L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
91  const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
92 
93  // A value of 0 indicates apps should use dark mode. A non-zero or missing
94  // value indicates apps should use light mode.
95  DWORD light_mode;
96  DWORD light_mode_size = sizeof(light_mode);
97  LSTATUS const result =
98  RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
99  kGetPreferredBrightnessRegValue, RRF_RT_REG_DWORD, nullptr,
100  &light_mode, &light_mode_size);
101 
102  if (result == ERROR_SUCCESS) {
103  BOOL enable_dark_mode = light_mode == 0;
104  DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
105  &enable_dark_mode, sizeof(enable_dark_mode));
106  }
107 }
108 
109 // Inserts |content| into the window tree.
110 void SetChildContent(HWND content, HWND window) {
111  SetParent(content, window);
112  RECT client_rect;
113  GetClientRect(window, &client_rect);
114  MoveWindow(content, client_rect.left, client_rect.top,
115  client_rect.right - client_rect.left,
116  client_rect.bottom - client_rect.top, true);
117 }
118 
119 // Adjusts a 1D segment (defined by origin and size) to fit entirely within
120 // a destination segment. If the segment is larger than the destination, it is
121 // first shrunk to fit. Then, it's shifted to be within the bounds.
122 //
123 // Let the destination be "{...}" and the segment to adjust be "[...]".
124 //
125 // Case 1: The segment sticks out to the right.
126 //
127 // Before: {------[----}------]
128 // After: {------[----]}
129 //
130 // Case 2: The segment sticks out to the left.
131 //
132 // Before: [------{----]------}
133 // After: {[----]------}
134 void AdjustAlongAxis(LONG dst_origin, LONG dst_size, LONG* origin, LONG* size) {
135  *size = std::min(dst_size, *size);
136  if (*origin < dst_origin)
137  *origin = dst_origin;
138  else
139  *origin = std::min(dst_origin + dst_size, *origin + *size) - *size;
140 }
141 
142 RECT AdjustToFit(const RECT& parent, const RECT& child) {
143  auto new_x = child.left;
144  auto new_y = child.top;
145  auto new_width = flutter::RectWidth(child);
146  auto new_height = flutter::RectHeight(child);
147  AdjustAlongAxis(parent.left, flutter::RectWidth(parent), &new_x, &new_width);
148  AdjustAlongAxis(parent.top, flutter::RectHeight(parent), &new_y, &new_height);
149  RECT result;
150  result.left = new_x;
151  result.right = new_x + new_width;
152  result.top = new_y;
153  result.bottom = new_y + new_height;
154  return result;
155 }
156 
157 flutter::BoxConstraints FromWindowConstraints(
158  const flutter::WindowConstraints& preferred_constraints) {
159  std::optional<flutter::Size> smallest, biggest;
160  if (preferred_constraints.has_view_constraints) {
161  smallest = flutter::Size(preferred_constraints.view_min_width,
162  preferred_constraints.view_min_height);
163  if (preferred_constraints.view_max_width > 0 &&
164  preferred_constraints.view_max_height > 0) {
165  biggest = flutter::Size(preferred_constraints.view_max_width,
166  preferred_constraints.view_max_height);
167  }
168  }
169 
170  return flutter::BoxConstraints(smallest, biggest);
171 }
172 
173 } // namespace
174 
175 namespace flutter {
176 
177 std::unique_ptr<HostWindow> HostWindow::CreateRegularWindow(
178  WindowManager* window_manager,
179  FlutterWindowsEngine* engine,
180  const WindowSizeRequest& preferred_size,
181  const WindowConstraints& preferred_constraints,
182  LPCWSTR title,
183  bool sized_to_content,
184  bool resizable) {
185  return std::unique_ptr<HostWindow>(
186  new HostWindowRegular(window_manager, engine, preferred_size,
187  FromWindowConstraints(preferred_constraints), title,
188  sized_to_content, resizable));
189 }
190 
191 std::unique_ptr<HostWindow> HostWindow::CreateDialogWindow(
192  WindowManager* window_manager,
193  FlutterWindowsEngine* engine,
194  const WindowSizeRequest& preferred_size,
195  const WindowConstraints& preferred_constraints,
196  LPCWSTR title,
197  HWND parent,
198  bool sized_to_content,
199  bool resizable) {
200  return std::unique_ptr<HostWindow>(new HostWindowDialog(
201  window_manager, engine, preferred_size,
202  FromWindowConstraints(preferred_constraints), title,
203  parent ? parent : std::optional<HWND>(), sized_to_content, resizable));
204 }
205 
206 std::unique_ptr<HostWindow> HostWindow::CreateTooltipWindow(
207  WindowManager* window_manager,
208  FlutterWindowsEngine* engine,
209  const WindowConstraints& preferred_constraints,
210  GetWindowPositionCallback get_position_callback,
211  HWND parent) {
212  return std::unique_ptr<HostWindowTooltip>(new HostWindowTooltip(
213  window_manager, engine, FromWindowConstraints(preferred_constraints),
214  get_position_callback, parent));
215 }
216 
217 std::unique_ptr<HostWindow> HostWindow::CreatePopupWindow(
218  WindowManager* window_manager,
219  FlutterWindowsEngine* engine,
220  const WindowConstraints& preferred_constraints,
221  GetWindowPositionCallback get_position_callback,
222  HWND parent) {
223  return std::unique_ptr<HostWindowPopup>(new HostWindowPopup(
224  window_manager, engine, FromWindowConstraints(preferred_constraints),
225  get_position_callback, parent));
226 }
227 
229  FlutterWindowsEngine* engine)
230  : window_manager_(window_manager), engine_(engine) {}
231 
233  HostWindowInitializationParams const& params) {
234  // Set up the view.
235  auto view_window = std::make_unique<FlutterWindow>(
236  params.initial_window_rect.width(), params.initial_window_rect.height(),
238 
239  std::unique_ptr<FlutterWindowsView> view =
240  engine_->CreateView(std::move(view_window), params.is_sized_to_content,
241  params.box_constraints, params.sizing_delegate);
242  FML_CHECK(view != nullptr);
243 
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.
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  FML_CHECK(RegisterClassEx(&window_class));
269  }
270 
271  // Create the native window.
272  window_handle_ = CreateWindowEx(
273  params.extended_window_style, kWindowClassName, params.title,
274  params.window_style, params.initial_window_rect.left(),
275  params.initial_window_rect.top(), params.initial_window_rect.width(),
276  params.initial_window_rect.height(),
277  params.owner_window ? *params.owner_window : nullptr, nullptr,
278  GetModuleHandle(nullptr), engine_->windows_proc_table().get());
279  FML_CHECK(window_handle_ != nullptr);
280 
281  // Adjust the window position so its origin aligns with the top-left corner
282  // of the window frame, not the window rectangle (which includes the
283  // drop-shadow). This adjustment must be done post-creation since the frame
284  // rectangle is only available after the window has been created.
285  RECT frame_rect;
286  DwmGetWindowAttribute(window_handle_, DWMWA_EXTENDED_FRAME_BOUNDS,
287  &frame_rect, sizeof(frame_rect));
288  RECT window_rect;
289  GetWindowRect(window_handle_, &window_rect);
290  LONG const left_dropshadow_width = frame_rect.left - window_rect.left;
291  LONG const top_dropshadow_height = window_rect.top - frame_rect.top;
292  SetWindowPos(window_handle_, nullptr,
293  window_rect.left - left_dropshadow_width,
294  window_rect.top - top_dropshadow_height, 0, 0,
295  SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
296 
297  UpdateTheme(window_handle_);
298 
299  SetChildContent(view_controller_->view()->GetWindowHandle(), window_handle_);
300 
301  // Defer showing the window until the first frame is rendered so the user
302  // doesn't see a blank window. Each view has its own first-frame callback,
303  // so this works correctly for multi-window apps.
304  view_controller_->view()->SetFirstFrameCallback(
305  [hwnd = window_handle_, cmd_show = params.nCmdShow]() {
306  if (::IsWindow(hwnd)) {
307  ShowWindow(hwnd, cmd_show);
308  }
309  });
310  archetype_ = params.archetype;
311  SetWindowLongPtr(window_handle_, GWLP_USERDATA,
312  reinterpret_cast<LONG_PTR>(this));
313 }
314 
315 HostWindow::~HostWindow() {
316  if (view_controller_) {
317  // Unregister the window class. Fail silently if other windows are still
318  // using the class, as only the last window can successfully unregister it.
319  if (!UnregisterClass(kWindowClassName, GetModuleHandle(nullptr))) {
320  // Clear the error state after the failed unregistration.
321  SetLastError(ERROR_SUCCESS);
322  }
323  }
324 }
325 
326 HostWindow* HostWindow::GetThisFromHandle(HWND hwnd) {
327  wchar_t class_name[256];
328  if (!GetClassName(hwnd, class_name, sizeof(class_name) / sizeof(wchar_t))) {
329  FML_LOG(ERROR) << "Failed to get class name for window handle " << hwnd
330  << ": " << GetLastErrorAsString();
331  return nullptr;
332  }
333  // Ignore window handles that do not match the expected class name.
334  if (wcscmp(class_name, kWindowClassName) != 0) {
335  return nullptr;
336  }
337 
338  return reinterpret_cast<HostWindow*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
339 }
340 
341 HWND HostWindow::GetWindowHandle() const {
342  return window_handle_;
343 }
344 
345 HWND HostWindow::GetFlutterViewWindowHandle() const {
346  return view_controller_->view()->GetWindowHandle();
347 }
348 
349 void HostWindow::FocusRootViewOf(HostWindow* window) {
350  auto child_content = window->view_controller_->view()->GetWindowHandle();
351  if (window != nullptr && child_content != nullptr) {
352  SetFocus(child_content);
353  }
354 };
355 
356 void HostWindow::HandleWindowActivation(HWND hwnd, WPARAM wparam) {
357  if (LOWORD(wparam) == WA_INACTIVE) {
358  // The window is being deactivated; leave focus untouched. Focusing this
359  // window's view would call SetFocus on its content, which reactivates the
360  // window (SetFocus activates the parent of the focused window) and pulls it
361  // back to the top of the z-order, taking activation from the window that is
362  // being activated.
363  return;
364  }
365 
366  if (!IsWindowEnabled(hwnd)) {
367  // Prevent a disabled window (e.g. the owner of a modal dialog) from being
368  // activated using the task switcher by redirecting focus and activation to
369  // the first enabled descendant.
370  if (HostWindow* const enabled_descendant = FindFirstEnabledDescendant()) {
371  SetActiveWindow(enabled_descendant->GetWindowHandle());
372  FocusRootViewOf(this);
373  }
374  return;
375  }
376 
377  FocusRootViewOf(this);
378 }
379 
380 LRESULT HostWindow::WndProc(HWND hwnd,
381  UINT message,
382  WPARAM wparam,
383  LPARAM lparam) {
384  if (message == WM_NCCREATE) {
385  auto* const create_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
386  auto* const windows_proc_table =
387  static_cast<WindowsProcTable*>(create_struct->lpCreateParams);
388  windows_proc_table->EnableNonClientDpiScaling(hwnd);
389  } else if (HostWindow* const window = GetThisFromHandle(hwnd)) {
390  return window->HandleMessage(hwnd, message, wparam, lparam);
391  }
392 
393  return DefWindowProc(hwnd, message, wparam, lparam);
394 }
395 
396 LRESULT HostWindow::HandleMessage(HWND hwnd,
397  UINT message,
398  WPARAM wparam,
399  LPARAM lparam) {
400  auto result = engine_->window_proc_delegate_manager()->OnTopLevelWindowProc(
401  window_handle_, message, wparam, lparam);
402  if (result) {
403  return *result;
404  }
405 
406  switch (message) {
407  case WM_DESTROY:
408  is_being_destroyed_ = true;
409  break;
410 
411  case WM_NCLBUTTONDOWN: {
412  // Fix for 500ms hang after user clicks on the title bar, but before
413  // moving mouse. Reference:
414  // https://gamedev.net/forums/topic/672094-keeping-things-moving-during-win32-moveresize-events/5254386/
415  if (SendMessage(window_handle_, WM_NCHITTEST, wparam, lparam) ==
416  HTCAPTION) {
417  POINT cursorPos;
418  // Get the current cursor position and synthesize WM_MOUSEMOVE to
419  // unblock default window proc implementation for WM_NCLBUTTONDOWN at
420  // HTCAPTION.
421  GetCursorPos(&cursorPos);
422  ScreenToClient(window_handle_, &cursorPos);
423  PostMessage(window_handle_, WM_MOUSEMOVE, 0,
424  MAKELPARAM(cursorPos.x, cursorPos.y));
425  }
426  break;
427  }
428 
429  case WM_DPICHANGED: {
430  auto* const new_scaled_window_rect = reinterpret_cast<RECT*>(lparam);
431  LONG const width =
432  new_scaled_window_rect->right - new_scaled_window_rect->left;
433  LONG const height =
434  new_scaled_window_rect->bottom - new_scaled_window_rect->top;
435  SetWindowPos(hwnd, nullptr, new_scaled_window_rect->left,
436  new_scaled_window_rect->top, width, height,
437  SWP_NOZORDER | SWP_NOACTIVATE);
438  return 0;
439  }
440 
441  case WM_GETMINMAXINFO: {
442  RECT window_rect;
443  GetWindowRect(hwnd, &window_rect);
444  RECT client_rect;
445  GetClientRect(hwnd, &client_rect);
446  LONG const non_client_width = (window_rect.right - window_rect.left) -
447  (client_rect.right - client_rect.left);
448  LONG const non_client_height = (window_rect.bottom - window_rect.top) -
449  (client_rect.bottom - client_rect.top);
450 
451  UINT const dpi = flutter::GetDpiForHWND(hwnd);
452  double const scale_factor =
453  static_cast<double>(dpi) / USER_DEFAULT_SCREEN_DPI;
454 
455  MINMAXINFO* info = reinterpret_cast<MINMAXINFO*>(lparam);
456  Size const min_physical_size = ClampToVirtualScreen(Size(
457  box_constraints_.smallest().width() * scale_factor + non_client_width,
458  box_constraints_.smallest().height() * scale_factor +
459  non_client_height));
460 
461  info->ptMinTrackSize.x = min_physical_size.width();
462  info->ptMinTrackSize.y = min_physical_size.height();
463  Size const max_physical_size = ClampToVirtualScreen(Size(
464  box_constraints_.biggest().width() * scale_factor + non_client_width,
465  box_constraints_.biggest().height() * scale_factor +
466  non_client_height));
467 
468  info->ptMaxTrackSize.x = max_physical_size.width();
469  info->ptMaxTrackSize.y = max_physical_size.height();
470  return 0;
471  }
472 
473  case WM_SIZE: {
474  auto child_content = view_controller_->view()->GetWindowHandle();
475  if (child_content != nullptr) {
476  // Resize and reposition the child content window.
477  RECT client_rect;
478  GetClientRect(hwnd, &client_rect);
479  MoveWindow(child_content, client_rect.left, client_rect.top,
480  client_rect.right - client_rect.left,
481  client_rect.bottom - client_rect.top, TRUE);
482  }
483  return 0;
484  }
485 
486  case WM_ACTIVATE:
487  HandleWindowActivation(hwnd, wparam);
488  return 0;
489 
490  case WM_DWMCOLORIZATIONCOLORCHANGED:
491  UpdateTheme(hwnd);
492  return 0;
493 
494  default:
495  break;
496  }
497 
498  if (!view_controller_) {
499  return 0;
500  }
501 
502  return DefWindowProc(hwnd, message, wparam, lparam);
503 }
504 
505 void HostWindow::SetContentSize(const WindowSizeRequest& size) {
506  if (!size.has_preferred_view_size) {
507  return;
508  }
509 
510  if (GetFullscreen()) {
511  std::optional<Size> const window_size = GetWindowSizeForClientSize(
512  *engine_->windows_proc_table(),
514  box_constraints_.smallest(), box_constraints_.biggest(),
515  saved_window_info_.style, saved_window_info_.ex_style, nullptr);
516  if (!window_size) {
517  return;
518  }
519 
520  saved_window_info_.client_size =
522  .height = size.preferred_view_height};
523  saved_window_info_.rect.right =
524  saved_window_info_.rect.left + static_cast<LONG>(window_size->width());
525  saved_window_info_.rect.bottom =
526  saved_window_info_.rect.top + static_cast<LONG>(window_size->height());
527  } else {
528  WINDOWINFO window_info = {.cbSize = sizeof(WINDOWINFO)};
529  GetWindowInfo(window_handle_, &window_info);
530 
531  std::optional<Size> const window_size = GetWindowSizeForClientSize(
532  *engine_->windows_proc_table(),
534  box_constraints_.smallest(), box_constraints_.biggest(),
535  window_info.dwStyle, window_info.dwExStyle, nullptr);
536 
537  if (!window_size) {
538  return;
539  }
540 
541  SetWindowPos(window_handle_, NULL, 0, 0, window_size->width(),
542  window_size->height(),
543  SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
544  }
545 }
546 
547 void HostWindow::SetConstraints(const WindowConstraints& constraints) {
548  box_constraints_ = FromWindowConstraints(constraints);
549 
550  if (GetFullscreen()) {
551  std::optional<Size> const window_size = GetWindowSizeForClientSize(
552  *engine_->windows_proc_table(),
553  Size(saved_window_info_.client_size.width,
554  saved_window_info_.client_size.height),
555  box_constraints_.smallest(), box_constraints_.biggest(),
556  saved_window_info_.style, saved_window_info_.ex_style, nullptr);
557  if (!window_size) {
558  return;
559  }
560 
561  saved_window_info_.rect.right =
562  saved_window_info_.rect.left + static_cast<LONG>(window_size->width());
563  saved_window_info_.rect.bottom =
564  saved_window_info_.rect.top + static_cast<LONG>(window_size->height());
565  } else {
566  auto const client_size = GetWindowContentSize(window_handle_);
567  auto const current_size = Size(client_size.width, client_size.height);
568  WINDOWINFO window_info = {.cbSize = sizeof(WINDOWINFO)};
569  GetWindowInfo(window_handle_, &window_info);
570  std::optional<Size> const window_size = GetWindowSizeForClientSize(
571  *engine_->windows_proc_table(), current_size,
572  box_constraints_.smallest(), box_constraints_.biggest(),
573  window_info.dwStyle, window_info.dwExStyle, nullptr);
574 
575  if (window_size && current_size != window_size) {
576  SetWindowPos(window_handle_, NULL, 0, 0, window_size->width(),
577  window_size->height(),
578  SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
579  }
580  }
581 }
582 
583 // The fullscreen method is largely adapted from the method found in chromium:
584 // See:
585 //
586 // * https://chromium.googlesource.com/chromium/src/+/refs/heads/main/ui/views/win/fullscreen_handler.h
587 // * https://chromium.googlesource.com/chromium/src/+/refs/heads/main/ui/views/win/fullscreen_handler.cc
588 void HostWindow::SetFullscreen(
589  bool fullscreen,
590  std::optional<FlutterEngineDisplayId> display_id) {
591  if (fullscreen == GetFullscreen()) {
592  return;
593  }
594 
595  if (fullscreen) {
596  WINDOWINFO window_info = {.cbSize = sizeof(WINDOWINFO)};
597  GetWindowInfo(window_handle_, &window_info);
598  saved_window_info_.style = window_info.dwStyle;
599  saved_window_info_.ex_style = window_info.dwExStyle;
600  // Store the original window rect, DPI, and monitor info to detect changes
601  // and more accurately restore window placements when exiting fullscreen.
602  ::GetWindowRect(window_handle_, &saved_window_info_.rect);
603  saved_window_info_.client_size = GetWindowContentSize(window_handle_);
604  saved_window_info_.dpi = GetDpiForHWND(window_handle_);
605  saved_window_info_.monitor =
606  MonitorFromWindow(window_handle_, MONITOR_DEFAULTTONEAREST);
607  saved_window_info_.monitor_info.cbSize =
608  sizeof(saved_window_info_.monitor_info);
609  GetMonitorInfo(saved_window_info_.monitor,
610  &saved_window_info_.monitor_info);
611  }
612 
613  if (fullscreen) {
614  // Next, get the raw HMONITOR that we want to be fullscreened on
615  HMONITOR monitor =
616  MonitorFromWindow(window_handle_, MONITOR_DEFAULTTONEAREST);
617  if (display_id) {
618  if (auto const display =
619  engine_->display_manager()->FindById(display_id.value())) {
620  monitor = reinterpret_cast<HMONITOR>(display->display_id);
621  }
622  }
623 
624  MONITORINFO monitor_info;
625  monitor_info.cbSize = sizeof(monitor_info);
626  if (!GetMonitorInfo(monitor, &monitor_info)) {
627  FML_LOG(ERROR) << "Cannot set window fullscreen because the monitor info "
628  "was not found";
629  }
630 
631  auto const width = RectWidth(monitor_info.rcMonitor);
632  auto const height = RectHeight(monitor_info.rcMonitor);
633  WINDOWINFO window_info = {.cbSize = sizeof(WINDOWINFO)};
634  GetWindowInfo(window_handle_, &window_info);
635 
636  // Set new window style and size.
637  SetWindowLong(window_handle_, GWL_STYLE,
638  saved_window_info_.style & ~(WS_CAPTION | WS_THICKFRAME));
639  SetWindowLong(
640  window_handle_, GWL_EXSTYLE,
641  saved_window_info_.ex_style & ~(WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE |
642  WS_EX_CLIENTEDGE | WS_EX_STATICEDGE));
643 
644  // We call SetWindowPos first to set the window flags immediately. This
645  // makes it so that the WM_GETMINMAXINFO gets called with the correct window
646  // and content sizes.
647  SetWindowPos(window_handle_, NULL, 0, 0, 0, 0,
648  SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
649 
650  SetWindowPos(window_handle_, nullptr, monitor_info.rcMonitor.left,
651  monitor_info.rcMonitor.top, width, height,
652  SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
653  } else {
654  // Restore the window style and bounds saved prior to entering fullscreen.
655  // Use WS_VISIBLE for windows shown after SetFullscreen: crbug.com/1062251.
656  // Making multiple window adjustments here is ugly, but if SetWindowPos()
657  // doesn't redraw, the taskbar won't be repainted.
658  SetWindowLong(window_handle_, GWL_STYLE,
659  saved_window_info_.style | WS_VISIBLE);
660  SetWindowLong(window_handle_, GWL_EXSTYLE, saved_window_info_.ex_style);
661 
662  // We call SetWindowPos first to set the window flags immediately. This
663  // makes it so that the WM_GETMINMAXINFO gets called with the correct window
664  // and content sizes.
665  SetWindowPos(window_handle_, NULL, 0, 0, 0, 0,
666  SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
667 
668  HMONITOR monitor =
669  MonitorFromRect(&saved_window_info_.rect, MONITOR_DEFAULTTONEAREST);
670  MONITORINFO monitor_info;
671  monitor_info.cbSize = sizeof(monitor_info);
672  GetMonitorInfo(monitor, &monitor_info);
673 
674  auto window_rect = saved_window_info_.rect;
675 
676  // Adjust the window bounds to restore, if displays were disconnected,
677  // virtually rearranged, or otherwise changed metrics during fullscreen.
678  if (monitor != saved_window_info_.monitor ||
679  !AreRectsEqual(saved_window_info_.monitor_info.rcWork,
680  monitor_info.rcWork)) {
681  window_rect = AdjustToFit(monitor_info.rcWork, window_rect);
682  }
683 
684  auto const fullscreen_dpi = GetDpiForHWND(window_handle_);
685  SetWindowPos(window_handle_, nullptr, window_rect.left, window_rect.top,
686  RectWidth(window_rect), RectHeight(window_rect),
687  SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
688  auto const final_dpi = GetDpiForHWND(window_handle_);
689  if (final_dpi != saved_window_info_.dpi || final_dpi != fullscreen_dpi) {
690  // Reissue SetWindowPos if the DPI changed from saved or fullscreen DPIs.
691  // The first call may misinterpret bounds spanning displays, if the
692  // fullscreen display's DPI does not match the target display's DPI.
693  //
694  // Scale and clamp the bounds if the final DPI changed from the saved DPI.
695  // This more accurately matches the original placement, while avoiding
696  // unexpected offscreen placement in a recongifured multi-screen space.
697  if (final_dpi != saved_window_info_.dpi) {
698  auto const scale =
699  final_dpi / static_cast<float>(saved_window_info_.dpi);
700  auto const width = static_cast<LONG>(scale * RectWidth(window_rect));
701  auto const height = static_cast<LONG>(scale * RectHeight(window_rect));
702  window_rect.right = window_rect.left + width;
703  window_rect.bottom = window_rect.top + height;
704  window_rect = AdjustToFit(monitor_info.rcWork, window_rect);
705  }
706 
707  SetWindowPos(window_handle_, nullptr, window_rect.left, window_rect.top,
708  RectWidth(window_rect), RectHeight(window_rect),
709  SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
710  }
711  }
712 
713  if (!task_bar_list_) {
714  HRESULT hr =
715  ::CoCreateInstance(CLSID_TaskbarList, nullptr, CLSCTX_INPROC_SERVER,
716  IID_PPV_ARGS(&task_bar_list_));
717  if (SUCCEEDED(hr) && FAILED(task_bar_list_->HrInit())) {
718  task_bar_list_ = nullptr;
719  }
720  }
721 
722  // As per MSDN marking the window as fullscreen should ensure that the
723  // taskbar is moved to the bottom of the Z-order when the fullscreen window
724  // is activated. If the window is not fullscreen, the Shell falls back to
725  // heuristics to determine how the window should be treated, which means
726  // that it could still consider the window as fullscreen. :(
727  if (task_bar_list_) {
728  task_bar_list_->MarkFullscreenWindow(window_handle_, !!fullscreen);
729  }
730 
731  is_fullscreen_ = fullscreen;
732 }
733 
734 bool HostWindow::GetFullscreen() const {
735  return is_fullscreen_;
736 }
737 
738 ActualWindowSize HostWindow::GetWindowContentSize(HWND hwnd) {
739  RECT rect;
740  GetClientRect(hwnd, &rect);
741  double const dpr = FlutterDesktopGetDpiForHWND(hwnd) /
742  static_cast<double>(USER_DEFAULT_SCREEN_DPI);
743  double const width = rect.right / dpr;
744  double const height = rect.bottom / dpr;
745  return {
746  .width = rect.right / dpr,
747  .height = rect.bottom / dpr,
748  };
749 }
750 
751 std::optional<Size> HostWindow::GetWindowSizeForClientSize(
752  WindowsProcTable const& win32,
753  Size const& client_size,
754  std::optional<Size> smallest,
755  std::optional<Size> biggest,
756  DWORD window_style,
757  DWORD extended_window_style,
758  std::optional<HWND> const& owner_hwnd) {
759  UINT const dpi = GetDpiForHWND(owner_hwnd ? *owner_hwnd : nullptr);
760  double const scale_factor =
761  static_cast<double>(dpi) / USER_DEFAULT_SCREEN_DPI;
762  RECT rect = {
763  .right = static_cast<LONG>(client_size.width() * scale_factor),
764  .bottom = static_cast<LONG>(client_size.height() * scale_factor)};
765 
766  if (!win32.AdjustWindowRectExForDpi(&rect, window_style, FALSE,
767  extended_window_style, dpi)) {
768  FML_LOG(ERROR) << "Failed to run AdjustWindowRectExForDpi: "
769  << GetLastErrorAsString();
770  return std::nullopt;
771  }
772 
773  double width = static_cast<double>(rect.right - rect.left);
774  double height = static_cast<double>(rect.bottom - rect.top);
775 
776  // Apply size constraints.
777  double const non_client_width = width - (client_size.width() * scale_factor);
778  double const non_client_height =
779  height - (client_size.height() * scale_factor);
780  if (smallest) {
781  flutter::Size min_physical_size = ClampToVirtualScreen(
782  flutter::Size(smallest->width() * scale_factor + non_client_width,
783  smallest->height() * scale_factor + non_client_height));
784  width = std::max(width, min_physical_size.width());
785  height = std::max(height, min_physical_size.height());
786  }
787  if (biggest) {
788  flutter::Size max_physical_size = ClampToVirtualScreen(
789  flutter::Size(biggest->width() * scale_factor + non_client_width,
790  biggest->height() * scale_factor + non_client_height));
791  width = std::min(width, max_physical_size.width());
792  height = std::min(height, max_physical_size.height());
793  }
794 
795  return flutter::Size{width, height};
796 }
797 
798 void HostWindow::EnableRecursively(bool enable) {
799  EnableWindow(window_handle_, enable);
800 
801  for (HostWindow* const owned : GetOwnedWindows()) {
802  owned->EnableRecursively(enable);
803  }
804 }
805 
806 HostWindow* HostWindow::FindFirstEnabledDescendant() const {
807  if (IsWindowEnabled(window_handle_)) {
808  return const_cast<HostWindow*>(this);
809  }
810 
811  for (HostWindow* const owned : GetOwnedWindows()) {
812  if (HostWindow* const result = owned->FindFirstEnabledDescendant()) {
813  return result;
814  }
815  }
816 
817  return nullptr;
818 }
819 
820 std::vector<HostWindow*> HostWindow::GetOwnedWindows() const {
821  std::vector<HostWindow*> owned_windows;
822  struct EnumData {
823  HWND owner_window_handle;
824  std::vector<HostWindow*>* owned_windows;
825  } data{window_handle_, &owned_windows};
826 
827  EnumWindows(
828  [](HWND hwnd, LPARAM lparam) -> BOOL {
829  auto* const data = reinterpret_cast<EnumData*>(lparam);
830  if (GetWindow(hwnd, GW_OWNER) == data->owner_window_handle) {
831  HostWindow* const window = GetThisFromHandle(hwnd);
832  if (window && !window->is_being_destroyed_) {
833  data->owned_windows->push_back(window);
834  }
835  }
836  return TRUE;
837  },
838  reinterpret_cast<LPARAM>(&data));
839 
840  return owned_windows;
841 }
842 
843 HostWindow* HostWindow::GetOwnerWindow() const {
844  if (HWND const owner_window_handle = GetWindow(GetWindowHandle(), GW_OWNER)) {
845  return GetThisFromHandle(owner_window_handle);
846  }
847  return nullptr;
848 };
849 
850 void HostWindow::DisableRecursively() {
851  // Disable the window itself.
852  EnableWindow(window_handle_, false);
853 
854  for (HostWindow* const owned : GetOwnedWindows()) {
855  owned->DisableRecursively();
856  }
857 }
858 
859 void HostWindow::UpdateModalStateLayer() {
860  auto children = GetOwnedWindows();
861 
862  if (children.empty()) {
863  // Leaf window in the active path, enable it.
864  EnableWindow(window_handle_, true);
865  } else {
866  // Non-leaf window in the active path, disable it and process children.
867  EnableWindow(window_handle_, false);
868 
869  // On same level of window hierarchy the most recently created window
870  // will remain enabled.
871  auto latest_child = *std::max_element(
872  children.begin(), children.end(), [](HostWindow* a, HostWindow* b) {
873  return a->view_controller_->view()->view_id() <
874  b->view_controller_->view()->view_id();
875  });
876 
877  for (HostWindow* const child : children) {
878  if (child == latest_child) {
879  child->UpdateModalStateLayer();
880  } else {
881  child->DisableRecursively();
882  }
883  }
884  }
885 }
886 
887 } // namespace flutter
std::shared_ptr< WindowsProcTable > windows_proc_table()
std::shared_ptr< DisplayManagerWin32 > display_manager()
std::unique_ptr< FlutterWindowsView > CreateView(std::unique_ptr< WindowBindingHandler > window, bool is_sized_to_content, const BoxConstraints &box_constraints, FlutterWindowsViewSizingDelegate *sizing_delegate=nullptr)
HWND GetWindowHandle() const
Definition: host_window.cc:341
static std::unique_ptr< HostWindow > CreatePopupWindow(WindowManager *window_manager, FlutterWindowsEngine *engine, const WindowConstraints &preferred_constraints, GetWindowPositionCallback get_position_callback, HWND parent)
Definition: host_window.cc:217
static std::unique_ptr< HostWindow > CreateDialogWindow(WindowManager *window_manager, FlutterWindowsEngine *engine, const WindowSizeRequest &preferred_size, const WindowConstraints &preferred_constraints, LPCWSTR title, HWND parent, bool sized_to_content, bool resizable)
Definition: host_window.cc:191
void InitializeFlutterView(HostWindowInitializationParams const &params)
Definition: host_window.cc:232
std::unique_ptr< FlutterWindowsViewController > view_controller_
Definition: host_window.h:259
static LRESULT WndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
Definition: host_window.cc:380
static std::unique_ptr< HostWindow > CreateTooltipWindow(WindowManager *window_manager, FlutterWindowsEngine *engine, const WindowConstraints &preferred_constraints, GetWindowPositionCallback get_position_callback, HWND parent)
Definition: host_window.cc:206
static std::unique_ptr< HostWindow > CreateRegularWindow(WindowManager *window_manager, FlutterWindowsEngine *engine, const WindowSizeRequest &preferred_size, const WindowConstraints &preferred_constraints, LPCWSTR title, bool sized_to_content, bool resizable)
Definition: host_window.cc:177
FlutterWindowsEngine * engine_
Definition: host_window.h:254
HostWindow(WindowManager *window_manager, FlutterWindowsEngine *engine)
Definition: host_window.cc:228
HostWindow * FindFirstEnabledDescendant() const
Definition: host_window.cc:806
virtual BOOL AdjustWindowRectExForDpi(LPRECT lpRect, DWORD dwStyle, BOOL bMenu, DWORD dwExStyle, UINT dpi) const
virtual BOOL EnableNonClientDpiScaling(HWND hwnd) const
UINT FlutterDesktopGetDpiForHWND(HWND hwnd)
#define DWMWA_USE_IMMERSIVE_DARK_MODE
Definition: host_window.cc:83
union flutter::testing::@104::KeyboardChange::@0 content
Win32Message message
WindowRect *(* GetWindowPositionCallback)(const WindowSize &child_size, const WindowRect &parent_rect, const WindowRect &output_rect)
UINT GetDpiForHWND(HWND hwnd)
Definition: dpi_utils.cc:128
LONG RectWidth(const RECT &r)
Definition: rect_helper.h:11
bool AreRectsEqual(const RECT &a, const RECT &b)
Definition: rect_helper.h:19
LONG RectHeight(const RECT &r)
Definition: rect_helper.h:15
FlutterWindowsViewSizingDelegate * sizing_delegate
Definition: host_window.h:159