/** ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** * Copyright 2022 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** * * @modified Tom Clay, 2026 - Adapted for ReXGlue runtime */ #include #include #include #include #include #include #include #include #include #include #include #include #include // Must be included before Windows headers for things like NOMINMAX. #include #include #include #include #include #include #include #include namespace { uint32_t ResolveWindowWidth(uint32_t requested_width) { if (REXCVAR_GET(window_width) > 0) { return uint32_t(REXCVAR_GET(window_width)); } if (!rex::cvar::HasNonDefaultValue("window_width")) { if (rex::cvar::HasNonDefaultValue("video_mode_width") && REXCVAR_GET(video_mode_width) > 0) { return uint32_t(std::clamp(REXCVAR_GET(video_mode_width), 1, 8192)); } int32_t preset_width = 0; int32_t preset_height = 0; if (rex::graphics::video_mode_util::TryGetResolutionPresetFromCVar(preset_width, preset_height)) { return uint32_t(std::clamp(preset_width, 1, 8192)); } } return requested_width; } uint32_t ResolveWindowHeight(uint32_t requested_height) { if (REXCVAR_GET(window_height) > 0) { return uint32_t(REXCVAR_GET(window_height)); } if (!rex::cvar::HasNonDefaultValue("window_height")) { if (rex::cvar::HasNonDefaultValue("video_mode_height") && REXCVAR_GET(video_mode_height) > 0) { return uint32_t(std::clamp(REXCVAR_GET(video_mode_height), 1, 8192)); } int32_t preset_width = 0; int32_t preset_height = 0; if (rex::graphics::video_mode_util::TryGetResolutionPresetFromCVar(preset_width, preset_height)) { return uint32_t(std::clamp(preset_height, 1, 8192)); } } return requested_height; } BOOL CALLBACK EnumMonitorsCallback(HMONITOR monitor, HDC, LPRECT, LPARAM data) { auto* monitors = reinterpret_cast*>(data); monitors->push_back(monitor); return TRUE; } HMONITOR GetMonitorByIndex(int32_t index) { if (index <= 0) { return nullptr; } std::vector monitors; EnumDisplayMonitors(nullptr, nullptr, EnumMonitorsCallback, reinterpret_cast(&monitors)); if (index <= static_cast(monitors.size())) { return monitors[index - 1]; } return nullptr; } } // namespace namespace rex { namespace ui { std::unique_ptr Window::Create(WindowedAppContext& app_context, const std::string_view title, uint32_t desired_logical_width, uint32_t desired_logical_height) { desired_logical_width = ResolveWindowWidth(desired_logical_width); desired_logical_height = ResolveWindowHeight(desired_logical_height); return std::make_unique(app_context, title, desired_logical_width, desired_logical_height); } Win32Window::Win32Window(WindowedAppContext& app_context, const std::string_view title, uint32_t desired_logical_width, uint32_t desired_logical_height) : Window(app_context, title, desired_logical_width, desired_logical_height), arrow_cursor_(LoadCursor(nullptr, IDC_ARROW)) { dpi_ = GetCurrentSystemDpi(); } Win32Window::~Win32Window() { EnterDestructor(); if (cursor_auto_hide_timer_) { DeleteTimerQueueTimer(nullptr, cursor_auto_hide_timer_, nullptr); cursor_auto_hide_timer_ = nullptr; } if (hwnd_) { // Set hwnd_ to null to ignore events from now on since this Win32Window is // entering an indeterminate state. HWND hwnd = hwnd_; hwnd_ = nullptr; SetWindowLongPtr(hwnd, GWLP_USERDATA, 0); DestroyWindow(hwnd); } if (icon_) { DestroyIcon(icon_); } } uint32_t Win32Window::GetMediumDpi() const { return USER_DEFAULT_SCREEN_DPI; } bool Win32Window::OpenImpl() { const Win32WindowedAppContext& win32_app_context = static_cast(app_context()); HINSTANCE hinstance = win32_app_context.hinstance(); static bool has_registered_class = false; if (!has_registered_class) { WNDCLASSEXW wcex; wcex.cbSize = sizeof(wcex); wcex.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; wcex.lpfnWndProc = Win32Window::WndProcThunk; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hinstance; wcex.hIcon = LoadIconW(hinstance, L"MAINICON"); wcex.hIconSm = nullptr; // LoadIconW(hinstance, L"MAINICON"); wcex.hCursor = arrow_cursor_; // Matches the black background color of the presenter's painting. wcex.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); wcex.lpszMenuName = nullptr; wcex.lpszClassName = L"RexWindowClass"; if (!RegisterClassExW(&wcex)) { REXLOG_ERROR("RegisterClassEx failed"); return false; } has_registered_class = true; } const Win32MenuItem* main_menu = static_cast(GetMainMenu()); // Setup the initial size for the non-fullscreen window. With per-monitor DPI, // this is also done to be able to obtain the initial window rectangle (with // CW_USEDEFAULT) to get the monitor for the window position, and then to // adjust the normal window size to the new DPI. // Save the initial desired size since it may be modified by the handler of // the WM_SIZE sent during window creation - it's needed for the initial // per-monitor DPI scaling. uint32_t initial_desired_logical_width = GetDesiredLogicalWidth(); uint32_t initial_desired_logical_height = GetDesiredLogicalHeight(); const Win32WindowedAppContext::PerMonitorDpiV2Api* per_monitor_dpi_v2_api = win32_app_context.per_monitor_dpi_v2_api(); const Win32WindowedAppContext::PerMonitorDpiV1Api* per_monitor_dpi_v1_api = win32_app_context.per_monitor_dpi_v1_api(); // Even with per-monitor DPI, take the closest approximation (system DPI) to // potentially more accurately determine the initial monitor. dpi_ = GetCurrentSystemDpi(); DWORD window_style = WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS; DWORD window_ex_style = WS_EX_APPWINDOW | WS_EX_CONTROLPARENT; RECT window_size_rect; window_size_rect.left = 0; window_size_rect.top = 0; window_size_rect.right = LONG(ConvertSizeDpi(initial_desired_logical_width, dpi_, USER_DEFAULT_SCREEN_DPI)); window_size_rect.bottom = LONG(ConvertSizeDpi(initial_desired_logical_height, dpi_, USER_DEFAULT_SCREEN_DPI)); AdjustWindowRectangle(window_size_rect, window_style, BOOL(main_menu != nullptr), window_ex_style, dpi_); // Create the window. Though WM_NCCREATE will assign to `hwnd_` too, still do // the assignment here to handle the case of a failure after WM_NCCREATE, for // instance. auto wide_title = rex::string::to_utf16(GetTitle()); hwnd_ = CreateWindowExW( window_ex_style, L"RexWindowClass", reinterpret_cast(wide_title.c_str()), window_style, CW_USEDEFAULT, CW_USEDEFAULT, window_size_rect.right - window_size_rect.left, window_size_rect.bottom - window_size_rect.top, nullptr, nullptr, hinstance, this); if (!hwnd_) { REXLOG_ERROR("CreateWindowExW failed"); return false; } // For per-monitor DPI, obtain the DPI of the monitor the window was created // on, and adjust the initial normal size for it. If as a result of this // resizing, the window is moved to a different monitor, the WM_DPICHANGED // handler will do the needed correction. uint32_t initial_monitor_dpi = dpi_; if (per_monitor_dpi_v2_api) { initial_monitor_dpi = per_monitor_dpi_v2_api->get_dpi_for_window(hwnd_); } else if (per_monitor_dpi_v1_api) { HMONITOR monitor = MonitorFromWindow(hwnd_, MONITOR_DEFAULTTONEAREST); UINT monitor_dpi_x, monitor_dpi_y; if (monitor && SUCCEEDED(per_monitor_dpi_v1_api->get_dpi_for_monitor( monitor, MDT_DEFAULT, &monitor_dpi_x, &monitor_dpi_y))) { initial_monitor_dpi = monitor_dpi_x; } } if (dpi_ != initial_monitor_dpi) { dpi_ = initial_monitor_dpi; WINDOWPLACEMENT initial_dpi_placement; // Note that WINDOWPLACEMENT contains workspace coordinates, which are // adjusted to exclude toolbars such as the taskbar - the positions and // rectangle origins there can't be mixed with origins of rectangles in // virtual screen coordinates such as those involved in functions like // GetWindowRect. initial_dpi_placement.length = sizeof(initial_dpi_placement); if (GetWindowPlacement(hwnd_, &initial_dpi_placement)) { window_size_rect.left = 0; window_size_rect.top = 0; window_size_rect.right = LONG(ConvertSizeDpi(initial_desired_logical_width, dpi_, USER_DEFAULT_SCREEN_DPI)); window_size_rect.bottom = LONG(ConvertSizeDpi(initial_desired_logical_height, dpi_, USER_DEFAULT_SCREEN_DPI)); AdjustWindowRectangle(window_size_rect, window_style, BOOL(main_menu != nullptr), window_ex_style, dpi_); initial_dpi_placement.rcNormalPosition.right = initial_dpi_placement.rcNormalPosition.left + (window_size_rect.right - window_size_rect.left); initial_dpi_placement.rcNormalPosition.bottom = initial_dpi_placement.rcNormalPosition.top + (window_size_rect.bottom - window_size_rect.top); SetWindowPlacement(hwnd_, &initial_dpi_placement); } } // Disable rounded corners starting with Windows 11 (or silently receive and // ignore E_INVALIDARG on Windows versions before 10.0.22000.0), primarily to // preserve all pixels of the guest output. DWM_WINDOW_CORNER_PREFERENCE window_corner_preference = DWMWCP_DONOTROUND; DwmSetWindowAttribute(hwnd_, DWMWA_WINDOW_CORNER_PREFERENCE, &window_corner_preference, sizeof(window_corner_preference)); // Disable flicks. ATOM atom = GlobalAddAtomW(L"MicrosoftTabletPenServiceProperty"); const DWORD_PTR dwHwndTabletProperty = TABLET_DISABLE_PRESSANDHOLD | // disables press and hold (right-click) // gesture TABLET_DISABLE_PENTAPFEEDBACK | // disables UI feedback on pen up (waves) TABLET_DISABLE_PENBARRELFEEDBACK | // disables UI feedback on pen button // down (circle) TABLET_DISABLE_FLICKS | // disables pen flicks (back, forward, drag down, // drag up) TABLET_DISABLE_TOUCHSWITCH | TABLET_DISABLE_SMOOTHSCROLLING | TABLET_DISABLE_TOUCHUIFORCEON | TABLET_ENABLE_MULTITOUCHDATA; SetPropW(hwnd_, L"MicrosoftTabletPenServiceProperty", reinterpret_cast(dwHwndTabletProperty)); GlobalDeleteAtom(atom); // Enable file dragging from external sources DragAcceptFiles(hwnd_, true); // Apply the initial state from the Window that the window shouldn't be // visibly transitioned to. if (icon_) { SendMessageW(hwnd_, WM_SETICON, ICON_BIG, reinterpret_cast(icon_)); SendMessageW(hwnd_, WM_SETICON, ICON_SMALL, reinterpret_cast(icon_)); } // Move the window to the requested monitor before entering fullscreen so // that MonitorFromWindow picks the correct display. if (int32_t monitor_index = REXCVAR_GET(monitor); monitor_index > 0) { HMONITOR target = GetMonitorByIndex(monitor_index); if (target) { MONITORINFO mi; mi.cbSize = sizeof(mi); if (GetMonitorInfo(target, &mi)) { RECT wr; GetWindowRect(hwnd_, &wr); int w = wr.right - wr.left; int h = wr.bottom - wr.top; int x = mi.rcWork.left + (mi.rcWork.right - mi.rcWork.left - w) / 2; int y = mi.rcWork.top + (mi.rcWork.bottom - mi.rcWork.top - h) / 2; SetWindowPos(hwnd_, nullptr, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); } } } if (IsFullscreen()) { // Go fullscreen after setting up everything related to the placement of the // non-fullscreen window. WindowDestructionReceiver destruction_receiver(this); ApplyFullscreenEntry(destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { return true; } } else { if (main_menu) { SetMenu(hwnd_, main_menu->handle()); } } // Finally show the window. ShowWindow(hwnd_, SW_SHOWNORMAL); // Report the initial actual state after opening, messages for which might // have missed if they were processed during CreateWindowExW when the HWND was // not yet attached to the Win32Window. { WindowDestructionReceiver destruction_receiver(this); // Report the desired logical size of the client area in the non-maximized // state after the initial layout setup in Windows. WINDOWPLACEMENT shown_placement; shown_placement.length = sizeof(shown_placement); if (GetWindowPlacement(hwnd_, &shown_placement)) { // Get the size of the non-client area to subtract it from the size of the // entire window in its non-maximized state, to get the client area. For // safety, in case the window is somehow smaller than its non-client area // (AdjustWindowRect is not exact in various cases also, such as when the // menu becomes multiline), clamp to 0. RECT non_client_area_rect = {}; AdjustWindowRectangle(non_client_area_rect); OnDesiredLogicalSizeUpdate( SizeToLogical(uint32_t(std::max( (shown_placement.rcNormalPosition.right - shown_placement.rcNormalPosition.left) - (non_client_area_rect.right - non_client_area_rect.left), LONG(0)))), SizeToLogical(uint32_t(std::max( (shown_placement.rcNormalPosition.bottom - shown_placement.rcNormalPosition.top) - (non_client_area_rect.bottom - non_client_area_rect.top), LONG(0))))); } // Report the actual physical size in the current state. // GetClientRect returns a rectangle with 0 origin. RECT shown_client_rect; if (GetClientRect(hwnd_, &shown_client_rect)) { OnActualSizeUpdate(uint32_t(shown_client_rect.right), uint32_t(shown_client_rect.bottom), destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) { return true; } } OnFocusUpdate(GetFocus() == hwnd_, destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) { return true; } } // Apply the initial state from the Window that involves interaction with the // user. if (IsMouseCaptureRequested()) { SetCapture(hwnd_); } cursor_currently_auto_hidden_ = false; CursorVisibility cursor_visibility = GetCursorVisibility(); if (cursor_visibility != CursorVisibility::kVisible) { if (cursor_visibility == CursorVisibility::kAutoHidden) { if (!GetCursorPos(&cursor_auto_hide_last_screen_pos_)) { cursor_auto_hide_last_screen_pos_.x = LONG_MAX; cursor_auto_hide_last_screen_pos_.y = LONG_MAX; } cursor_currently_auto_hidden_ = true; } // OnFocusUpdate needs to be done before this. SetCursorIfFocusedOnClientArea(nullptr); } return true; } void Win32Window::RequestCloseImpl() { // Note that CloseWindow doesn't close the window, rather, it only minimizes // it - need to send WM_CLOSE to let the Win32Window WndProc perform all the // shutdown. SendMessageW(hwnd_, WM_CLOSE, 0, 0); // The window might have been deleted by the close handler, don't do anything // with *this anymore (if that's needed, use a WindowDestructionReceiver). } uint32_t Win32Window::GetLatestDpiImpl() const { // hwnd_ may be null in this function, but the latest DPI is stored in a // variable anyway. return dpi_; } float Win32Window::QueryDisplayRefreshHzImpl() const { if (!hwnd_) { return 0.0f; } HMONITOR monitor = MonitorFromWindow(hwnd_, MONITOR_DEFAULTTONEAREST); if (!monitor) { return 0.0f; } MONITORINFOEXW mi = {}; mi.cbSize = sizeof(mi); if (!GetMonitorInfoW(monitor, &mi)) { return 0.0f; } DEVMODEW dm = {}; dm.dmSize = sizeof(dm); if (!EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm) || !(dm.dmFields & DM_DISPLAYFREQUENCY) || dm.dmDisplayFrequency <= 1) { // 0 and 1 are the documented "hardware default" sentinels. return 0.0f; } return float(dm.dmDisplayFrequency); } void Win32Window::ApplyNewFullscreen() { // Various functions here may send messages that may result in the // listeners being invoked, and potentially cause the destruction of the // window or fullscreen being toggled from inside this function. WindowDestructionReceiver destruction_receiver(this); if (IsFullscreen()) { ApplyFullscreenEntry(destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) { return; } } else { // Changing the style and the menu may change the size too, don't handle // the resize multiple times (also potentially with the listeners changing // the desired fullscreen if called from the handling of some message like // WM_SIZE). BeginBatchedSizeUpdate(); // Reinstate the non-client area. SetWindowLong(hwnd_, GWL_STYLE, GetWindowLong(hwnd_, GWL_STYLE) | WS_OVERLAPPEDWINDOW); if (destruction_receiver.IsWindowDestroyedOrClosed()) { if (!destruction_receiver.IsWindowDestroyed()) { EndBatchedSizeUpdate(destruction_receiver); } return; } const Win32MenuItem* main_menu = static_cast(GetMainMenu()); if (main_menu) { SetMenu(hwnd_, main_menu->handle()); if (destruction_receiver.IsWindowDestroyedOrClosed()) { if (!destruction_receiver.IsWindowDestroyed()) { EndBatchedSizeUpdate(destruction_receiver); } return; } } // For some reason, WM_DPICHANGED is not sent when the window is borderless // fullscreen with per-monitor DPI awareness v1 (on Windows versions since // Windows 8.1 before Windows 10 1703) - refresh the current DPI explicitly. dpi_ = GetCurrentDpi(); if (dpi_ != pre_fullscreen_dpi_) { // Rescale the pre-fullscreen non-maximized window size to the new DPI as // WM_DPICHANGED with the new rectangle was received for the fullscreen // window size, not the windowed one. Simulating the behavior of the // automatic resizing when changing the scale in the Windows settings (as // of Windows 11 21H2 at least), which keeps the physical top-left origin // of the entire window including the non-client area, but rescales the // size. // Note that WINDOWPLACEMENT contains workspace coordinates, which are // adjusted to exclude toolbars such as the taskbar - the positions and // rectangle origins there can't be mixed with origins of rectangles in // virtual screen coordinates such as those involved in functions like // GetWindowRect. RECT new_dpi_rect; new_dpi_rect.left = 0; new_dpi_rect.top = 0; new_dpi_rect.right = LONG(ConvertSizeDpi(pre_fullscreen_normal_client_width_, dpi_, pre_fullscreen_dpi_)); new_dpi_rect.bottom = LONG(ConvertSizeDpi(pre_fullscreen_normal_client_height_, dpi_, pre_fullscreen_dpi_)); AdjustWindowRectangle(new_dpi_rect); pre_fullscreen_placement_.rcNormalPosition.right = pre_fullscreen_placement_.rcNormalPosition.left + (new_dpi_rect.right - new_dpi_rect.left); pre_fullscreen_placement_.rcNormalPosition.bottom = pre_fullscreen_placement_.rcNormalPosition.top + (new_dpi_rect.bottom - new_dpi_rect.top); } SetWindowPlacement(hwnd_, &pre_fullscreen_placement_); if (destruction_receiver.IsWindowDestroyedOrClosed()) { if (!destruction_receiver.IsWindowDestroyed()) { EndBatchedSizeUpdate(destruction_receiver); } return; } // https://devblogs.microsoft.com/oldnewthing/20131017-00/?p=2903 SetWindowPos(hwnd_, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_FRAMECHANGED); if (destruction_receiver.IsWindowDestroyedOrClosed()) { if (!destruction_receiver.IsWindowDestroyed()) { EndBatchedSizeUpdate(destruction_receiver); } return; } EndBatchedSizeUpdate(destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) { return; } } } void Win32Window::ApplyNewTitle() { auto wide_title = rex::string::to_utf16(GetTitle()); SetWindowTextW(hwnd_, reinterpret_cast(wide_title.c_str())); } void Win32Window::LoadAndApplyIcon(const void* buffer, size_t size, bool can_apply_state_in_current_phase) { bool reset = !buffer || !size; HICON new_icon, new_icon_small; if (reset) { if (!icon_) { // The icon is already the default one. return; } if (!hwnd_) { // Don't need to get the actual icon from the class if there's nothing to // set it for yet (and there's no HWND to get it from, and the class may // have not been registered yet also). DestroyIcon(icon_); icon_ = nullptr; return; } new_icon = reinterpret_cast(GetClassLongPtrW(hwnd_, GCLP_HICON)); new_icon_small = reinterpret_cast(GetClassLongPtrW(hwnd_, GCLP_HICONSM)); // Not caring if it's null in the class, accepting anything the class // specifies. } else { new_icon = CreateIconFromResourceEx(static_cast(const_cast(buffer)), DWORD(size), TRUE, 0x00030000, 0, 0, LR_DEFAULTCOLOR | LR_DEFAULTSIZE); if (!new_icon) { return; } new_icon_small = new_icon; } if (hwnd_) { SendMessageW(hwnd_, WM_SETICON, ICON_BIG, reinterpret_cast(new_icon)); SendMessageW(hwnd_, WM_SETICON, ICON_SMALL, reinterpret_cast(new_icon_small)); } // The old icon is not in use anymore, safe to destroy it now. if (icon_) { DestroyIcon(icon_); icon_ = nullptr; } if (!reset) { assert_true(new_icon_small == new_icon); icon_ = new_icon; } } void Win32Window::ApplyNewMainMenu(MenuItem* old_main_menu) { if (IsFullscreen()) { // The menu will be set when exiting fullscreen. return; } const Win32MenuItem* main_menu = static_cast(GetMainMenu()); WindowDestructionReceiver destruction_receiver(this); SetMenu(hwnd_, main_menu ? main_menu->handle() : nullptr); if (destruction_receiver.IsWindowDestroyedOrClosed()) { return; } } void Win32Window::CompleteMainMenuItemsUpdateImpl() { if (IsFullscreen()) { return; } DrawMenuBar(hwnd_); } void Win32Window::ApplyNewMouseCapture() { WindowDestructionReceiver destruction_receiver(this); SetCapture(hwnd_); if (destruction_receiver.IsWindowDestroyedOrClosed()) { return; } } void Win32Window::ApplyNewMouseRelease() { if (GetCapture() != hwnd_) { return; } WindowDestructionReceiver destruction_receiver(this); ReleaseCapture(); if (destruction_receiver.IsWindowDestroyedOrClosed()) { return; } } void Win32Window::ApplyNewCursorVisibility(CursorVisibility old_cursor_visibility) { CursorVisibility new_cursor_visibility = GetCursorVisibility(); cursor_currently_auto_hidden_ = false; if (new_cursor_visibility == CursorVisibility::kAutoHidden) { if (!GetCursorPos(&cursor_auto_hide_last_screen_pos_)) { cursor_auto_hide_last_screen_pos_.x = LONG_MAX; cursor_auto_hide_last_screen_pos_.y = LONG_MAX; } cursor_currently_auto_hidden_ = true; } else if (old_cursor_visibility == CursorVisibility::kAutoHidden) { if (cursor_auto_hide_timer_) { DeleteTimerQueueTimer(nullptr, cursor_auto_hide_timer_, nullptr); cursor_auto_hide_timer_ = nullptr; } } SetCursorIfFocusedOnClientArea(new_cursor_visibility == CursorVisibility::kVisible ? arrow_cursor_ : nullptr); } void Win32Window::FocusImpl() { SetFocus(hwnd_); } std::unique_ptr Win32Window::CreateSurfaceImpl(Surface::TypeFlags allowed_types) { HINSTANCE hInstance = static_cast(app_context()).hinstance(); if (allowed_types & Surface::kTypeFlag_Win32Hwnd) { return std::make_unique(hInstance, hwnd_); } return nullptr; } void Win32Window::RequestPaintImpl() { InvalidateRect(hwnd_, nullptr, FALSE); } BOOL Win32Window::AdjustWindowRectangle(RECT& rect, DWORD style, BOOL menu, DWORD ex_style, UINT dpi) const { const Win32WindowedAppContext& win32_app_context = static_cast(app_context()); const Win32WindowedAppContext::PerMonitorDpiV2Api* per_monitor_dpi_v2_api = win32_app_context.per_monitor_dpi_v2_api(); if (per_monitor_dpi_v2_api) { return per_monitor_dpi_v2_api->adjust_window_rect_ex_for_dpi(&rect, style, menu, ex_style, dpi); } // Before per-monitor DPI v2, there was no rescaling of the non-client // area at runtime at all, so throughout the execution of the process it will // behave the same regardless of the DPI. return AdjustWindowRectEx(&rect, style, menu, ex_style); } BOOL Win32Window::AdjustWindowRectangle(RECT& rect) const { if (!hwnd_) { return FALSE; } return AdjustWindowRectangle(rect, GetWindowLong(hwnd_, GWL_STYLE), BOOL(GetMainMenu() != nullptr), GetWindowLong(hwnd_, GWL_EXSTYLE), dpi_); } uint32_t Win32Window::GetCurrentSystemDpi() const { const Win32WindowedAppContext& win32_app_context = static_cast(app_context()); const Win32WindowedAppContext::PerMonitorDpiV2Api* per_monitor_dpi_v2_api = win32_app_context.per_monitor_dpi_v2_api(); if (per_monitor_dpi_v2_api) { return per_monitor_dpi_v2_api->get_dpi_for_system(); } HDC screen_hdc = GetDC(nullptr); if (!screen_hdc) { return USER_DEFAULT_SCREEN_DPI; } // According to MSDN, x and y are identical. int logical_pixels_x = GetDeviceCaps(screen_hdc, LOGPIXELSX); ReleaseDC(nullptr, screen_hdc); return uint32_t(logical_pixels_x); } uint32_t Win32Window::GetCurrentDpi() const { if (hwnd_) { const Win32WindowedAppContext& win32_app_context = static_cast(app_context()); const Win32WindowedAppContext::PerMonitorDpiV2Api* per_monitor_dpi_v2_api = win32_app_context.per_monitor_dpi_v2_api(); if (per_monitor_dpi_v2_api) { return per_monitor_dpi_v2_api->get_dpi_for_window(hwnd_); } const Win32WindowedAppContext::PerMonitorDpiV1Api* per_monitor_dpi_v1_api = win32_app_context.per_monitor_dpi_v1_api(); if (per_monitor_dpi_v1_api) { HMONITOR monitor = MonitorFromWindow(hwnd_, MONITOR_DEFAULTTONEAREST); UINT monitor_dpi_x, monitor_dpi_y; if (monitor && SUCCEEDED(per_monitor_dpi_v1_api->get_dpi_for_monitor( monitor, MDT_DEFAULT, &monitor_dpi_x, &monitor_dpi_y))) { // According to MSDN, x and y are identical. return monitor_dpi_x; } } } return GetCurrentSystemDpi(); } void Win32Window::ApplyFullscreenEntry(WindowDestructionReceiver& destruction_receiver) { if (!IsFullscreen()) { return; } // https://blogs.msdn.com/b/oldnewthing/archive/2010/04/12/9994016.aspx // No reason to use MONITOR_DEFAULTTOPRIMARY instead of // MONITOR_DEFAULTTONEAREST, however. pre_fullscreen_dpi_ = dpi_; pre_fullscreen_placement_.length = sizeof(pre_fullscreen_placement_); HMONITOR monitor; MONITORINFO monitor_info; monitor_info.cbSize = sizeof(monitor_info); if (!GetWindowPlacement(hwnd_, &pre_fullscreen_placement_) || !(monitor = MonitorFromWindow(hwnd_, MONITOR_DEFAULTTONEAREST)) || !GetMonitorInfo(monitor, &monitor_info)) { OnDesiredFullscreenUpdate(false); return; } // Preserve values for DPI rescaling of the window in the non-maximized state // if DPI is changed mid-fullscreen. // Get the size of the non-client area to subtract it from the size of the // entire window in its non-maximized state, to get the client area. For // safety, in case the window is somehow smaller than its non-client area // (AdjustWindowRect is not exact in various cases also, such as when the menu // becomes multiline), clamp to 0. RECT non_client_area_rect = {}; AdjustWindowRectangle(non_client_area_rect); pre_fullscreen_normal_client_width_ = uint32_t(std::max((pre_fullscreen_placement_.rcNormalPosition.right - pre_fullscreen_placement_.rcNormalPosition.left) - (non_client_area_rect.right - non_client_area_rect.left), LONG(0))); pre_fullscreen_normal_client_height_ = uint32_t(std::max((pre_fullscreen_placement_.rcNormalPosition.bottom - pre_fullscreen_placement_.rcNormalPosition.top) - (non_client_area_rect.bottom - non_client_area_rect.top), LONG(0))); // Changing the style and the menu may change the size too, don't handle the // resize multiple times (also potentially with the listeners changing the // desired fullscreen if called from the handling of some message like // WM_SIZE). BeginBatchedSizeUpdate(); // Remove the non-client area. if (GetMainMenu()) { SetMenu(hwnd_, nullptr); if (destruction_receiver.IsWindowDestroyedOrClosed()) { if (!destruction_receiver.IsWindowDestroyed()) { EndBatchedSizeUpdate(destruction_receiver); } return; } } SetWindowLong(hwnd_, GWL_STYLE, GetWindowLong(hwnd_, GWL_STYLE) & ~DWORD(WS_OVERLAPPEDWINDOW)); if (destruction_receiver.IsWindowDestroyedOrClosed()) { if (!destruction_receiver.IsWindowDestroyed()) { EndBatchedSizeUpdate(destruction_receiver); } return; } // Resize the window to fullscreen. It is important that this is done _after_ // disabling the decorations and the menu, to make sure that composition will // not have to be done for the new size of the window at all, so independent, // low-latency presentation is possible immediately - if the window was // involved in composition, it may stay composed persistently until some other // state change that sometimes helps, sometimes doesn't, even if it becomes // borderless fullscreen again - this occurs sometimes at least on Windows 11 // 21H2 on Nvidia GeForce GTX 1070 on driver version 472.12. SetWindowPos(hwnd_, HWND_TOP, monitor_info.rcMonitor.left, monitor_info.rcMonitor.top, monitor_info.rcMonitor.right - monitor_info.rcMonitor.left, monitor_info.rcMonitor.bottom - monitor_info.rcMonitor.top, SWP_NOOWNERZORDER | SWP_FRAMECHANGED); if (destruction_receiver.IsWindowDestroyedOrClosed()) { if (!destruction_receiver.IsWindowDestroyed()) { EndBatchedSizeUpdate(destruction_receiver); } return; } EndBatchedSizeUpdate(destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) { return; } } void Win32Window::HandleSizeUpdate(WindowDestructionReceiver& destruction_receiver) { if (!hwnd_) { // Batched size update ended when the window has already been closed, for // instance. return; } { MonitorUpdateEvent e(this, false); OnMonitorUpdate(e); } // For the desired size in the normal, not maximized and not fullscreen state. if (!IsFullscreen()) { WINDOWPLACEMENT window_placement; window_placement.length = sizeof(window_placement); if (GetWindowPlacement(hwnd_, &window_placement)) { // window_placement.rcNormalPosition is the entire window's rectangle, not // only the client area - convert to client. // https://devblogs.microsoft.com/oldnewthing/20131017-00/?p=2903 // For safety, in case the window is somehow smaller than its non-client // area (AdjustWindowRect is not exact in various cases also, such as when // the menu becomes multiline), clamp to 0. RECT non_client_area_rect = {}; if (AdjustWindowRectangle(non_client_area_rect)) { OnDesiredLogicalSizeUpdate( SizeToLogical(uint32_t(std::max( (window_placement.rcNormalPosition.right - window_placement.rcNormalPosition.left) - (non_client_area_rect.right - non_client_area_rect.left), LONG(0)))), SizeToLogical(uint32_t(std::max( (window_placement.rcNormalPosition.bottom - window_placement.rcNormalPosition.top) - (non_client_area_rect.bottom - non_client_area_rect.top), LONG(0))))); } } } // For the actual state. // GetClientRect returns a rectangle with 0 origin. RECT client_rect; if (GetClientRect(hwnd_, &client_rect)) { OnActualSizeUpdate(uint32_t(client_rect.right), uint32_t(client_rect.bottom), destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) { return; } } } void Win32Window::BeginBatchedSizeUpdate() { // It's okay if batched_size_update_contained_* are not false when beginning // a batched update, in case the new batched update was started by a window // listener called from within EndBatchedSizeUpdate. ++batched_size_update_depth_; } void Win32Window::EndBatchedSizeUpdate(WindowDestructionReceiver& destruction_receiver) { assert_not_zero(batched_size_update_depth_); if (--batched_size_update_depth_) { return; } // Resetting batched_size_update_contained_* in closing, not opening, because // a listener may start a new batch, and finish it, and there won't be need to // handle the deferred messages twice. if (batched_size_update_contained_wm_size_) { batched_size_update_contained_wm_size_ = false; HandleSizeUpdate(destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { return; } } if (batched_size_update_contained_wm_paint_) { batched_size_update_contained_wm_paint_ = false; RequestPaint(); } } bool Win32Window::HandleMouse(UINT message, WPARAM wParam, LPARAM lParam, WindowDestructionReceiver& destruction_receiver) { // Mouse messages usually contain the position in the client area in lParam, // but WM_MOUSEWHEEL is an exception, it passes the screen position. int32_t message_x = GET_X_LPARAM(lParam); int32_t message_y = GET_Y_LPARAM(lParam); bool message_pos_is_screen = message == WM_MOUSEWHEEL; POINT client_pos = {message_x, message_y}; if (message_pos_is_screen) { ScreenToClient(hwnd_, &client_pos); } if (GetCursorVisibility() == CursorVisibility::kAutoHidden) { POINT screen_pos = {message_x, message_y}; if (message_pos_is_screen || ClientToScreen(hwnd_, &screen_pos)) { if (screen_pos.x != cursor_auto_hide_last_screen_pos_.x || screen_pos.y != cursor_auto_hide_last_screen_pos_.y) { // WM_MOUSEMOVE messages followed by WM_SETCURSOR may be sent for // reasons not always involving actual mouse movement performed by the // user. They're sent when the position of the cursor relative to the // client area has been changed, as well as other events related to // window management (including when creating the window), even when not // interacting with the OS. These should not be revealing the cursor. // Only revealing it if the mouse has actually been moved. cursor_currently_auto_hidden_ = false; SetCursorAutoHideTimer(); // There's no need to SetCursor here, mouse messages relevant to the // cursor within the window are always followed by WM_SETCURSOR. cursor_auto_hide_last_screen_pos_ = screen_pos; } } } MouseEvent::Button button = MouseEvent::Button::kNone; int32_t scroll_y = 0; switch (message) { case WM_LBUTTONDOWN: case WM_LBUTTONUP: button = MouseEvent::Button::kLeft; break; case WM_RBUTTONDOWN: case WM_RBUTTONUP: button = MouseEvent::Button::kRight; break; case WM_MBUTTONDOWN: case WM_MBUTTONUP: button = MouseEvent::Button::kMiddle; break; case WM_XBUTTONDOWN: case WM_XBUTTONUP: switch (GET_XBUTTON_WPARAM(wParam)) { case XBUTTON1: button = MouseEvent::Button::kX1; break; case XBUTTON2: button = MouseEvent::Button::kX2; break; default: // Still handle the movement. break; } break; case WM_MOUSEMOVE: button = MouseEvent::Button::kNone; break; case WM_MOUSEWHEEL: button = MouseEvent::Button::kNone; static_assert(MouseEvent::kScrollPerDetent == WHEEL_DELTA, "Assuming the Windows scroll amount can be passed directly to " "MouseEvent"); scroll_y = GET_WHEEL_DELTA_WPARAM(wParam); break; default: return false; } MouseEvent e(this, button, client_pos.x, client_pos.y, 0, scroll_y); switch (message) { case WM_LBUTTONDOWN: case WM_RBUTTONDOWN: case WM_MBUTTONDOWN: case WM_XBUTTONDOWN: OnMouseDown(e, destruction_receiver); break; case WM_LBUTTONUP: case WM_RBUTTONUP: case WM_MBUTTONUP: case WM_XBUTTONUP: OnMouseUp(e, destruction_receiver); break; case WM_MOUSEMOVE: OnMouseMove(e, destruction_receiver); break; case WM_MOUSEWHEEL: OnMouseWheel(e, destruction_receiver); break; default: break; } // Returning immediately anyway - no need to check // destruction_receiver.IsWindowDestroyed(). return e.is_handled(); } bool Win32Window::HandleKeyboard(UINT message, WPARAM wParam, LPARAM lParam, WindowDestructionReceiver& destruction_receiver) { KeyEvent e(this, VirtualKey(wParam), lParam & 0xFFFF, !!(lParam & (LPARAM(1) << 30)), !!(GetKeyState(VK_SHIFT) & 0x80), !!(GetKeyState(VK_CONTROL) & 0x80), !!(GetKeyState(VK_MENU) & 0x80), !!(GetKeyState(VK_LWIN) & 0x80)); switch (message) { case WM_KEYDOWN: case WM_SYSKEYDOWN: OnKeyDown(e, destruction_receiver); break; case WM_KEYUP: case WM_SYSKEYUP: OnKeyUp(e, destruction_receiver); break; case WM_CHAR: case WM_SYSCHAR: OnKeyChar(e, destruction_receiver); break; default: break; } // Returning immediately anyway - no need to check // destruction_receiver.IsWindowDestroyed(). return e.is_handled(); } void Win32Window::SetCursorIfFocusedOnClientArea(HCURSOR cursor) const { if (!HasFocus()) { return; } POINT cursor_pos; if (!GetCursorPos(&cursor_pos)) { return; } if (WindowFromPoint(cursor_pos) == hwnd_ && SendMessage(hwnd_, WM_NCHITTEST, 0, MAKELONG(cursor_pos.x, cursor_pos.y)) == HTCLIENT) { SetCursor(cursor); } } void Win32Window::SetCursorAutoHideTimer() { // Reset the timer by deleting the old timer and creating the new one. // ChangeTimerQueueTimer doesn't work if the timer has already expired. if (cursor_auto_hide_timer_) { DeleteTimerQueueTimer(nullptr, cursor_auto_hide_timer_, nullptr); cursor_auto_hide_timer_ = nullptr; } // After making sure that the callback is not callable anymore // (DeleteTimerQueueTimer waits for the completion of the callback if it has // been called already, or cancels it if it's hasn't), update the most recent // message revision. last_cursor_auto_hide_queued = last_cursor_auto_hide_signaled + 1; CreateTimerQueueTimer(&cursor_auto_hide_timer_, nullptr, AutoHideCursorTimerCallback, this, kDefaultCursorAutoHideMilliseconds, 0, WT_EXECUTEINTIMERTHREAD | WT_EXECUTEONLYONCE); } void Win32Window::AutoHideCursorTimerCallback(void* parameter, BOOLEAN timer_or_wait_fired) { if (!timer_or_wait_fired) { // Not a timer callback. return; } Win32Window& window = *static_cast(parameter); window.last_cursor_auto_hide_signaled = window.last_cursor_auto_hide_queued; SendMessage(window.hwnd_, kUserMessageAutoHideCursor, window.last_cursor_auto_hide_signaled, 0); } LRESULT Win32Window::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { if (message >= WM_MOUSEFIRST && message <= WM_MOUSELAST) { WindowDestructionReceiver destruction_receiver(this); // Returning immediately anyway - no need to check // destruction_receiver.IsWindowDestroyed() afterwards. return HandleMouse(message, wParam, lParam, destruction_receiver) ? 0 : DefWindowProcW(hWnd, message, wParam, lParam); } if (message >= WM_KEYFIRST && message <= WM_KEYLAST) { WindowDestructionReceiver destruction_receiver(this); // Returning immediately anyway - no need to check // destruction_receiver.IsWindowDestroyed() afterwards. return HandleKeyboard(message, wParam, lParam, destruction_receiver) ? 0 : DefWindowProcW(hWnd, message, wParam, lParam); } switch (message) { case WM_CLOSE: // In case the Windows window was somehow forcibly destroyed without // WM_CLOSE. case WM_DESTROY: { if (cursor_auto_hide_timer_) { DeleteTimerQueueTimer(nullptr, cursor_auto_hide_timer_, nullptr); cursor_auto_hide_timer_ = nullptr; } { WindowDestructionReceiver destruction_receiver(this); OnBeforeClose(destruction_receiver); if (destruction_receiver.IsWindowDestroyed()) { break; } } // Set hwnd_ to null to ignore events from now on since this Win32Window // is entering an indeterminate state - this should be done at some point // in closing anyway. hwnd_ = nullptr; SetWindowLongPtr(hWnd, GWLP_USERDATA, 0); if (message != WM_DESTROY) { DestroyWindow(hWnd); } OnAfterClose(); } break; case WM_DROPFILES: { HDROP drop_handle = reinterpret_cast(wParam); auto drop_count = DragQueryFileW(drop_handle, 0xFFFFFFFFu, nullptr, 0); if (drop_count > 0) { // Get required buffer size UINT path_size = DragQueryFileW(drop_handle, 0, nullptr, 0); if (path_size > 0 && path_size < 0xFFFFFFFFu) { std::u16string path; ++path_size; // Ensure space for the null terminator path.resize(path_size); // Reserve space // Only getting first file dropped (other files ignored) path_size = DragQueryFileW(drop_handle, 0, (LPWSTR)&path[0], path_size); if (path_size > 0) { path.resize(path_size); // Will drop the null terminator FileDropEvent e(this, rex::to_path(path)); WindowDestructionReceiver destruction_receiver(this); OnFileDrop(e, destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) { DragFinish(drop_handle); break; } } } } DragFinish(drop_handle); } break; case WM_MOVE: { MonitorUpdateEvent e(this, false); OnMonitorUpdate(e); } break; case WM_SIZE: { if (batched_size_update_depth_) { batched_size_update_contained_wm_size_ = true; } else { WindowDestructionReceiver destruction_receiver(this); HandleSizeUpdate(destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) { break; } } } break; case WM_PAINT: { if (batched_size_update_depth_) { // Avoid painting an outdated surface during a batched size update when // WM_SIZE handling is deferred. batched_size_update_contained_wm_paint_ = true; } else { ValidateRect(hwnd_, nullptr); OnPaint(); } // Custom painting via OnPaint - don't pass to DefWindowProc. return 0; } break; case WM_ERASEBKGND: { if (HasSurface()) { // Don't erase between paints because painting may be dropped if nothing // has changed since the last one. return 0; } } break; case WM_DISPLAYCHANGE: { MonitorUpdateEvent e(this, true); OnMonitorUpdate(e); } break; case WM_DPICHANGED: { // Note that for some reason, WM_DPICHANGED is not sent when the window is // borderless fullscreen with per-monitor DPI awareness v1. dpi_ = GetCurrentDpi(); WindowDestructionReceiver destruction_receiver(this); { UISetupEvent e(this); OnDpiChanged(e, destruction_receiver); // The window might have been closed by the handler, check hwnd_ too // since it's needed below. if (destruction_receiver.IsWindowDestroyedOrClosed()) { break; } } auto rect = reinterpret_cast(lParam); if (rect) { // SetWindowPos arguments according to WM_DPICHANGED MSDN documentation. // https://docs.microsoft.com/en-us/windows/win32/hidpi/wm-dpichanged // There's no need to handle the maximized state any special way (by // updating the window placement instead of the window position in this // case, for instance), as Windows (by design) restores the window when // changing the DPI to a new one. SetWindowPos(hwnd_, nullptr, int(rect->left), int(rect->top), int(rect->right - rect->left), int(rect->bottom - rect->top), SWP_NOZORDER | SWP_NOACTIVATE); if (destruction_receiver.IsWindowDestroyedOrClosed()) { break; } } } break; case WM_KILLFOCUS: { WindowDestructionReceiver destruction_receiver(this); OnFocusUpdate(false, destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) { break; } } break; case WM_SETFOCUS: { WindowDestructionReceiver destruction_receiver(this); OnFocusUpdate(true, destruction_receiver); if (destruction_receiver.IsWindowDestroyedOrClosed()) { break; } } break; case WM_SETCURSOR: { if (reinterpret_cast(wParam) == hwnd_ && HasFocus() && LOWORD(lParam) == HTCLIENT) { switch (GetCursorVisibility()) { case CursorVisibility::kAutoHidden: { // Always revealing the cursor in case of events like clicking, but // WM_MOUSEMOVE messages may be sent for reasons not always // involving actual mouse movement performed by the user. Revealing // the cursor in case of movement is done in HandleMouse instead. if (HIWORD(lParam) != WM_MOUSEMOVE) { cursor_currently_auto_hidden_ = false; SetCursorAutoHideTimer(); } if (cursor_currently_auto_hidden_) { SetCursor(nullptr); return TRUE; } } break; case CursorVisibility::kHidden: SetCursor(nullptr); return TRUE; default: break; } } // For the non-client area, and for visible cursor, letting normal // processing happen, setting the cursor to an arrow or to something // specific to non-client parts of the window. } break; case kUserMessageAutoHideCursor: { // Recheck the cursor visibility - the callback might have been called // before or while the timer is deleted. Also ignore messages from // outdated mouse interactions. if (GetCursorVisibility() == CursorVisibility::kAutoHidden && wParam == last_cursor_auto_hide_queued) { // The timer object is not needed anymore. if (cursor_auto_hide_timer_) { DeleteTimerQueueTimer(nullptr, cursor_auto_hide_timer_, nullptr); cursor_auto_hide_timer_ = nullptr; } cursor_currently_auto_hidden_ = true; SetCursorIfFocusedOnClientArea(nullptr); } return 0; } break; case WM_TABLET_QUERYSYSTEMGESTURESTATUS: return // disables press and hold (right-click) gesture TABLET_DISABLE_PRESSANDHOLD | // disables UI feedback on pen up (waves) TABLET_DISABLE_PENTAPFEEDBACK | // disables UI feedback on pen button down (circle) TABLET_DISABLE_PENBARRELFEEDBACK | // disables pen flicks (back, forward, drag down, drag up) TABLET_DISABLE_FLICKS | TABLET_DISABLE_TOUCHSWITCH | TABLET_DISABLE_SMOOTHSCROLLING | TABLET_DISABLE_TOUCHUIFORCEON | TABLET_ENABLE_MULTITOUCHDATA; case WM_MENUCOMMAND: { MENUINFO menu_info = {}; menu_info.cbSize = sizeof(menu_info); menu_info.fMask = MIM_MENUDATA; GetMenuInfo(HMENU(lParam), &menu_info); auto parent_item = reinterpret_cast(menu_info.dwMenuData); auto child_item = reinterpret_cast(parent_item->child(wParam)); assert_not_null(child_item); WindowDestructionReceiver destruction_receiver(this); child_item->OnSelected(); if (destruction_receiver.IsWindowDestroyed()) { break; } // The menu item might have been destroyed by its OnSelected, don't do // anything with it here from now on. } break; } // The window might have been destroyed by the handlers, don't interact with // *this in this function from now on. // Passing the original hWnd argument rather than hwnd_ as the window might // have been closed or destroyed by a handler, making hwnd_ null even though // DefWindowProc still needs to be called to propagate the closing-related // messages needed by Windows, or inaccessible (due to use-after-free) at all. return DefWindowProcW(hWnd, message, wParam, lParam); } LRESULT CALLBACK Win32Window::WndProcThunk(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { if (hWnd) { Win32Window* window = nullptr; if (message == WM_NCCREATE) { auto create_struct = reinterpret_cast(lParam); window = reinterpret_cast(create_struct->lpCreateParams); SetWindowLongPtr(hWnd, GWLP_USERDATA, (__int3264)(LONG_PTR)window); // Don't miss any messages (as they may have effect on the actual state // stored in Win32Window) sent before the completion of CreateWindowExW // dropped because `window->hwnd_ != hWnd`, when the result of // CreateWindowExW still hasn't been assigned to `hwnd_` (however, don't // reattach this window to a closed window if WM_NCCREATE was somehow sent // to a window being closed). if (window->phase() == Phase::kOpening) { assert_true(!window->hwnd_ || window->hwnd_ == hWnd); window->hwnd_ = hWnd; } // Enable non-client area DPI scaling for AdjustWindowRectExForDpi to work // correctly between Windows 10 1607 (when AdjustWindowRectExForDpi and // EnableNonClientDpiScaling were added) and 1703 (when per-monitor // awareness version 2 was added with automatically enabled non-client // area DPI scaling). const Win32WindowedAppContext& win32_app_context = static_cast(window->app_context()); const Win32WindowedAppContext::PerMonitorDpiV2Api* per_monitor_dpi_v2_api = win32_app_context.per_monitor_dpi_v2_api(); if (per_monitor_dpi_v2_api) { per_monitor_dpi_v2_api->enable_non_client_dpi_scaling(hWnd); } // Already fully handled, no need to call Win32Window::WndProc. } else { window = reinterpret_cast(GetWindowLongPtr(hWnd, GWLP_USERDATA)); if (window && window->hwnd_ == hWnd) { return window->WndProc(hWnd, message, wParam, lParam); } } } return DefWindowProcW(hWnd, message, wParam, lParam); } std::unique_ptr MenuItem::Create(Type type, const std::string& text, const std::string& hotkey, std::function callback) { return std::make_unique(type, text, hotkey, callback); } Win32MenuItem::Win32MenuItem(Type type, const std::string& text, const std::string& hotkey, std::function callback) : MenuItem(type, text, hotkey, std::move(callback)) { switch (type) { case MenuItem::Type::kNormal: handle_ = CreateMenu(); break; case MenuItem::Type::kPopup: handle_ = CreatePopupMenu(); break; default: // May just be a placeholder. break; } if (handle_) { MENUINFO menu_info = {}; menu_info.cbSize = sizeof(menu_info); menu_info.fMask = MIM_MENUDATA | MIM_STYLE; menu_info.dwMenuData = ULONG_PTR(this); menu_info.dwStyle = MNS_NOTIFYBYPOS; SetMenuInfo(handle_, &menu_info); } } Win32MenuItem::~Win32MenuItem() { if (handle_) { DestroyMenu(handle_); } } void Win32MenuItem::SetEnabled(bool enabled) { UINT enable_flags = MF_BYPOSITION | (enabled ? MF_ENABLED : MF_GRAYED); UINT i = 0; for (auto iter = children_.begin(); iter != children_.end(); ++iter, ++i) { EnableMenuItem(handle_, i, enable_flags); } } void Win32MenuItem::OnChildAdded(MenuItem* generic_child_item) { auto child_item = static_cast(generic_child_item); switch (child_item->type()) { case MenuItem::Type::kNormal: // Nothing special. break; case MenuItem::Type::kPopup: { auto wide_text = rex::string::to_utf16(child_item->text()); AppendMenuW(handle_, MF_POPUP, reinterpret_cast(child_item->handle()), reinterpret_cast(wide_text.c_str())); } break; case MenuItem::Type::kSeparator: AppendMenuW(handle_, MF_SEPARATOR, UINT_PTR(child_item->handle_), 0); break; case MenuItem::Type::kString: auto full_name = child_item->text(); if (!child_item->hotkey().empty()) { full_name += "\t" + child_item->hotkey(); } auto wide_name = rex::string::to_utf16(full_name); AppendMenuW(handle_, MF_STRING, UINT_PTR(child_item->handle_), reinterpret_cast(wide_name.c_str())); break; } } void Win32MenuItem::OnChildRemoved(MenuItem* generic_child_item) {} } // namespace ui } // namespace rex