Overhaul the VR menu UI: header, tabs, live video options, hover/click feedback, i18n, and dragging
build / build (push) Successful in 2m25s

- Header with title, git-tag-derived version, and a live FPS/frame-time/
  CPU/GPU-utilization/free-memory stats ribbon below the tab bar (FPS via
  a new per-frame tracker in xr_session.c; CPU/GPU via the OpenXR
  XR_META_performance_metrics extension, gracefully unavailable on
  runtimes that don't support it)
- Bottom tab bar with a live-editable Video Options tab (renderer/
  resolution), applying changes immediately via qs_config.c
- Hover and press feedback on every menu/keyboard button via a
  StateListDrawable (Android's default ripple never animates on this
  permanently unattached view tree), with a lighter default button color
  for visibility
- i18n for all menu/keyboard chrome text, following the game's own
  language setting (Strings.java)
- The main menu is now draggable via its header, matching the keyboard;
  xr_input.c's drag handling is generalized to per-overlay state instead
  of being keyboard-only
- The keyboard's title bar now matches the menu header's style
This commit is contained in:
ml
2026-09-05 07:02:30 +02:00
parent c4fd4eb3dd
commit 09280dd410
14 changed files with 1150 additions and 148 deletions
+85 -70
View File
@@ -26,17 +26,24 @@ static const float kHandColor[2][4] = {
{1.0f, 0.85f, 0.1f, 1.0f}, // right: amber
};
// The keyboard overlay's title bar (drag handle + Close button) is handled
// entirely here, before a hit would normally be dispatched to
// KeyboardOverlay as a touch - these fractions must agree with
// KeyboardOverlay.java's TITLE_BAR_HEIGHT/CLOSE_BUTTON_WIDTH constants (in
// pixels, out of KeyboardOverlay.WIDTH/HEIGHT there). A hit in the top
// TITLE_BAR_V_FRACTION of the quad, at or left of CLOSE_BUTTON_U_FRACTION,
// is the drag handle; right of that (still within the title bar) is the
// Close button, which is dispatched as an ordinary click instead. The menu
// launcher overlay has no title bar - it's a plain click-only panel.
#define TITLE_BAR_V_FRACTION (90.0f / 768.0f)
#define CLOSE_BUTTON_U_FRACTION (1.0f - 160.0f / 1024.0f)
// Both overlays' title bars (drag handle, plus the keyboard's Close button)
// are handled entirely here, before a hit would normally be dispatched to
// the Java side as a touch. A hit in the top *_TITLE_BAR_V_FRACTION of a
// quad, at or left of its own drag-handle-U-fraction, is the drag handle;
// right of that (still within the title bar) is dispatched as an ordinary
// click instead - the keyboard's Close button, or (the menu has no such
// button - MENU_DRAG_HANDLE_U_FRACTION is 1.0f, so its whole title bar
// width is the drag handle) nothing.
//
// KEYBOARD_TITLE_BAR_V_FRACTION/KEYBOARD_CLOSE_BUTTON_U_FRACTION must agree
// with KeyboardOverlay.java's TITLE_BAR_HEIGHT/CLOSE_BUTTON_WIDTH constants
// (in pixels, out of KeyboardOverlay.WIDTH/HEIGHT there);
// MENU_TITLE_BAR_V_FRACTION with MenuOverlay.java's HEADER_HEIGHT (out of
// MenuOverlay.HEIGHT).
#define KEYBOARD_TITLE_BAR_V_FRACTION (90.0f / 768.0f)
#define KEYBOARD_CLOSE_BUTTON_U_FRACTION (1.0f - 160.0f / 1024.0f)
#define MENU_TITLE_BAR_V_FRACTION (56.0f / 552.0f)
#define MENU_DRAG_HANDLE_U_FRACTION 1.0f
static XrInstance g_instance = XR_NULL_HANDLE;
static XrSession g_session = XR_NULL_HANDLE;
@@ -75,17 +82,24 @@ static bool g_menu_touch_active[2] = {false, false};
static bool g_keyboard_prev_hit[2] = {false, false};
static bool g_keyboard_touch_active[2] = {false, false};
// Drag state for repositioning the keyboard overlay via its title bar (see
// TITLE_BAR_V_FRACTION/CLOSE_BUTTON_U_FRACTION above) - the menu launcher
// has no title bar, so it's never draggable. g_keyboard_drag_hand is -1
// when g_keyboard_dragging is false. The offset is the vector from the
// quad's center to the grabbed point at drag-start, kept constant for the
// whole gesture so the quad follows the hand rigidly rather than
// re-centering under the ray each frame.
static bool g_keyboard_dragging = false;
static int g_keyboard_drag_hand = -1;
static float g_keyboard_drag_offset_x = 0.0f;
static float g_keyboard_drag_offset_y = 0.0f;
// Drag state for repositioning an overlay via its title bar (see the
// *_TITLE_BAR_V_FRACTION/*_DRAG_HANDLE_U_FRACTION or
// *_CLOSE_BUTTON_U_FRACTION defines above). hand is -1 when dragging is
// false. offset_x/offset_y is the vector from the quad's center to the
// grabbed point at drag-start, kept constant for the whole gesture so the
// quad follows the hand rigidly rather than re-centering under the ray
// each frame. Each draggable overlay (keyboard, menu) gets its own
// instance so they can be dragged independently/simultaneously by
// different hands.
typedef struct {
bool dragging;
int hand;
float offset_x;
float offset_y;
} XrDragState;
static XrDragState g_keyboard_drag = {false, -1, 0.0f, 0.0f};
static XrDragState g_menu_drag = {false, -1, 0.0f, 0.0f};
// A "view" reference space, located once per frame (not per hand) to get
// an approximate head position for the laser-beam billboard math below -
@@ -341,17 +355,21 @@ bool xr_input_init(XrInstance instance, XrSession session, int64_t swapchain_for
// Hit-tests/dispatches this hand's ray against one overlay quad - shared
// by the menu launcher and keyboard overlays in xr_input_sync_and_draw()
// below. dragCapable enables the keyboard-only title-bar/drag-handle
// handling (see TITLE_BAR_V_FRACTION/CLOSE_BUTTON_U_FRACTION); the menu
// launcher has no title bar, so it's always false there and every hit is a
// plain click. Returns true only when this hand's ray is actually
// relevant to this overlay this frame - landing on it now, or continuing
// a touch/drag begun on a previous frame for this hand that hasn't been
// released yet - not merely because the overlay is visible. The caller
// should only stop trying other targets (menu/keyboard/game quad) when
// this returns true; a visible-but-unclaimed overlay falls through so a
// lower-priority target (ultimately the game quad) can still be aimed at.
static bool xr_input_try_overlay(XrOverlay *overlay, bool dragCapable, int hand,
// below. titleBarVFraction/dragHandleUFraction and dragState select the
// per-overlay title-bar/drag-handle geometry and drag bookkeeping (see the
// *_TITLE_BAR_V_FRACTION/*_DRAG_HANDLE_U_FRACTION/*_CLOSE_BUTTON_U_FRACTION
// defines above and g_keyboard_drag/g_menu_drag) - both overlays are
// draggable via their title bar, and any hit below titleBarVFraction is
// always a plain click regardless of overlay. Returns true only when this
// hand's ray is actually relevant to this overlay this frame - landing on
// it now, or continuing a touch/drag begun on a previous frame for this
// hand that hasn't been released yet - not merely because the overlay is
// visible. The caller should only stop trying other targets (menu/
// keyboard/game quad) when this returns true; a visible-but-unclaimed
// overlay falls through so a lower-priority target (ultimately the game
// quad) can still be aimed at.
static bool xr_input_try_overlay(XrOverlay *overlay, float titleBarVFraction,
float dragHandleUFraction, XrDragState *dragState, int hand,
const XrSpaceLocation *location, float fx, float fy, float fz,
bool selectDownEdge, bool selectUpEdge, bool *touchActive,
bool *prevHit, const char *quadName, float *outCursorU,
@@ -367,7 +385,7 @@ static bool xr_input_try_overlay(XrOverlay *overlay, bool dragCapable, int hand,
// the ray this frame even if it has strayed off the panel's current
// bounds - see `claims` below.
bool hadTouch = *touchActive;
bool hadDrag = dragCapable && g_keyboard_dragging && g_keyboard_drag_hand == hand;
bool hadDrag = dragState->dragging && dragState->hand == hand;
float quadCenterX, quadCenterY, distance, halfWidth, halfHeight;
xr_overlay_get_quad_extent(overlay, &quadCenterX, &quadCenterY, &distance, &halfWidth,
@@ -417,34 +435,23 @@ static bool xr_input_try_overlay(XrOverlay *overlay, bool dragCapable, int hand,
// it's visible.
bool claims = hit || hadTouch || hadDrag;
if (!dragCapable) {
if (selectDownEdge && hit) {
xr_overlay_touch(overlay, u, v, true);
*touchActive = true;
} else if (selectUpEdge && *touchActive) {
xr_overlay_touch(overlay, u, v, false);
*touchActive = false;
}
return claims;
}
bool onTitleBar = hit && v < TITLE_BAR_V_FRACTION;
bool onDragHandle = onTitleBar && u <= CLOSE_BUTTON_U_FRACTION;
bool onTitleBar = hit && v < titleBarVFraction;
bool onDragHandle = onTitleBar && u <= dragHandleUFraction;
if (selectDownEdge) {
if (onDragHandle) {
g_keyboard_dragging = true;
g_keyboard_drag_hand = hand;
g_keyboard_drag_offset_x = worldX - quadCenterX;
g_keyboard_drag_offset_y = worldY - quadCenterY;
dragState->dragging = true;
dragState->hand = hand;
dragState->offset_x = worldX - quadCenterX;
dragState->offset_y = worldY - quadCenterY;
} else if (hit) {
xr_overlay_touch(overlay, u, v, true);
*touchActive = true;
}
} else if (selectUpEdge) {
if (g_keyboard_dragging && g_keyboard_drag_hand == hand) {
g_keyboard_dragging = false;
g_keyboard_drag_hand = -1;
if (dragState->dragging && dragState->hand == hand) {
dragState->dragging = false;
dragState->hand = -1;
} else if (*touchActive) {
xr_overlay_touch(overlay, u, v, false);
*touchActive = false;
@@ -454,9 +461,9 @@ static bool xr_input_try_overlay(XrOverlay *overlay, bool dragCapable, int hand,
// Re-intersects against the same fixed-distance plane every frame the
// drag continues, so the quad tracks the hand in real time rather than
// only jumping on the down/up edges above.
if (g_keyboard_dragging && g_keyboard_drag_hand == hand && planeHit) {
xr_overlay_set_position(overlay, worldX - g_keyboard_drag_offset_x,
worldY - g_keyboard_drag_offset_y);
if (dragState->dragging && dragState->hand == hand && planeHit) {
xr_overlay_set_position(overlay, worldX - dragState->offset_x,
worldY - dragState->offset_y);
}
return claims;
}
@@ -630,8 +637,12 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
// A drag can't get stuck active across a hide/reopen - e.g. Close
// being hit on one hand mid-drag on the other, or the session ending.
if (!keyboardVisible) {
g_keyboard_dragging = false;
g_keyboard_drag_hand = -1;
g_keyboard_drag.dragging = false;
g_keyboard_drag.hand = -1;
}
if (!menuVisible) {
g_menu_drag.dragging = false;
g_menu_drag.hand = -1;
}
// A hand only ever gets a laser-beam quad this frame if it actually
@@ -813,11 +824,12 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
bool keyboardPlaneHit = false;
float keyboardPlaneDistance = 0.0f;
if (!g_game_touch_active[hand] &&
xr_input_try_overlay(keyboardOverlay, true, hand, &location, fx, fy, fz,
selectDownEdge, selectUpEdge, &g_keyboard_touch_active[hand],
&g_keyboard_prev_hit[hand], "keyboard", &keyboardCursorU,
&keyboardCursorV, &keyboardCursorHit, &keyboardPlaneHit,
&keyboardPlaneDistance)) {
xr_input_try_overlay(keyboardOverlay, KEYBOARD_TITLE_BAR_V_FRACTION,
KEYBOARD_CLOSE_BUTTON_U_FRACTION, &g_keyboard_drag, hand,
&location, fx, fy, fz, selectDownEdge, selectUpEdge,
&g_keyboard_touch_active[hand], &g_keyboard_prev_hit[hand],
"keyboard", &keyboardCursorU, &keyboardCursorV,
&keyboardCursorHit, &keyboardPlaneHit, &keyboardPlaneDistance)) {
if (haveHead && keyboardPlaneHit)
xr_input_build_beam(hand, &location, fx, fy, fz,
fminf(keyboardPlaneDistance, MAX_BEAM_LENGTH_METERS), &headLoc);
@@ -826,10 +838,11 @@ void xr_input_sync_and_draw(XrSpace baseSpace, XrTime time, bool draw) {
bool menuPlaneHit = false;
float menuPlaneDistance = 0.0f;
if (!g_game_touch_active[hand] &&
xr_input_try_overlay(menuOverlay, false, hand, &location, fx, fy, fz, selectDownEdge,
selectUpEdge, &g_menu_touch_active[hand], &g_menu_prev_hit[hand],
"menu", &menuCursorU, &menuCursorV, &menuCursorHit, &menuPlaneHit,
&menuPlaneDistance)) {
xr_input_try_overlay(menuOverlay, MENU_TITLE_BAR_V_FRACTION,
MENU_DRAG_HANDLE_U_FRACTION, &g_menu_drag, hand, &location, fx,
fy, fz, selectDownEdge, selectUpEdge, &g_menu_touch_active[hand],
&g_menu_prev_hit[hand], "menu", &menuCursorU, &menuCursorV,
&menuCursorHit, &menuPlaneHit, &menuPlaneDistance)) {
if (haveHead && menuPlaneHit)
xr_input_build_beam(hand, &location, fx, fy, fz,
fminf(menuPlaneDistance, MAX_BEAM_LENGTH_METERS), &headLoc);
@@ -920,6 +933,8 @@ void xr_input_shutdown(void) {
g_prev_menu = false;
g_prev_space_key = false;
g_prev_forward_key = false;
g_keyboard_dragging = false;
g_keyboard_drag_hand = -1;
g_keyboard_drag.dragging = false;
g_keyboard_drag.hand = -1;
g_menu_drag.dragging = false;
g_menu_drag.hand = -1;
}