mirror of
https://github.com/xenia-project/xenia.git
synced 2024-05-31 19:17:55 -04:00
1349 lines
51 KiB
C++
1349 lines
51 KiB
C++
/**
|
|
******************************************************************************
|
|
* 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. *
|
|
******************************************************************************
|
|
*/
|
|
|
|
#include "xenia/ui/window_win.h"
|
|
|
|
#include <algorithm>
|
|
#include <memory>
|
|
#include <string>
|
|
|
|
#include "xenia/base/assert.h"
|
|
#include "xenia/base/filesystem.h"
|
|
#include "xenia/base/logging.h"
|
|
#include "xenia/ui/surface_win.h"
|
|
|
|
// Must be included before Windows headers for things like NOMINMAX.
|
|
#include "xenia/base/platform_win.h"
|
|
#include "xenia/ui/virtual_key.h"
|
|
#include "xenia/ui/windowed_app_context_win.h"
|
|
|
|
#include <ShellScalingApi.h>
|
|
#include <dwmapi.h>
|
|
|
|
namespace xe {
|
|
namespace ui {
|
|
|
|
std::unique_ptr<Window> Window::Create(WindowedAppContext& app_context,
|
|
const std::string_view title,
|
|
uint32_t desired_logical_width,
|
|
uint32_t desired_logical_height) {
|
|
return std::make_unique<Win32Window>(
|
|
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<const Win32WindowedAppContext&>(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"XeniaWindowClass";
|
|
if (!RegisterClassExW(&wcex)) {
|
|
XELOGE("RegisterClassEx failed");
|
|
return false;
|
|
}
|
|
has_registered_class = true;
|
|
}
|
|
|
|
const Win32MenuItem* main_menu =
|
|
static_cast<const Win32MenuItem*>(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.
|
|
hwnd_ = CreateWindowExW(
|
|
window_ex_style, L"XeniaWindowClass",
|
|
reinterpret_cast<LPCWSTR>(xe::to_utf16(GetTitle()).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_) {
|
|
XELOGE("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<HANDLE>(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<LPARAM>(icon_));
|
|
SendMessageW(hwnd_, WM_SETICON, ICON_SMALL,
|
|
reinterpret_cast<LPARAM>(icon_));
|
|
}
|
|
|
|
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_;
|
|
}
|
|
|
|
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<const Win32MenuItem*>(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() {
|
|
SetWindowTextW(hwnd_,
|
|
reinterpret_cast<LPCWSTR>(xe::to_utf16(GetTitle()).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<HICON>(GetClassLongPtrW(hwnd_, GCLP_HICON));
|
|
new_icon_small =
|
|
reinterpret_cast<HICON>(GetClassLongPtrW(hwnd_, GCLP_HICONSM));
|
|
// Not caring if it's null in the class, accepting anything the class
|
|
// specifies.
|
|
} else {
|
|
new_icon = CreateIconFromResourceEx(
|
|
static_cast<PBYTE>(const_cast<void*>(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<LPARAM>(new_icon));
|
|
SendMessageW(hwnd_, WM_SETICON, ICON_SMALL,
|
|
reinterpret_cast<LPARAM>(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<const Win32MenuItem*>(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<Surface> Win32Window::CreateSurfaceImpl(
|
|
Surface::TypeFlags allowed_types) {
|
|
HINSTANCE hInstance =
|
|
static_cast<const Win32WindowedAppContext&>(app_context()).hinstance();
|
|
if (allowed_types & Surface::kTypeFlag_Win32Hwnd) {
|
|
return std::make_unique<Win32HwndSurface>(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<const Win32WindowedAppContext&>(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<const Win32WindowedAppContext&>(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<const Win32WindowedAppContext&>(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:
|
|
OnKeyDown(e, destruction_receiver);
|
|
break;
|
|
case WM_KEYUP:
|
|
OnKeyUp(e, destruction_receiver);
|
|
break;
|
|
case WM_CHAR:
|
|
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<Win32Window*>(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
|
|
: DefWindowProc(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
|
|
: DefWindowProc(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<HDROP>(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, xe::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: {
|
|
OnMonitorUpdate(MonitorUpdateEvent(this, false));
|
|
} 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: {
|
|
OnMonitorUpdate(MonitorUpdateEvent(this, true));
|
|
} 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<const RECT*>(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<HWND>(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 = {0};
|
|
menu_info.cbSize = sizeof(menu_info);
|
|
menu_info.fMask = MIM_MENUDATA;
|
|
GetMenuInfo(HMENU(lParam), &menu_info);
|
|
auto parent_item = reinterpret_cast<Win32MenuItem*>(menu_info.dwMenuData);
|
|
auto child_item =
|
|
reinterpret_cast<Win32MenuItem*>(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 DefWindowProc(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<LPCREATESTRUCT>(lParam);
|
|
window = reinterpret_cast<Win32Window*>(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<const Win32WindowedAppContext&>(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<Win32Window*>(GetWindowLongPtr(hWnd, GWLP_USERDATA));
|
|
if (window && window->hwnd_ == hWnd) {
|
|
return window->WndProc(hWnd, message, wParam, lParam);
|
|
}
|
|
}
|
|
}
|
|
return DefWindowProc(hWnd, message, wParam, lParam);
|
|
}
|
|
|
|
std::unique_ptr<ui::MenuItem> MenuItem::Create(Type type,
|
|
const std::string& text,
|
|
const std::string& hotkey,
|
|
std::function<void()> callback) {
|
|
return std::make_unique<Win32MenuItem>(type, text, hotkey, callback);
|
|
}
|
|
|
|
Win32MenuItem::Win32MenuItem(Type type, const std::string& text,
|
|
const std::string& hotkey,
|
|
std::function<void()> 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 = {0};
|
|
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<Win32MenuItem*>(generic_child_item);
|
|
|
|
switch (child_item->type()) {
|
|
case MenuItem::Type::kNormal:
|
|
// Nothing special.
|
|
break;
|
|
case MenuItem::Type::kPopup:
|
|
AppendMenuW(
|
|
handle_, MF_POPUP, reinterpret_cast<UINT_PTR>(child_item->handle()),
|
|
reinterpret_cast<LPCWSTR>(xe::to_utf16(child_item->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();
|
|
}
|
|
AppendMenuW(handle_, MF_STRING, UINT_PTR(child_item->handle_),
|
|
reinterpret_cast<LPCWSTR>(xe::to_utf16(full_name).c_str()));
|
|
break;
|
|
}
|
|
}
|
|
|
|
void Win32MenuItem::OnChildRemoved(MenuItem* generic_child_item) {}
|
|
|
|
} // namespace ui
|
|
} // namespace xe
|