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]
This commit is contained in:
Pixelplanet committed 2026-10-20 12:29:45 +02:00
1 parent 7aede92244
commit 2f684e23a2
4 files changed
+77 -21

No files matched your search

+13
View File
@@ -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]`.
---
+36 -19
View File
@@ -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()) {
+2
View File
@@ -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};
+26 -2
View File
@@ -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<int>(CtrlButton::HideHud)) {
const CtrlButtonRect rH = get_ctrl_button_rect(static_cast<int>(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<int>(CtrlButton::Stop)) {
const CtrlButtonRect rSt = get_ctrl_button_rect(static_cast<int>(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<int>(CtrlButton::ScreenToggle));
drawFitCentered(rS.cx, rS.cy, rS.hw * 1.88f, pillSc, themeTextCol, hudScreen_);
// Tooltip text for button hover
if (pointerButton_ == static_cast<int>(CtrlButton::HideHud)) {
const CtrlButtonRect rH = get_ctrl_button_rect(static_cast<int>(CtrlButton::HideHud));
drawFitCentered(rH.cx, rH.cy + 0.065f, 0.12f, pillSc * 0.90f, themeTextCol, "Hide UI");
} else if (pointerButton_ == static_cast<int>(CtrlButton::Stop)) {
const CtrlButtonRect rSt = get_ctrl_button_rect(static_cast<int>(CtrlButton::Stop));
drawFitCentered(rSt.cx, rSt.cy + 0.065f, 0.09f, pillSc * 0.90f, themeTextCol, "Stop");
}
}
}