Fix cursor hiding on input (#18445)

The CoreWindow approach to implementing this has proven itself to be bug
prone. This PR switches to using the much better Win32 ShowCursor API,
which uses a reference count. This prevents the exact sort of race
condition we have where we we disable the cursor in our code and the
WinUI code then sets it to a different cursor internally which gets the
system out of sync. There's no WinUI API to just hide the cursor and if
it did, it would probably be a Boolean which would result in the same
issue.

Closes #18400

## Validation Steps Performed
It's difficult to assert the correctness of this approach, outside of
just trying it out (which I did and it works). The good news is that
this uses a static bool to ensure we only hide it exactly once and show
it exactly once and we do the latter on every WM_ACTIVATE message which
should hopefully restore the cursor when tabbing out and back in at
least.
This commit is contained in:
Leonard Hecker
2025-01-21 20:56:50 +01:00
committed by GitHub
parent 25392ea604
commit c56fb1b2d2
5 changed files with 65 additions and 63 deletions

View File

@@ -1354,6 +1354,7 @@ PNMLINK
pntm
POBJECT
Podcast
POINTERUPDATE
POINTSLIST
policheck
POLYTEXTW

View File

@@ -47,66 +47,6 @@ constexpr std::wstring_view StateCollapsed{ L"Collapsed" };
DEFINE_ENUM_FLAG_OPERATORS(winrt::Microsoft::Terminal::Control::CopyFormat);
DEFINE_ENUM_FLAG_OPERATORS(winrt::Microsoft::Terminal::Control::MouseButtonState);
// WinUI 3's UIElement.ProtectedCursor property allows someone to set the cursor on a per-element basis.
// This would allow us to hide the cursor when the TermControl has input focus and someone starts typing.
// Unfortunately, no equivalent exists for WinUI 2 so we fake it with the CoreWindow.
// There are 3 downsides:
// * SetPointerCapture() is global state and may interfere with other components.
// * You can't start dragging the cursor (for text selection) while it's still hidden.
// * The CoreWindow covers the union of all window rectangles, so the cursor is hidden even if it's outside
// the current foreground window, but still on top of another Terminal window in the background.
static void hideCursorUntilMoved()
{
static bool cursorIsHidden;
static const auto shouldVanish = []() {
BOOL shouldVanish = TRUE;
SystemParametersInfoW(SPI_GETMOUSEVANISH, 0, &shouldVanish, 0);
if (!shouldVanish)
{
return false;
}
const auto window = CoreWindow::GetForCurrentThread();
static constexpr auto releaseCapture = [](CoreWindow window, PointerEventArgs) {
if (cursorIsHidden)
{
window.ReleasePointerCapture();
}
};
static constexpr auto restoreCursor = [](CoreWindow window, PointerEventArgs) {
if (cursorIsHidden)
{
cursorIsHidden = false;
window.PointerCursor(CoreCursor{ CoreCursorType::Arrow, 0 });
}
};
winrt::Windows::Foundation::TypedEventHandler<CoreWindow, PointerEventArgs> releaseCaptureHandler{ releaseCapture };
std::ignore = window.PointerMoved(releaseCaptureHandler);
std::ignore = window.PointerPressed(releaseCaptureHandler);
std::ignore = window.PointerReleased(releaseCaptureHandler);
std::ignore = window.PointerWheelChanged(releaseCaptureHandler);
std::ignore = window.PointerCaptureLost(restoreCursor);
return true;
}();
if (shouldVanish && !cursorIsHidden)
{
try
{
const auto window = CoreWindow::GetForCurrentThread();
window.PointerCursor(nullptr);
window.SetPointerCapture();
cursorIsHidden = true;
}
catch (...)
{
// Swallow the 0x80070057 "Failed to get pointer information." exception that randomly occurs.
// Curiously, it doesn't happen during the PointerCursor() but during the SetPointerCapture() call (thanks, WinUI).
}
}
}
// InputPane::GetForCurrentView() does not reliably work for XAML islands,
// as it assumes that there's a 1:1 relationship between windows and threads.
//
@@ -1551,8 +1491,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation
return;
}
hideCursorUntilMoved();
const auto ch = e.Character();
const auto keyStatus = e.KeyStatus();
const auto scanCode = gsl::narrow_cast<WORD>(keyStatus.ScanCode);

View File

@@ -26,6 +26,48 @@ using VirtualKeyModifiers = winrt::Windows::System::VirtualKeyModifiers;
#define XAML_HOSTING_WINDOW_CLASS_NAME L"CASCADIA_HOSTING_WINDOW_CLASS"
#define IDM_SYSTEM_MENU_BEGIN 0x1000
// WinUI doesn't support the "Hide cursor on input" setting which is why all the modern Windows apps
// are broken in that respect. We want to support it though, so we implement an imitation of it here.
// We use the classic ShowCursor() API to hide it on keydown and show it on a select few messages.
// WinUI's SetPointerCapture() cannot be used for this, because it races with internal WinUI code
// calling that function and has proven itself to be very unreliable in practice.
//
// With UWP half the input stack got split off, and so most input events get rerouted through
// the CoreInput child window running in another thread (aka InputHost aka Windows.UI.Input).
// HideCursor() is called by WindowEmperor because WM_KEYDOWN is otherwise sent directly to that
// CoreInput window. Same for WM_POINTERUPDATE which we use to reliably detect cursor movement.
// WM_ACTIVATE on the other hand is only sent to each specific window and cannot be hooked by
// inspecting the MSG struct coming from GetMessage. That's why the code must be here.
// Best not think about this too much...
bool IslandWindow::IsCursorHidden() noexcept
{
return _cursorHidden;
}
void IslandWindow::HideCursor() noexcept
{
static const auto shouldVanish = []() noexcept {
BOOL shouldVanish = TRUE;
SystemParametersInfoW(SPI_GETMOUSEVANISH, 0, &shouldVanish, 0);
return shouldVanish != FALSE;
}();
if (!_cursorHidden && shouldVanish)
{
ShowCursor(FALSE);
_cursorHidden = true;
}
}
void IslandWindow::ShowCursorMaybe(const UINT message) noexcept
{
if (_cursorHidden && (message == WM_ACTIVATE || message == WM_POINTERUPDATE))
{
_cursorHidden = false;
ShowCursor(TRUE);
}
}
IslandWindow::IslandWindow() noexcept :
_interopWindowHandle{ nullptr },
_rootGrid{ nullptr },
@@ -425,6 +467,11 @@ void IslandWindow::_OnGetMinMaxInfo(const WPARAM /*wParam*/, const LPARAM lParam
[[nodiscard]] LRESULT IslandWindow::MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept
{
if (IsCursorHidden())
{
ShowCursorMaybe(message);
}
switch (message)
{
case WM_GETMINMAXINFO:

View File

@@ -14,6 +14,10 @@ class IslandWindow :
public BaseWindow<IslandWindow>
{
public:
static bool IsCursorHidden() noexcept;
static void HideCursor() noexcept;
static void ShowCursorMaybe(const UINT message) noexcept;
IslandWindow() noexcept;
virtual ~IslandWindow() override;
@@ -143,7 +147,7 @@ protected:
bool _minimizeToNotificationArea{ false };
std::unordered_map<UINT, SystemMenuItemInfo> _systemMenuItems;
UINT _systemMenuNextItemId;
UINT _systemMenuNextItemId = 0;
void _resetSystemMenu();
private:
@@ -154,4 +158,6 @@ private:
// though the total height will take into account the non-client area
// and the requirements of components hosted in the client area
static constexpr float minimumHeight = 0;
inline static bool _cursorHidden;
};

View File

@@ -417,6 +417,16 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow)
_dispatchSpecialKey(msg);
continue;
}
if (msg.message == WM_KEYDOWN)
{
IslandWindow::HideCursor();
}
}
if (IslandWindow::IsCursorHidden())
{
IslandWindow::ShowCursorMaybe(msg.message);
}
TranslateMessage(&msg);