blob: 5dcb2a618d1647d378d3865d7e8b3168493e4185 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "flutter/shell/platform/windows/window_win32.h"
#include <imm.h>
#include <cstring>
#include "dpi_utils_win32.h"
namespace flutter {
namespace {
static constexpr int32_t kDefaultPointerDeviceId = 0;
// This method is only valid during a window message related to mouse/touch
// input.
// See
// https://docs.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages?redirectedfrom=MSDN#distinguishing-pen-input-from-mouse-and-touch.
static FlutterPointerDeviceKind GetFlutterPointerDeviceKind() {
constexpr LPARAM kTouchOrPenSignature = 0xFF515700;
constexpr LPARAM kTouchSignature = kTouchOrPenSignature | 0x80;
constexpr LPARAM kSignatureMask = 0xFFFFFF00;
LPARAM info = GetMessageExtraInfo();
if ((info & kSignatureMask) == kTouchOrPenSignature) {
if ((info & kTouchSignature) == kTouchSignature) {
return kFlutterPointerDeviceKindTouch;
}
return kFlutterPointerDeviceKindStylus;
}
return kFlutterPointerDeviceKindMouse;
}
char32_t CodePointFromSurrogatePair(wchar_t high, wchar_t low) {
return 0x10000 + ((static_cast<char32_t>(high) & 0x000003FF) << 10) +
(low & 0x3FF);
}
static const int kMinTouchDeviceId = 0;
static const int kMaxTouchDeviceId = 128;
} // namespace
WindowWin32::WindowWin32()
: touch_id_generator_(kMinTouchDeviceId, kMaxTouchDeviceId) {
// Get the DPI of the primary monitor as the initial DPI. If Per-Monitor V2 is
// supported, |current_dpi_| should be updated in the
// kWmDpiChangedBeforeParent message.
current_dpi_ = GetDpiForHWND(nullptr);
}
WindowWin32::~WindowWin32() {
Destroy();
}
void WindowWin32::InitializeChild(const char* title,
unsigned int width,
unsigned int height) {
Destroy();
std::wstring converted_title = NarrowToWide(title);
WNDCLASS window_class = RegisterWindowClass(converted_title);
auto* result = CreateWindowEx(
0, window_class.lpszClassName, converted_title.c_str(),
WS_CHILD | WS_VISIBLE, CW_DEFAULT, CW_DEFAULT, width, height,
HWND_MESSAGE, nullptr, window_class.hInstance, this);
if (result == nullptr) {
auto error = GetLastError();
LPWSTR message = nullptr;
size_t size = FormatMessageW(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
reinterpret_cast<LPWSTR>(&message), 0, NULL);
OutputDebugString(message);
LocalFree(message);
}
}
std::wstring WindowWin32::NarrowToWide(const char* source) {
size_t length = strlen(source);
size_t outlen = 0;
std::wstring wideTitle(length, L'#');
mbstowcs_s(&outlen, &wideTitle[0], length + 1, source, length);
return wideTitle;
}
WNDCLASS WindowWin32::RegisterWindowClass(std::wstring& title) {
window_class_name_ = title;
WNDCLASS window_class{};
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
window_class.lpszClassName = title.c_str();
window_class.style = CS_HREDRAW | CS_VREDRAW;
window_class.cbClsExtra = 0;
window_class.cbWndExtra = 0;
window_class.hInstance = GetModuleHandle(nullptr);
window_class.hIcon = nullptr;
window_class.hbrBackground = 0;
window_class.lpszMenuName = nullptr;
window_class.lpfnWndProc = WndProc;
RegisterClass(&window_class);
return window_class;
}
LRESULT CALLBACK WindowWin32::WndProc(HWND const window,
UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
if (message == WM_NCCREATE) {
auto cs = reinterpret_cast<CREATESTRUCT*>(lparam);
SetWindowLongPtr(window, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(cs->lpCreateParams));
auto that = static_cast<WindowWin32*>(cs->lpCreateParams);
that->window_handle_ = window;
that->text_input_manager_.SetWindowHandle(window);
RegisterTouchWindow(window, 0);
} else if (WindowWin32* that = GetThisFromHandle(window)) {
return that->HandleMessage(message, wparam, lparam);
}
return DefWindowProc(window, message, wparam, lparam);
}
void WindowWin32::TrackMouseLeaveEvent(HWND hwnd) {
if (!tracking_mouse_leave_) {
TRACKMOUSEEVENT tme;
tme.cbSize = sizeof(tme);
tme.hwndTrack = hwnd;
tme.dwFlags = TME_LEAVE;
TrackMouseEvent(&tme);
tracking_mouse_leave_ = true;
}
}
void WindowWin32::OnGetObject(UINT const message,
WPARAM const wparam,
LPARAM const lparam) {
LRESULT reference_result = static_cast<LRESULT>(0L);
// Only the lower 32 bits of lparam are valid when checking the object id
// because it sometimes gets sign-extended incorrectly (but not always).
DWORD obj_id = static_cast<DWORD>(static_cast<DWORD_PTR>(lparam));
bool is_msaa_request = static_cast<DWORD>(OBJID_CLIENT) == obj_id;
if (is_msaa_request) {
// On Windows, we don't get a notification that the screen reader has been
// enabled or disabled. There is an API to query for screen reader state,
// but that state isn't set by all screen readers, including by Narrator,
// the screen reader that ships with Windows:
// https://docs.microsoft.com/en-us/windows/win32/winauto/screen-reader-parameter
//
// Instead, we enable semantics in Flutter if Windows issues queries for
// Microsoft Active Accessibility (MSAA) COM objects.
OnUpdateSemanticsEnabled(true);
// TODO(cbracken): https://github.com/flutter/flutter/issues/77838
// Once AccessibilityBridge is wired up, look up the IAccessible
// representing the root view and call LresultFromObject.
}
}
void WindowWin32::OnImeSetContext(UINT const message,
WPARAM const wparam,
LPARAM const lparam) {
if (wparam != 0) {
text_input_manager_.CreateImeWindow();
}
}
void WindowWin32::OnImeStartComposition(UINT const message,
WPARAM const wparam,
LPARAM const lparam) {
text_input_manager_.CreateImeWindow();
OnComposeBegin();
}
void WindowWin32::OnImeComposition(UINT const message,
WPARAM const wparam,
LPARAM const lparam) {
// Update the IME window position.
text_input_manager_.UpdateImeWindow();
if (lparam & GCS_COMPSTR) {
// Read the in-progress composing string.
long pos = text_input_manager_.GetComposingCursorPosition();
std::optional<std::u16string> text =
text_input_manager_.GetComposingString();
if (text) {
OnComposeChange(text.value(), pos);
}
} else if (lparam & GCS_RESULTSTR) {
// Commit but don't end composing.
// Read the committed composing string.
long pos = text_input_manager_.GetComposingCursorPosition();
std::optional<std::u16string> text = text_input_manager_.GetResultString();
if (text) {
OnComposeChange(text.value(), pos);
OnComposeCommit();
}
}
}
void WindowWin32::OnImeEndComposition(UINT const message,
WPARAM const wparam,
LPARAM const lparam) {
text_input_manager_.DestroyImeWindow();
OnComposeEnd();
}
void WindowWin32::OnImeRequest(UINT const message,
WPARAM const wparam,
LPARAM const lparam) {
// TODO(cbracken): Handle IMR_RECONVERTSTRING, IMR_DOCUMENTFEED,
// and IMR_QUERYCHARPOSITION messages.
// https://github.com/flutter/flutter/issues/74547
}
void WindowWin32::AbortImeComposing() {
text_input_manager_.AbortComposing();
}
void WindowWin32::UpdateCursorRect(const Rect& rect) {
text_input_manager_.UpdateCaretRect(rect);
}
static uint16_t ResolveKeyCode(uint16_t original,
bool extended,
uint8_t scancode) {
switch (original) {
case VK_SHIFT:
case VK_LSHIFT:
return MapVirtualKey(scancode, MAPVK_VSC_TO_VK_EX);
case VK_MENU:
case VK_LMENU:
return extended ? VK_RMENU : VK_LMENU;
case VK_CONTROL:
case VK_LCONTROL:
return extended ? VK_RCONTROL : VK_LCONTROL;
default:
return original;
}
}
static bool IsPrintable(uint32_t c) {
constexpr char32_t kMinPrintable = ' ';
constexpr char32_t kDelete = 0x7F;
return c >= kMinPrintable && c != kDelete;
}
LRESULT
WindowWin32::HandleMessage(UINT const message,
WPARAM const wparam,
LPARAM const lparam) noexcept {
LPARAM result_lparam = lparam;
int xPos = 0, yPos = 0;
UINT width = 0, height = 0;
UINT button_pressed = 0;
FlutterPointerDeviceKind device_kind;
switch (message) {
case kWmDpiChangedBeforeParent:
current_dpi_ = GetDpiForHWND(window_handle_);
OnDpiScale(current_dpi_);
return 0;
case WM_SIZE:
width = LOWORD(lparam);
height = HIWORD(lparam);
current_width_ = width;
current_height_ = height;
HandleResize(width, height);
break;
case WM_TOUCH: {
UINT num_points = LOWORD(wparam);
touch_points_.resize(num_points);
auto touch_input_handle = reinterpret_cast<HTOUCHINPUT>(lparam);
if (GetTouchInputInfo(touch_input_handle, num_points,
touch_points_.data(), sizeof(TOUCHINPUT))) {
for (const auto& touch : touch_points_) {
// Generate a mapped ID for the Windows-provided touch ID
auto touch_id = touch_id_generator_.GetGeneratedId(touch.dwID);
POINT pt = {TOUCH_COORD_TO_PIXEL(touch.x),
TOUCH_COORD_TO_PIXEL(touch.y)};
ScreenToClient(window_handle_, &pt);
auto x = static_cast<double>(pt.x);
auto y = static_cast<double>(pt.y);
if (touch.dwFlags & TOUCHEVENTF_DOWN) {
OnPointerDown(x, y, kFlutterPointerDeviceKindTouch, touch_id,
WM_LBUTTONDOWN);
} else if (touch.dwFlags & TOUCHEVENTF_MOVE) {
OnPointerMove(x, y, kFlutterPointerDeviceKindTouch, touch_id);
} else if (touch.dwFlags & TOUCHEVENTF_UP) {
OnPointerUp(x, y, kFlutterPointerDeviceKindTouch, touch_id,
WM_LBUTTONDOWN);
OnPointerLeave(kFlutterPointerDeviceKindTouch, touch_id);
touch_id_generator_.ReleaseNumber(touch.dwID);
}
}
CloseTouchInputHandle(touch_input_handle);
}
return 0;
}
case WM_MOUSEMOVE:
device_kind = GetFlutterPointerDeviceKind();
if (device_kind == kFlutterPointerDeviceKindMouse) {
TrackMouseLeaveEvent(window_handle_);
xPos = GET_X_LPARAM(lparam);
yPos = GET_Y_LPARAM(lparam);
OnPointerMove(static_cast<double>(xPos), static_cast<double>(yPos),
device_kind, kDefaultPointerDeviceId);
}
break;
case WM_MOUSELEAVE:
device_kind = GetFlutterPointerDeviceKind();
if (device_kind == kFlutterPointerDeviceKindMouse) {
OnPointerLeave(device_kind, kDefaultPointerDeviceId);
}
// Once the tracked event is received, the TrackMouseEvent function
// resets. Set to false to make sure it's called once mouse movement is
// detected again.
tracking_mouse_leave_ = false;
break;
case WM_SETCURSOR: {
UINT hit_test_result = LOWORD(lparam);
if (hit_test_result == HTCLIENT) {
OnSetCursor();
return TRUE;
}
break;
}
case WM_SETFOCUS:
::CreateCaret(window_handle_, nullptr, 1, 1);
break;
case WM_KILLFOCUS:
::DestroyCaret();
break;
case WM_LBUTTONDOWN:
case WM_RBUTTONDOWN:
case WM_MBUTTONDOWN:
case WM_XBUTTONDOWN:
device_kind = GetFlutterPointerDeviceKind();
if (device_kind != kFlutterPointerDeviceKindMouse) {
break;
}
if (message == WM_LBUTTONDOWN) {
// Capture the pointer in case the user drags outside the client area.
// In this case, the "mouse leave" event is delayed until the user
// releases the button. It's only activated on left click given that
// it's more common for apps to handle dragging with only the left
// button.
SetCapture(window_handle_);
}
button_pressed = message;
if (message == WM_XBUTTONDOWN) {
button_pressed = GET_XBUTTON_WPARAM(wparam);
}
xPos = GET_X_LPARAM(lparam);
yPos = GET_Y_LPARAM(lparam);
OnPointerDown(static_cast<double>(xPos), static_cast<double>(yPos),
device_kind, kDefaultPointerDeviceId, button_pressed);
break;
case WM_LBUTTONUP:
case WM_RBUTTONUP:
case WM_MBUTTONUP:
case WM_XBUTTONUP:
device_kind = GetFlutterPointerDeviceKind();
if (device_kind != kFlutterPointerDeviceKindMouse) {
break;
}
if (message == WM_LBUTTONUP) {
ReleaseCapture();
}
button_pressed = message;
if (message == WM_XBUTTONUP) {
button_pressed = GET_XBUTTON_WPARAM(wparam);
}
xPos = GET_X_LPARAM(lparam);
yPos = GET_Y_LPARAM(lparam);
OnPointerUp(static_cast<double>(xPos), static_cast<double>(yPos),
device_kind, kDefaultPointerDeviceId, button_pressed);
break;
case WM_MOUSEWHEEL:
OnScroll(0.0,
-(static_cast<short>(HIWORD(wparam)) /
static_cast<double>(WHEEL_DELTA)),
kFlutterPointerDeviceKindMouse, kDefaultPointerDeviceId);
break;
case WM_MOUSEHWHEEL:
OnScroll((static_cast<short>(HIWORD(wparam)) /
static_cast<double>(WHEEL_DELTA)),
0.0, kFlutterPointerDeviceKindMouse, kDefaultPointerDeviceId);
break;
case WM_GETOBJECT:
OnGetObject(message, wparam, lparam);
break;
case WM_INPUTLANGCHANGE:
// TODO(cbracken): pass this to TextInputManager to aid with
// language-specific issues.
break;
case WM_IME_SETCONTEXT:
OnImeSetContext(message, wparam, lparam);
// Strip the ISC_SHOWUICOMPOSITIONWINDOW bit from lparam before passing it
// to DefWindowProc() so that the composition window is hidden since
// Flutter renders the composing string itself.
result_lparam &= ~ISC_SHOWUICOMPOSITIONWINDOW;
break;
case WM_IME_STARTCOMPOSITION:
OnImeStartComposition(message, wparam, lparam);
// Suppress further processing by DefWindowProc() so that the default
// system IME style isn't used, but rather the one set in the
// WM_IME_SETCONTEXT handler.
return TRUE;
case WM_IME_COMPOSITION:
OnImeComposition(message, wparam, lparam);
if (lparam & GCS_RESULTSTR || lparam & GCS_COMPSTR) {
// Suppress further processing by DefWindowProc() since otherwise it
// will emit the result string as WM_CHAR messages on commit. Instead,
// committing the composing text to the EditableText string is handled
// in TextInputModel::CommitComposing, triggered by
// OnImeEndComposition().
return TRUE;
}
break;
case WM_IME_ENDCOMPOSITION:
OnImeEndComposition(message, wparam, lparam);
return TRUE;
case WM_IME_REQUEST:
OnImeRequest(message, wparam, lparam);
break;
case WM_UNICHAR: {
// Tell third-pary app, we can support Unicode.
if (wparam == UNICODE_NOCHAR)
return TRUE;
// DefWindowProc will send WM_CHAR for this WM_UNICHAR.
break;
}
case WM_DEADCHAR:
case WM_SYSDEADCHAR:
case WM_CHAR:
case WM_SYSCHAR: {
static wchar_t s_pending_high_surrogate = 0;
wchar_t character = static_cast<wchar_t>(wparam);
std::u16string text({character});
char32_t code_point = character;
if (IS_HIGH_SURROGATE(character)) {
// Save to send later with the trailing surrogate.
s_pending_high_surrogate = character;
} else if (IS_LOW_SURROGATE(character) && s_pending_high_surrogate != 0) {
text.insert(text.begin(), s_pending_high_surrogate);
// Merge the surrogate pairs for the key event.
code_point =
CodePointFromSurrogatePair(s_pending_high_surrogate, character);
s_pending_high_surrogate = 0;
}
const unsigned int scancode = (lparam >> 16) & 0xff;
// All key presses that generate a character should be sent from
// WM_CHAR. In order to send the full key press information, the keycode
// is persisted in keycode_for_char_message_ obtained from WM_KEYDOWN.
//
// A high surrogate is always followed by a low surrogate, while a
// non-surrogate character always appears alone. Filter out high
// surrogates so that it's the low surrogate message that triggers
// the onKey, asks if the framework handles it (which can only be done
// once), and calls OnText during the redispatched messages.
if (keycode_for_char_message_ != 0 && !IS_HIGH_SURROGATE(character)) {
const bool extended = ((lparam >> 24) & 0x01) == 0x01;
const bool was_down = lparam & 0x40000000;
// Certain key combinations yield control characters as WM_CHAR's
// lParam. For example, 0x01 for Ctrl-A. Filter these characters.
// See
// https://docs.microsoft.com/en-us/windows/win32/learnwin32/accelerator-tables
const char32_t event_character =
(message == WM_DEADCHAR || message == WM_SYSDEADCHAR)
? Win32MapVkToChar(keycode_for_char_message_)
: IsPrintable(code_point) ? code_point
: 0;
bool handled = OnKey(keycode_for_char_message_, scancode, WM_KEYDOWN,
event_character, extended, was_down);
keycode_for_char_message_ = 0;
if (handled) {
// If the OnKey handler handles the message, then return so we don't
// pass it to OnText, because handling the message indicates that
// OnKey either just sent it to the framework to be processed.
//
// This message will be redispatched if not handled by the framework,
// during which the OnText (below) might be reached. However, if the
// original message was preceded by dead chars (such as ^ and e
// yielding ê), then since the redispatched message is no longer
// preceded by the dead char, the text will be wrong. Therefore we
// record the text here for the redispached event to use.
if (message == WM_CHAR) {
text_for_scancode_on_redispatch_[scancode] = text;
}
return 0;
}
}
// Of the messages handled here, only WM_CHAR should be treated as
// characters. WM_SYS*CHAR are not part of text input, and WM_DEADCHAR
// will be incorporated into a later WM_CHAR with the full character.
// Also filter out:
// - Lead surrogates, which like dead keys will be send once combined.
// - ASCII control characters, which are sent as WM_CHAR events for all
// control key shortcuts.
if (message == WM_CHAR && s_pending_high_surrogate == 0 &&
IsPrintable(character)) {
auto found_text_iter = text_for_scancode_on_redispatch_.find(scancode);
if (found_text_iter != text_for_scancode_on_redispatch_.end()) {
text = found_text_iter->second;
text_for_scancode_on_redispatch_.erase(found_text_iter);
}
OnText(text);
}
return 0;
}
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
case WM_KEYUP:
case WM_SYSKEYUP:
const bool is_keydown_message =
(message == WM_KEYDOWN || message == WM_SYSKEYDOWN);
// Check if this key produces a character. If so, the key press should
// be sent with the character produced at WM_CHAR. Store the produced
// keycode (it's not accessible from WM_CHAR) to be used in WM_CHAR.
//
// Messages with Control or Win modifiers down are never considered as
// character messages. This allows key combinations such as "CTRL + Digit"
// to properly produce key down events even though `MapVirtualKey` returns
// a valid character. See https://github.com/flutter/flutter/issues/85587.
unsigned int character = Win32MapVkToChar(wparam);
UINT next_key_message = PeekNextMessageType(WM_KEYFIRST, WM_KEYLAST);
bool has_wm_char =
(next_key_message == WM_DEADCHAR ||
next_key_message == WM_SYSDEADCHAR || next_key_message == WM_CHAR ||
next_key_message == WM_SYSCHAR);
if (character > 0 && is_keydown_message && has_wm_char) {
keycode_for_char_message_ = wparam;
return 0;
}
unsigned int keyCode(wparam);
const uint8_t scancode = (lparam >> 16) & 0xff;
const bool extended = ((lparam >> 24) & 0x01) == 0x01;
// If the key is a modifier, get its side.
keyCode = ResolveKeyCode(keyCode, extended, scancode);
const int action = is_keydown_message ? WM_KEYDOWN : WM_KEYUP;
const bool was_down = lparam & 0x40000000;
if (OnKey(keyCode, scancode, action, 0, extended, was_down)) {
return 0;
}
break;
}
return Win32DefWindowProc(window_handle_, message, wparam, result_lparam);
}
UINT WindowWin32::GetCurrentDPI() {
return current_dpi_;
}
UINT WindowWin32::GetCurrentWidth() {
return current_width_;
}
UINT WindowWin32::GetCurrentHeight() {
return current_height_;
}
HWND WindowWin32::GetWindowHandle() {
return window_handle_;
}
void WindowWin32::Destroy() {
if (window_handle_) {
text_input_manager_.SetWindowHandle(nullptr);
DestroyWindow(window_handle_);
window_handle_ = nullptr;
}
UnregisterClass(window_class_name_.c_str(), nullptr);
}
void WindowWin32::HandleResize(UINT width, UINT height) {
current_width_ = width;
current_height_ = height;
OnResize(width, height);
}
UINT WindowWin32::PeekNextMessageType(UINT wMsgFilterMin, UINT wMsgFilterMax) {
MSG next_message;
BOOL has_msg = Win32PeekMessage(&next_message, window_handle_, wMsgFilterMin,
wMsgFilterMax, PM_NOREMOVE);
if (!has_msg) {
return 0;
}
return next_message.message;
}
WindowWin32* WindowWin32::GetThisFromHandle(HWND const window) noexcept {
return reinterpret_cast<WindowWin32*>(
GetWindowLongPtr(window, GWLP_USERDATA));
}
LRESULT WindowWin32::Win32DefWindowProc(HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam) {
return DefWindowProc(hWnd, Msg, wParam, lParam);
}
BOOL WindowWin32::Win32PeekMessage(LPMSG lpMsg,
HWND hWnd,
UINT wMsgFilterMin,
UINT wMsgFilterMax,
UINT wRemoveMsg) {
return PeekMessage(lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax, wRemoveMsg);
}
uint32_t WindowWin32::Win32MapVkToChar(uint32_t virtual_key) {
return MapVirtualKey(virtual_key, MAPVK_VK_TO_CHAR);
}
} // namespace flutter