From 2f684e23a22750dccc7772ecbb016ee4cf9fdaa8 Mon Sep 17 00:00:00 2001 From: Pixelplanet Date: Tue, 20 Oct 2026 12:29:45 +0200 Subject: [PATCH] fix(ui): fix HUD dismissal flicker and center transport recessed island alignment - Prevent in.selectHeld and trigger activity from resetting hudDismissed_ after manual dismiss - Add App::dismissHud() to reset activity anchors to current controller poses and establish a 500ms cooldown - Center transport recessed island background at cx=0.0f, hw=0.34f, giving symmetrical 34mm padding across all 5 buttons - Add hover tooltip pills for [Hide UI] and [Stop] --- TODO.md | 13 +++++++++ src/app/app.cpp | 55 ++++++++++++++++++++++++------------- src/app/app.hpp | 2 ++ src/gfx/vulkan_renderer.cpp | 28 +++++++++++++++++-- 4 files changed, 77 insertions(+), 21 deletions(-) diff --git a/TODO.md b/TODO.md index a1bffee..5589d5f 100644 --- a/TODO.md +++ b/TODO.md @@ -53,6 +53,19 @@ - Updated `tools/package-release.sh` to package updater scripts in the release tarball. - Updated `README.md` with comprehensive update instructions across all platforms. +- [x] **6. Fix UI Dismissal Flicker and Recessed Island Alignment** + - **Symptom:** Clicking outside the controls or clicking the Hide UI button caused the UI to briefly flicker for 1 frame and immediately reappear. The Hide UI button was also visually misaligned (half outside the dark island). + - **Root Cause:** + - `in.selectHeld` and trigger activity handling in `App::tick()` continuously reset `hudDismissed_ = false` and set `activity = true` on subsequent frames of the trigger pull, fighting the dismissal call. + - Controller activity anchors were not reset on dismissal, and there was no dismissal grace cooldown, causing slight hand tremor during trigger release to immediately re-trigger movement thresholds. + - The center transport recessed island background in `VulkanRenderer` was rendered at `cx = -0.05f, hw = 0.30f`, clipping through the center of the Hide UI button at `+0.26f`. + - **Resolution:** + - Created `App::dismissHud()` which sets `hudDismissed_ = true`, records `hudDismissTime_`, zeroes out activity, hides the control bar and laser ray, and snaps controller activity anchors to their exact current positions. + - Added a 500ms dismissal cooldown preventing accidental re-engagement from trigger release tremors. + - Blocked `in.select` and `in.selectHeld` from clearing `hudDismissed_` while the HUD is dismissed. + - Re-centered the transport recessed island background to `cx = 0.0f, hw = 0.34f`, creating perfect 34mm symmetrical padding for all 5 buttons (`Stop`, `SeekBack`, `PlayPause`, `SeekFwd`, `HideHud`). + - Added hover tooltip pills for `[Hide UI]` and `[Stop]`. + --- diff --git a/src/app/app.cpp b/src/app/app.cpp index 58b5720..b1fdd70 100644 --- a/src/app/app.cpp +++ b/src/app/app.cpp @@ -311,6 +311,8 @@ void App::loadMedia(const std::string& path) { PIXELVR_LOG_WARN("Failed to open %s", path.c_str()); return; } + hudDismissed_ = false; + lastControllerActivity_ = std::chrono::steady_clock::now(); // 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 @@ -548,6 +550,20 @@ void App::performVaultLock() { PIXELVR_LOG_INFO("Vault locked and sanitized"); } +void App::dismissHud(const XrInputState& in, std::chrono::steady_clock::time_point nowT) { + hudDismissed_ = true; + hudDismissTime_ = nowT; + lastControllerActivity_ = nowT - std::chrono::seconds(10); + renderer_.setControlBar(false); + for (int h = 0; h < 2; ++h) { + if (in.controllers[h].active) { + for (int k = 0; k < 3; ++k) activityAnchorPos_[h][k] = in.controllers[h].position[k]; + for (int k = 0; k < 4; ++k) activityAnchorOrient_[h][k] = in.controllers[h].orientation[k]; + } + } + PIXELVR_LOG_INFO("Player HUD dismissed"); +} + void App::openSmbModal(bool isNew, const std::string& smbPath) { #ifdef PIXELVR_HAVE_FREETYPE smbModal_.active = true; @@ -1018,6 +1034,8 @@ int App::run() { // Controller activity: moving a controller past a small threshold (or // any button/stick input) keeps the laser, models and controls shown; // after a few seconds of stillness they fade away. + const auto timeSinceDismiss = nowT - hudDismissTime_; + const bool inDismissCooldown = hudDismissed_ && (timeSinceDismiss < std::chrono::milliseconds(500)); bool activity = false; for (int h = 0; h < 2; ++h) { const auto& c = in.controllers[h]; @@ -1042,8 +1060,8 @@ int App::run() { const float rotDiff = std::fabs(od); if (hudDismissed_) { - // Requires intentional hand movement to re-engage HUD after manual dismissal - if (moveDist > 0.10f || rotDiff < 0.970f) { + // Requires intentional hand movement after dismissal cooldown to re-engage HUD + if (!inDismissCooldown && (moveDist > 0.12f || rotDiff < 0.950f)) { hudDismissed_ = false; activity = true; for (int k = 0; k < 3; ++k) activityAnchorPos_[h][k] = c.position[k]; @@ -1056,12 +1074,17 @@ int App::run() { for (int k = 0; k < 4; ++k) activityAnchorOrient_[h][k] = c.orientation[k]; } } - if (in.togglePlay || in.browserToggle || in.select || in.selectHeld || - in.recenter || std::fabs(in.thumbstickX) > 0.15f || - std::fabs(in.thumbstickY) > 0.15f || - std::fabs(in.thumbstickRX) > 0.15f) { + const bool stickMoved = (std::fabs(in.thumbstickX) > 0.25f || + std::fabs(in.thumbstickY) > 0.25f || + std::fabs(in.thumbstickRX) > 0.25f || + std::fabs(in.thumbstickRY) > 0.25f); + if (in.togglePlay || in.browserToggle || in.recenter || stickMoved) { + activity = true; + if (!inDismissCooldown) { + hudDismissed_ = false; + } + } else if (!hudDismissed_ && (in.select || in.selectHeld)) { activity = true; - hudDismissed_ = false; } if (activity) { lastControllerActivity_ = nowT; @@ -1085,9 +1108,7 @@ int App::run() { browser_.setOpen(false); } else if (mediaOpen_ && renderer_.controlBarVisible()) { // HUD is visible during playback: B/Y dismisses the HUD! - hudDismissed_ = true; - lastControllerActivity_ = nowT - std::chrono::seconds(10); - renderer_.setControlBar(false); + dismissHud(in, nowT); } else { browser_.open(); } @@ -1523,7 +1544,7 @@ int App::run() { if (kbActive && !renderer_.laserOn(0) && !renderer_.laserOn(1)) { renderer_.setLaserOn(1, true); } - const bool showLaser = (uiPanelActive || controllersEngaged || kbActive); + const bool showLaser = (uiPanelActive || (controllersEngaged && !hudDismissed_) || kbActive); // Build each controller's render state: corrected model pose, // low-pass smoothed (the raw hand pose jitters), while the aim // pose stays raw so the laser remains responsive. @@ -2067,8 +2088,8 @@ int App::run() { // Cinema Transport Dock is only active when the Unified Spatial Hub is closed. const bool barOk = !browserOpen && !settingsOpen && !downloadsWindow_ && - pendingDl_.empty(); - if (barOk) { + pendingDl_.empty() && !actionMenu_.visible && !smbModal_.active; + if (barOk && renderer_.controlBarVisible()) { // Point-and-click the media control bar with the trigger. const int btn = renderer_.pointerButton(); // The trigger clicks the bar; A does too (A is the reliable @@ -2160,18 +2181,14 @@ int App::run() { renderer_.setSphereZoom(1.0f); } } else if (cb == VulkanRenderer::CtrlButton::HideHud) { - hudDismissed_ = true; - lastControllerActivity_ = nowT - std::chrono::seconds(10); - renderer_.setControlBar(false); + dismissHud(in, nowT); } #endif interacted = true; } // Click outside the control dock in empty space immediately hides the HUD if (in.select && btn < 0 && !renderer_.pointerOnTimeline() && !renderer_.pointerOnTimeToggle()) { - hudDismissed_ = true; - lastControllerActivity_ = nowT - std::chrono::seconds(10); - renderer_.setControlBar(false); + dismissHud(in, nowT); interacted = true; } if (in.select && renderer_.pointerOnTimeToggle()) { diff --git a/src/app/app.hpp b/src/app/app.hpp index 30e198d..5d80078 100644 --- a/src/app/app.hpp +++ b/src/app/app.hpp @@ -78,6 +78,8 @@ private: bool downloadsWindow_ = false; // downloads list window toggled from the bar bool dlWasActive_ = false; // edge detect: auto-open the window on start bool hudDismissed_ = false; // manually dismissed player controls / HUD + std::chrono::steady_clock::time_point hudDismissTime_{}; // timestamp of manual dismissal + void dismissHud(const XrInputState& in, std::chrono::steady_clock::time_point nowT); // Low-pass smoothed model pose per hand (the raw hand pose jitters; the // system's own controller model is visibly smoothed). bool modelSmoothInit_[2] = {false, false}; diff --git a/src/gfx/vulkan_renderer.cpp b/src/gfx/vulkan_renderer.cpp index 73091ef..c90f397 100644 --- a/src/gfx/vulkan_renderer.cpp +++ b/src/gfx/vulkan_renderer.cpp @@ -2252,8 +2252,10 @@ void VulkanRenderer::renderView(uint32_t viewIndex, uint32_t imageIndex, drawUiElement(vpPanel, 0.0f, kBarY + 0.125f, kDockHalfW - 0.045f, 0.052f, UiShape::RoundedBox, grooveBg, 0.35f, 0.015f, grooveBorder); - // Center transport recessed island - drawUiElement(vpPanel, -0.05f, kBarY - 0.045f, 0.30f, 0.098f, + // Center transport recessed island: centered at 0.0f, symmetrically + // enclosing [Stop] (-0.26), [Seek -10] (-0.13), (Play/Pause) (0.00), + // [Seek +10] (+0.13), and [Hide UI] (+0.26). + drawUiElement(vpPanel, 0.0f, kBarY - 0.045f, 0.34f, 0.098f, UiShape::RoundedBox, grooveBg, 0.50f, 0.015f, grooveBorder); const float btnBgHover[4] = { @@ -2364,6 +2366,19 @@ void VulkanRenderer::renderView(uint32_t viewIndex, uint32_t imageIndex, UiShape::RoundedBox, tipBg, 0.35f, 0.03f, themeAccentCol); } + // Button hover tooltip pills + if (pointerButton_ == static_cast(CtrlButton::HideHud)) { + const CtrlButtonRect rH = get_ctrl_button_rect(static_cast(CtrlButton::HideHud)); + const float tipBg[4] = {0.04f, 0.07f, 0.12f, 0.96f}; + drawUiElement(vpPanel, rH.cx, rH.cy + 0.065f, 0.068f, 0.024f, + UiShape::RoundedBox, tipBg, 0.35f, 0.025f, themeAccentCol); + } else if (pointerButton_ == static_cast(CtrlButton::Stop)) { + const CtrlButtonRect rSt = get_ctrl_button_rect(static_cast(CtrlButton::Stop)); + const float tipBg[4] = {0.04f, 0.07f, 0.12f, 0.96f}; + drawUiElement(vpPanel, rSt.cx, rSt.cy + 0.065f, 0.052f, 0.024f, + UiShape::RoundedBox, tipBg, 0.35f, 0.025f, themeAccentCol); + } + if (textReady_) { vkCmdBindPipeline(commandBuffer_, VK_PIPELINE_BIND_POINT_GRAPHICS, textPipeline_); vkCmdBindDescriptorSets(commandBuffer_, VK_PIPELINE_BIND_POINT_GRAPHICS, @@ -2407,6 +2422,15 @@ void VulkanRenderer::renderView(uint32_t viewIndex, uint32_t imageIndex, const CtrlButtonRect rS = get_ctrl_button_rect(static_cast(CtrlButton::ScreenToggle)); drawFitCentered(rS.cx, rS.cy, rS.hw * 1.88f, pillSc, themeTextCol, hudScreen_); + + // Tooltip text for button hover + if (pointerButton_ == static_cast(CtrlButton::HideHud)) { + const CtrlButtonRect rH = get_ctrl_button_rect(static_cast(CtrlButton::HideHud)); + drawFitCentered(rH.cx, rH.cy + 0.065f, 0.12f, pillSc * 0.90f, themeTextCol, "Hide UI"); + } else if (pointerButton_ == static_cast(CtrlButton::Stop)) { + const CtrlButtonRect rSt = get_ctrl_button_rect(static_cast(CtrlButton::Stop)); + drawFitCentered(rSt.cx, rSt.cy + 0.065f, 0.09f, pillSc * 0.90f, themeTextCol, "Stop"); + } } }