mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-19 18:11:39 -05:00
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:
1
.github/actions/spelling/expect/expect.txt
vendored
1
.github/actions/spelling/expect/expect.txt
vendored
@@ -1354,6 +1354,7 @@ PNMLINK
|
||||
pntm
|
||||
POBJECT
|
||||
Podcast
|
||||
POINTERUPDATE
|
||||
POINTSLIST
|
||||
policheck
|
||||
POLYTEXTW
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user