From ec9821f1af2f103b194986cd823614811bacfb2f Mon Sep 17 00:00:00 2001 From: Pixelplanet Date: Tue, 20 Oct 2026 15:06:50 +0200 Subject: [PATCH] fix(input): optimize keyboard height/tilt, isolate text fields, and fix transport dock trigger --- src/app/app.cpp | 27 ++++++++++++-- src/gfx/vulkan_renderer.cpp | 27 +++++++------- src/xr/vr_keyboard.cpp | 71 +++++++++++++++++++++---------------- src/xr/vr_keyboard.hpp | 1 + 4 files changed, 80 insertions(+), 46 deletions(-) diff --git a/src/app/app.cpp b/src/app/app.cpp index a97f190..0dfaeeb 100644 --- a/src/app/app.cpp +++ b/src/app/app.cpp @@ -313,6 +313,13 @@ void App::loadMedia(const std::string& path) { } hudDismissed_ = false; lastControllerActivity_ = std::chrono::steady_clock::now(); + VrKeyboard::instance().hide(); + actionMenu_.visible = false; + smbModal_.active = false; + smbModal_.editingField = SmbModalState::Field::None; + browser_.setOpen(false); + downloadsWindow_ = false; + pendingDl_.clear(); // Refine the stereo layout from the frame aspect ratio when the filename did // not state it: once the projection is a VR type the aspect is unambiguous @@ -665,6 +672,8 @@ void App::saveSmbModal() { SmbCredentialsManager::instance().addOrUpdateShare(s); smbModal_.active = false; + smbModal_.editingField = SmbModalState::Field::None; + VrKeyboard::instance().hide(); actionMenu_.visible = false; browser_.openNetwork(); #endif @@ -676,6 +685,8 @@ void App::deleteSmbModal() { SmbCredentialsManager::instance().deleteShare(smbModal_.origHost, smbModal_.origShare); } smbModal_.active = false; + smbModal_.editingField = SmbModalState::Field::None; + VrKeyboard::instance().hide(); actionMenu_.visible = false; browser_.openNetwork(); #endif @@ -1115,12 +1126,18 @@ int App::run() { if (in.browserToggle) { if (actionMenu_.visible) { actionMenu_.visible = false; + smbModal_.active = false; + smbModal_.editingField = SmbModalState::Field::None; + VrKeyboard::instance().hide(); } else if (downloadsWindow_) { downloadsWindow_ = false; } else if (settings_.isOpen()) { settings_.close(); } else if (browser_.isOpen()) { browser_.setOpen(false); + smbModal_.active = false; + smbModal_.editingField = SmbModalState::Field::None; + VrKeyboard::instance().hide(); } else if (mediaOpen_ && renderer_.controlBarVisible()) { // HUD is visible during playback: B/Y dismisses the HUD! dismissHud(in, nowT); @@ -1544,11 +1561,11 @@ int App::run() { } } const bool uiPanelActive = browserOpen || settingsOpen || - downloadsWindow_ || !pendingDl_.empty() || actionMenu_.visible; + downloadsWindow_ || !pendingDl_.empty() || actionMenu_.visible || smbModal_.active; // The Transport HUD is only visible during playback when the // Unified Spatial Hub and modals are closed. const bool barBlocked = browserOpen || settingsOpen || downloadsWindow_ || - !pendingDl_.empty() || actionMenu_.visible; + !pendingDl_.empty() || actionMenu_.visible || smbModal_.active; // Controls (buttons + timeline) show only while a controller is // actively moving; they fade after a few seconds of stillness. renderer_.setControlBar(!barBlocked && controllersEngaged && !hudDismissed_ && @@ -1932,6 +1949,8 @@ int App::run() { // Cancel actionMenu_.visible = false; smbModal_.active = false; + smbModal_.editingField = SmbModalState::Field::None; + VrKeyboard::instance().hide(); } else { // Delete Share deleteSmbModal(); @@ -1940,6 +1959,8 @@ int App::run() { // Cancel actionMenu_.visible = false; smbModal_.active = false; + smbModal_.editingField = SmbModalState::Field::None; + VrKeyboard::instance().hide(); } } interacted = true; @@ -2209,7 +2230,7 @@ int App::run() { interacted = true; } // Click outside the control dock in empty space immediately hides the HUD - if (in.select && btn < 0 && !renderer_.pointerOnTimeline() && !renderer_.pointerOnTimeToggle()) { + if (in.select && !renderer_.pointerActive()) { dismissHud(in, nowT); interacted = true; } diff --git a/src/gfx/vulkan_renderer.cpp b/src/gfx/vulkan_renderer.cpp index 6b52e37..6678ec1 100644 --- a/src/gfx/vulkan_renderer.cpp +++ b/src/gfx/vulkan_renderer.cpp @@ -1312,16 +1312,7 @@ void VulkanRenderer::updatePointer() { // 0. Virtual Keyboard takes top focus priority if (kbOpen) { - // First check if pointing at the text preview box on the panel - if (hx >= -kPanelW && hx <= kPanelW && hyy >= -kPanelTop && hyy <= kPanelTop) { - pointerActive_ = true; - pointerHand_ = h; - pointerHit_[0] = world.x; - pointerHit_[1] = world.y; - pointerHit_[2] = world.z; - break; - } - // Next check if pointing directly at the virtual keyboard overlay keys + // First priority: Check if pointing directly at the virtual keyboard overlay keys if (keyboardModal_.hasTransform) { const auto& m = keyboardModal_.transform; // Column 0: Local X axis (right) @@ -1341,9 +1332,9 @@ void VulkanRenderer::updatePointer() { const Vec3 rel = hitP - kbCenter; const float localX = dot(rel, kbRight); const float localY = dot(rel, kbUp); - // Overlay width is 1.05m (halfW = 0.525m). Height is ~0.45m (halfH = 0.225m). - // Allow generous interactive margin (0.58m x 0.28m) - if (std::fabs(localX) <= 0.58f && std::fabs(localY) <= 0.28f) { + // Overlay width is 1.05m (halfW = 0.525m). Height is ~0.40m (halfH = 0.20m). + // Allow generous interactive margin (0.58m x 0.26m) + if (std::fabs(localX) <= 0.58f && std::fabs(localY) <= 0.26f) { pointerActive_ = true; pointerHand_ = h; pointerHit_[0] = hitP.x; @@ -1354,6 +1345,16 @@ void VulkanRenderer::updatePointer() { } } } + // Second priority: Check if pointing at the floating modal text box directly above the keyboard + // Modal box center is cy = 0.16f, half-width 0.23f, half-height 0.13f + if (std::fabs(hx) <= 0.25f && std::fabs(hyy - 0.16f) <= 0.14f) { + pointerActive_ = true; + pointerHand_ = h; + pointerHit_[0] = world.x; + pointerHit_[1] = world.y; + pointerHit_[2] = world.z; + break; + } continue; } diff --git a/src/xr/vr_keyboard.cpp b/src/xr/vr_keyboard.cpp index 0e95715..dc5a646 100644 --- a/src/xr/vr_keyboard.cpp +++ b/src/xr/vr_keyboard.cpp @@ -212,11 +212,18 @@ bool VrKeyboard::show(const std::string& description, auto* ov = reinterpret_cast(vrOverlay_); if (!ov) return false; + if (isOpen_) { + hide(); + } + // Explicitly hide any existing keyboard session in SteamVR to clear lingering text buffers + ov->HideKeyboard(); + description_ = description; existingText_ = existingText; currentText_ = existingText; isPassword_ = isPassword; doneSubmitted_ = false; + userTyped_ = false; const vr::EGamepadTextInputMode mode = isPassword ? vr::k_EGamepadTextInputModePassword @@ -257,15 +264,18 @@ bool VrKeyboard::show(const std::string& description, } const float yaw = std::atan2(-fx, -fz); - // Place keyboard 1.05m in front of eyes, 0.35m below eye level + // Place keyboard 1.05m in front of eyes, 0.20m below eye level. + // The modal dialog box above ends at ~hy + 0.03m; this places the top edge of the + // keyboard (half-height ~0.20m) right at hy - 0.004m, exactly 3.4cm below the modal dialog. const float dist = 1.05f; const float kbX = hx + fx * dist; - const float kbY = hy - 0.35f; + const float kbY = hy - 0.20f; const float kbZ = hz + fz * dist; - // Tilt upward toward eyes by ~22 degrees (-0.38 rad). - // Top side slants backward away from user; face normal points up and toward eyes. - const float tilt = -0.38f; + // Tilt upward toward eyes by ~11.5 degrees (-0.20 rad). + // Center is 20cm below eyes at 1.05m distance: atan2(0.20, 1.05) ~= 0.189 rad. + // Tilt of -0.20 rad angles the keyboard face normal directly at the user's eyes! + const float tilt = -0.20f; vr::HmdMatrix34_t kbMat{}; kbMat.m[0][0] = std::cos(yaw); kbMat.m[0][1] = std::sin(yaw) * std::sin(tilt); @@ -329,6 +339,9 @@ void VrKeyboard::hide() { isOpen_ = false; kbConfigured_ = false; doneSubmitted_ = false; + userTyped_ = false; + currentText_.clear(); + existingText_.clear(); PIXELVR_LOG_INFO("VrKeyboard: Hidden virtual keyboard"); } @@ -342,6 +355,7 @@ void VrKeyboard::poll(std::function onDone, vr::VREvent_t event{}; while (sys->PollNextEvent(&event, sizeof(event))) { if (event.eventType == vr::VREvent_KeyboardCharInput) { + userTyped_ = true; char keyBuf[1024] = {0}; uint32_t len = ov->GetKeyboardText(keyBuf, sizeof(keyBuf)); if (len > 0) { @@ -374,6 +388,10 @@ void VrKeyboard::poll(std::function onDone, if (len > 0) { text = std::string(buf, len < sizeof(buf) ? len : std::strlen(buf)); } + ov->HideKeyboard(); + currentText_.clear(); + existingText_.clear(); + userTyped_ = false; PIXELVR_LOG_INFO("VrKeyboard: User submitted text via KeyboardDone: '%s'", text.c_str()); if (onDone) { onDone(text); @@ -382,27 +400,16 @@ void VrKeyboard::poll(std::function onDone, event.eventType == vr::VREvent_KeyboardClosed_Global) { isOpen_ = false; kbConfigured_ = false; + ov->HideKeyboard(); PIXELVR_LOG_INFO("VrKeyboard: Keyboard closed (doneSubmitted=%d)", doneSubmitted_ ? 1 : 0); if (!doneSubmitted_) { - // On Steam Frame / SteamOS, pressing the checkmark or closing the keyboard issues KeyboardClosed - // without KeyboardDone. Read the buffer and treat non-empty / modified input as submit! - char buf[1024] = {0}; - uint32_t len = ov->GetKeyboardText(buf, sizeof(buf)); - std::string text = currentText_; - if (len > 0) { - text = std::string(buf, len < sizeof(buf) ? len : std::strlen(buf)); - } - if (!text.empty() || text != existingText_) { - PIXELVR_LOG_INFO("VrKeyboard: Treating KeyboardClosed as submit with text: '%s'", text.c_str()); - doneSubmitted_ = true; - if (onDone) { - onDone(text); - } - } else { - PIXELVR_LOG_INFO("VrKeyboard: Keyboard cancelled without text change"); - if (onCancel) { - onCancel(); - } + // Closed without KeyboardDone: treat as cancel and forget any input + currentText_.clear(); + existingText_.clear(); + userTyped_ = false; + PIXELVR_LOG_INFO("VrKeyboard: Discarded input on KeyboardClosed"); + if (onCancel) { + onCancel(); } } } @@ -412,15 +419,19 @@ void VrKeyboard::poll(std::function onDone, if (isOpen_) { pollFramesSinceOpen_++; // Maintain and reinforce keyboard overlay configuration across the first 90 frames (~1 sec) - // to catch asynchronous creation by SteamVR's GamepadUI - if (!kbConfigured_ || pollFramesSinceOpen_ < 90) { + // until successfully configured + if (!kbConfigured_ && pollFramesSinceOpen_ < 90) { configureKeyboardOverlay(); } - char liveBuf[1024] = {0}; - uint32_t liveLen = ov->GetKeyboardText(liveBuf, sizeof(liveBuf)); - if (liveLen > 0) { - currentText_ = std::string(liveBuf, liveLen < sizeof(liveBuf) ? liveLen : std::strlen(liveBuf)); + // Only synchronize with SteamVR live buffer if the user has actively typed in this session. + // This avoids contaminating a fresh/empty input field with stale residual buffer from a previous field. + if (userTyped_) { + char liveBuf[1024] = {0}; + uint32_t liveLen = ov->GetKeyboardText(liveBuf, sizeof(liveBuf)); + if (liveLen > 0) { + currentText_ = std::string(liveBuf, liveLen < sizeof(liveBuf) ? liveLen : std::strlen(liveBuf)); + } } } } diff --git a/src/xr/vr_keyboard.hpp b/src/xr/vr_keyboard.hpp index 31a7c0b..700fb7c 100644 --- a/src/xr/vr_keyboard.hpp +++ b/src/xr/vr_keyboard.hpp @@ -48,6 +48,7 @@ private: bool available_ = false; bool isOpen_ = false; bool doneSubmitted_ = false; + bool userTyped_ = false; bool kbConfigured_ = false; int pollFramesSinceOpen_ = 0; float kbMat_[3][4]{};