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