Gamepad-aware cursor handling in server browser
Add gamepad-aware pointer handling so the server browser hides the mouse cursor when a controller is connected and only shows it after mouse movement/click. Changes include: - PrismaUI: track browser pointer mode, prefer gamepad detection, suppress native CursorMenu/MenuCursor when in gamepad mode, unfocus the Ultralight view to prevent PrismaUI drawing its cursor, and restore pointer mode on mouse activity. Integrate pointer-mode initialization on browser show and call PollGamepad + UpdateBrowserPointerMode in the permanent task loop. - MenuInput: expose IsGamepadConnected(), provide PollGamepad() wrapper and simplify Register() logging. - UI: add window.CO_App.setPointerMode to toggle a co-gamepad-mode class and clear hover state; add CSS to hide the HTML cursor when co-gamepad-mode is active. - docs: add dev-log entries describing the PrismaUI Ultralight cursor fix and the new gamepad-aware server browser behavior. These changes ensure consistent cursor behavior between mouse and controller input, prevent the Ultralight cursor from appearing in controller mode, and restore the cursor after mouse interaction.
This commit is contained in:
+13
-10
@@ -210,7 +210,7 @@ namespace F4T::MenuInput
|
||||
g_menuInputBlockLayer.reset();
|
||||
}
|
||||
|
||||
void PollGamepad()
|
||||
void PollGamepadInternal()
|
||||
{
|
||||
if (!g_browserActive.load()) {
|
||||
return;
|
||||
@@ -326,21 +326,24 @@ namespace F4T::MenuInput
|
||||
return forwarded;
|
||||
}
|
||||
|
||||
bool IsGamepadConnected()
|
||||
{
|
||||
const auto* inputDeviceManager = RE::BSInputDeviceManager::GetSingleton();
|
||||
return inputDeviceManager && inputDeviceManager->IsGamepadConnected();
|
||||
}
|
||||
|
||||
void Register()
|
||||
{
|
||||
if (g_registered.exchange(true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* taskInterface = F4SE::GetTaskInterface();
|
||||
if (taskInterface) {
|
||||
taskInterface->AddTaskPermanent([]() {
|
||||
PollGamepad();
|
||||
});
|
||||
REX::INFO("[MenuInput] Registered XInput polling for server browser.");
|
||||
} else {
|
||||
REX::WARN("[MenuInput] F4SE task interface unavailable; gamepad polling disabled.");
|
||||
}
|
||||
REX::INFO("[MenuInput] Browser gamepad forwarding registered.");
|
||||
}
|
||||
|
||||
void PollGamepad()
|
||||
{
|
||||
PollGamepadInternal();
|
||||
}
|
||||
|
||||
void SetBrowserActive(bool a_active)
|
||||
|
||||
+120
-1
@@ -12,6 +12,7 @@
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string_view>
|
||||
|
||||
@@ -31,6 +32,10 @@ namespace F4T::PrismaUI
|
||||
bool g_menuWatchTaskStarted = false;
|
||||
bool g_pendingShow = false;
|
||||
int g_cursorRestoreFramesLeft = 0;
|
||||
bool g_browserMouseCursorVisible = true;
|
||||
bool g_browserPreferGamepad = false;
|
||||
bool g_hasLastCursorPos = false;
|
||||
POINT g_lastCursorPos{};
|
||||
|
||||
std::string GetLogPrefix()
|
||||
{
|
||||
@@ -49,6 +54,112 @@ namespace F4T::PrismaUI
|
||||
REX::WARN(std::string_view{ message });
|
||||
}
|
||||
|
||||
void SuppressNativeGameCursor()
|
||||
{
|
||||
auto* ui = RE::UI::GetSingleton();
|
||||
if (!ui) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto cursorMenu = ui->GetMenu<RE::CursorMenu>()) {
|
||||
if (cursorMenu->uiMovie && cursorMenu->uiMovie->GetVisible()) {
|
||||
cursorMenu->uiMovie->SetVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (auto* menuCursor = RE::MenuCursor::GetSingleton()) {
|
||||
menuCursor->UnregisterCursor();
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyBrowserPointerMode(bool a_mouseCursorVisible)
|
||||
{
|
||||
g_browserMouseCursorVisible = a_mouseCursorVisible;
|
||||
|
||||
if (g_api && g_api->IsValid(g_serverBrowserView) && g_visible) {
|
||||
if (a_mouseCursorVisible) {
|
||||
// Third Focus arg is disableFocusMenu, not cursor visibility. PrismaUI draws
|
||||
// cursor.png only while input capture is active (after Focus).
|
||||
g_api->Focus(g_serverBrowserView, false, true);
|
||||
} else {
|
||||
// Unfocus disables PrismaUI input capture so its Ultralight cursor is not drawn.
|
||||
g_api->Unfocus(g_serverBrowserView);
|
||||
SuppressNativeGameCursor();
|
||||
}
|
||||
|
||||
const auto* mode = a_mouseCursorVisible ? "mouse" : "gamepad";
|
||||
const auto script = std::format(
|
||||
"if(window.CO_App&&window.CO_App.setPointerMode){{window.CO_App.setPointerMode('{}');}}",
|
||||
mode);
|
||||
g_api->Invoke(g_serverBrowserView, script.c_str());
|
||||
} else if (!a_mouseCursorVisible) {
|
||||
SuppressNativeGameCursor();
|
||||
}
|
||||
}
|
||||
|
||||
void ResetBrowserPointerTracking()
|
||||
{
|
||||
g_hasLastCursorPos = false;
|
||||
g_lastCursorPos = {};
|
||||
}
|
||||
|
||||
void UpdateBrowserPointerMode()
|
||||
{
|
||||
if (!g_visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!g_browserPreferGamepad) {
|
||||
if (!g_browserMouseCursorVisible) {
|
||||
ApplyBrowserPointerMode(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* renderWindow = RE::BSGraphics::GetCurrentRendererWindow();
|
||||
if (!renderWindow || !renderWindow->hwnd) {
|
||||
return;
|
||||
}
|
||||
|
||||
POINT cursor{};
|
||||
if (!::GetCursorPos(&cursor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
::ScreenToClient(reinterpret_cast<HWND>(renderWindow->hwnd), &cursor);
|
||||
|
||||
const auto mouseButtonDown =
|
||||
(GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0 ||
|
||||
(GetAsyncKeyState(VK_RBUTTON) & 0x8000) != 0 ||
|
||||
(GetAsyncKeyState(VK_MBUTTON) & 0x8000) != 0;
|
||||
|
||||
bool mouseMoved = false;
|
||||
if (g_hasLastCursorPos) {
|
||||
const auto deltaX = std::abs(cursor.x - g_lastCursorPos.x);
|
||||
const auto deltaY = std::abs(cursor.y - g_lastCursorPos.y);
|
||||
mouseMoved = deltaX > 1 || deltaY > 1;
|
||||
}
|
||||
|
||||
g_lastCursorPos = cursor;
|
||||
g_hasLastCursorPos = true;
|
||||
|
||||
if (!g_browserMouseCursorVisible && (mouseMoved || mouseButtonDown)) {
|
||||
ApplyBrowserPointerMode(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!g_browserMouseCursorVisible) {
|
||||
// PrismaUI re-shows CursorMenu each frame when input capture is off; keep it hidden.
|
||||
SuppressNativeGameCursor();
|
||||
}
|
||||
}
|
||||
|
||||
void InitializeBrowserPointerMode()
|
||||
{
|
||||
g_browserPreferGamepad = F4T::MenuInput::IsGamepadConnected();
|
||||
ResetBrowserPointerTracking();
|
||||
}
|
||||
|
||||
void RestoreNativeGameCursor()
|
||||
{
|
||||
auto* ui = RE::UI::GetSingleton();
|
||||
@@ -82,6 +193,10 @@ namespace F4T::PrismaUI
|
||||
return;
|
||||
}
|
||||
|
||||
if (!g_browserMouseCursorVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto* renderWindow = RE::BSGraphics::GetCurrentRendererWindow();
|
||||
if (!renderWindow || !renderWindow->hwnd) {
|
||||
return;
|
||||
@@ -156,8 +271,9 @@ namespace F4T::PrismaUI
|
||||
g_visible = true;
|
||||
F4T::MenuInput::SetBrowserActive(true);
|
||||
F4T::MainMenuInject::SetMainMenuInputSuppressed(true);
|
||||
InitializeBrowserPointerMode();
|
||||
g_api->Show(g_serverBrowserView);
|
||||
g_api->Focus(g_serverBrowserView, false, false);
|
||||
ApplyBrowserPointerMode(!g_browserPreferGamepad);
|
||||
F4T::ServerBrowser::ServerBrowserBridge::OnBrowserShown();
|
||||
g_api->Invoke(
|
||||
g_serverBrowserView,
|
||||
@@ -312,6 +428,9 @@ namespace F4T::PrismaUI
|
||||
}
|
||||
|
||||
taskInterface->AddTaskPermanent([]() {
|
||||
F4T::MenuInput::PollGamepad();
|
||||
UpdateBrowserPointerMode();
|
||||
|
||||
static std::uint32_t hoverSyncCounter = 0;
|
||||
if (++hoverSyncCounter % 2 != 0) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user