Skip to content

feat(ui/ux): update xserver quick menu to align with new UI#686

Merged
utkarshdalal merged 7 commits intoutkarshdalal:masterfrom
xXJSONDeruloXx:feat/xserver-quickmenu-sidepanel
Mar 2, 2026
Merged

feat(ui/ux): update xserver quick menu to align with new UI#686
utkarshdalal merged 7 commits intoutkarshdalal:masterfrom
xXJSONDeruloXx:feat/xserver-quickmenu-sidepanel

Conversation

@xXJSONDeruloXx
Copy link
Contributor

@xXJSONDeruloXx xXJSONDeruloXx commented Mar 1, 2026

  • rolls in most of the ui reworks to quick menu from feat: UI/UX Overhaul #395, adding controller focus, preserving suspend and resume of game proc, generally bringing in line with current master
image

Summary by cubic

Replaced the in-game back menu with a left-side QuickMenu panel that supports controller-first navigation and correctly routes gamepad input to the menu. Cleanly pauses/resumes the game; keyboard behavior stays the same.

  • New Features

    • Replaced NavigationDialog with a Compose QuickMenu in XServerScreen.
    • Gamepad-driven focus/selection; Compose consumes button and motion input while the menu is open with more reliable first-item focus.
    • Pauses game/audio while visible and resumes on dismiss; back closes the menu if already open.
    • Menu actions: toggle keyboard, show/hide input controls, edit controls (auto-creates a profile when needed), edit physical controller (shown only when a controller is connected), and exit game.
    • UI: left-side slide-in/out with rounded right corners; removed bold on focused text to prevent wrapping.
  • Bug Fixes

    • Fixed gamepad button routing and pause/resume logic while the sidebar is open and on dismiss.

Written for commit 2ff602f. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Quick Menu integrated into the main screen for streamlined access to keyboard, input settings, and profiles
    • Back button now dismisses the Quick Menu and routes navigation through it when open
  • Improvements

    • Refined menu entrance/exit animations and alignment for smoother presentation
    • More reliable focus handling with retry logic for quicker keyboard/gamepad navigation
    • Simplified menu typography for consistent item appearance

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 1, 2026

📝 Walkthrough

Walkthrough

Integrates QuickMenu into XServerScreen, changing back/overlay/navigation flows to route via the menu; updates QuickMenu animation direction, alignment, panel corners, focus retry behavior (up to 3 attempts with delays), makes items focusable, and simplifies item typography.

Changes

Cohort / File(s) Summary
QuickMenu component
app/src/main/java/app/gamenative/ui/component/QuickMenu.kt
Swapped slide animation to enter/exit from the left, changed alignment from CenterEnd→CenterStart and panel corners to topEnd/bottomEnd, added LaunchedEffect dependent on isVisible and menuItems.size, implemented up-to-3 focus retries with 80ms delays, added focusable to items, and simplified typography to base bodyLarge.
XServerScreen integration
app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
Added showQuickMenu / hasPhysicalController state and QuickMenu composable; replaced previous overlay/back handling to dismiss/show QuickMenu, added onQuickMenuItemSelected mapping menu actions to keyboard, input controls, profile/edit flows and exit logic, coordinated overlay pause/resume with QuickMenu visibility, and adjusted input/gamepad handling while menu is visible.

Sequence Diagram

sequenceDiagram
    participant User as "User"
    participant XServer as "XServerScreen\nrgba(0,128,255,0.5)"
    participant QuickMenu as "QuickMenu\nrgba(0,200,100,0.5)"
    participant Controller as "ControllerManager\nrgba(200,100,0,0.5)"
    participant App as "PluviaApp\nrgba(150,50,200,0.5)"

    User->>XServer: Press Back
    alt QuickMenu visible
        XServer->>QuickMenu: Dismiss()
        QuickMenu-->>XServer: onDismiss()
    else QuickMenu hidden
        XServer->>App: Pause overlay
        XServer->>Controller: Probe controllers
        Controller-->>XServer: Status
        alt Controller detected
            XServer->>QuickMenu: Show(hasPhysicalController=true)
            User->>QuickMenu: Navigate & Select(item)
            QuickMenu-->>XServer: onItemSelected(action)
            XServer->>XServer: Execute mapped action (toggle keyboard, open/close input controls, edit profiles, exit)
            XServer->>App: Resume overlay
        else No controller
            XServer->>XServer: Fallback flows (keyboard/other)
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped in quick to slide a panel bright,

From left it glided, soft in evening light,
I tapped for focus thrice with tiny paws,
Controllers hummed, the menu paused and thawed,
A bunny's little cheer — select, resume, delight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly relates to the main changeset objective: integrating a new QuickMenu UI component into XServerScreen with updated animations, focus handling, and menu-driven workflows for in-game interactions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt (3)

467-467: Clarify the profile fallback order.

The fallback chain manager?.getProfile(0) ?: profiles.getOrNull(2) ?: profiles.first() uses index 2 which appears arbitrary. Consider adding a comment explaining why profile at index 2 is preferred over others, or use a named constant if this represents a specific default profile.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt` at line
467, The fallback chain in XServerScreen using manager?.getProfile(0) ?:
profiles.getOrNull(2) ?: profiles.first() is unclear because
profiles.getOrNull(2) looks arbitrary; update this by replacing the magic index
with a named constant (e.g., DEFAULT_PROFILE_INDEX) or add a one-line comment
above the expression explaining why index 2 is preferred, and adjust any usages
of manager?.getProfile(0), profiles.getOrNull(2), or profiles.first()
accordingly so the intent (preferred default profile order) is explicit.

469-469: Consider guarding against null xServerView.

The non-null assertion xServerView!! could crash if this action is somehow triggered before the view is fully initialized. Although unlikely during normal operation, a defensive check would be safer.

🛡️ Suggested defensive check
-                        showInputControls(targetProfile, xServerView!!.getxServer().winHandler, container)
+                        val view = xServerView ?: return@let
+                        showInputControls(targetProfile, view.getxServer().winHandler, container)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt` at line
469, The call uses a non-null assertion on xServerView (xServerView!!), which
can crash if uninitialized; change to a safe null-check before calling
showInputControls — e.g., if (xServerView == null) return or log and return,
otherwise call showInputControls(xServerView.getxServer().winHandler,
targetProfile, container) (or use xServerView?.let {
showInputControls(targetProfile, it.getxServer().winHandler, container) }) so
showInputControls is only invoked when xServerView is non-null.

606-608: Consider moving controller scan off the main thread.

ControllerManager.scanForDevices() may perform I/O or device enumeration that could cause a brief UI stutter before the QuickMenu appears. Consider running this asynchronously or caching the result.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt` around
lines 606 - 608, The call to ControllerManager.scanForDevices() runs on the main
thread and can block UI; move the device enumeration to a background thread
(e.g., a coroutine on Dispatchers.IO or an executor) and only update
hasPhysicalController on the main thread afterward. Specifically, call
ControllerManager.getInstance().scanForDevices() off the UI thread, then call
controllerManager.getDetectedDevices().isNotEmpty() in the background and post
the result back to the main/UI thread to set hasPhysicalController so the
QuickMenu path does not stutter; ensure any exceptions during scan are
caught/logged and that repeated scans are guarded (e.g., debounce or cache) to
avoid repeated background work.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt`:
- Line 467: The fallback chain in XServerScreen using manager?.getProfile(0) ?:
profiles.getOrNull(2) ?: profiles.first() is unclear because
profiles.getOrNull(2) looks arbitrary; update this by replacing the magic index
with a named constant (e.g., DEFAULT_PROFILE_INDEX) or add a one-line comment
above the expression explaining why index 2 is preferred, and adjust any usages
of manager?.getProfile(0), profiles.getOrNull(2), or profiles.first()
accordingly so the intent (preferred default profile order) is explicit.
- Line 469: The call uses a non-null assertion on xServerView (xServerView!!),
which can crash if uninitialized; change to a safe null-check before calling
showInputControls — e.g., if (xServerView == null) return or log and return,
otherwise call showInputControls(xServerView.getxServer().winHandler,
targetProfile, container) (or use xServerView?.let {
showInputControls(targetProfile, it.getxServer().winHandler, container) }) so
showInputControls is only invoked when xServerView is non-null.
- Around line 606-608: The call to ControllerManager.scanForDevices() runs on
the main thread and can block UI; move the device enumeration to a background
thread (e.g., a coroutine on Dispatchers.IO or an executor) and only update
hasPhysicalController on the main thread afterward. Specifically, call
ControllerManager.getInstance().scanForDevices() off the UI thread, then call
controllerManager.getDetectedDevices().isNotEmpty() in the background and post
the result back to the main/UI thread to set hasPhysicalController so the
QuickMenu path does not stutter; ensure any exceptions during scan are
caught/logged and that repeated scans are guarded (e.g., debounce or cache) to
avoid repeated background work.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e0ce9e and 61dc8a6.

📒 Files selected for processing (2)
  • app/src/main/java/app/gamenative/ui/component/QuickMenu.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt`:
- Around line 452-474: The branch handling QuickMenuAction.INPUT_CONTROLS
toggles areControlsVisible unconditionally which can desync from the actual
InputControlsView when no show/hide action runs (e.g., profiles empty); change
the logic so areControlsVisible is flipped only when hideInputControls() or
showInputControls(...) actually executes: keep the hide path as-is (flip only
after hideInputControls()), and in the show path only call
showInputControls(...) and set areControlsVisible = true when a targetProfile is
found and showInputControls(...) is invoked (do not toggle when
profiles.isEmpty() or no targetProfile); use the existing symbols
QuickMenuAction.INPUT_CONTROLS, areControlsVisible, hideInputControls(),
showInputControls(...), PluviaApp.inputControlsManager, getProfiles/getProfile,
xServerView!!.getxServer().winHandler and container to locate and implement the
change.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 61dc8a6 and 721052f.

📒 Files selected for processing (1)
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt

Utkarsh Dalal and others added 2 commits March 1, 2026 19:56
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt (1)

452-474: ⚠️ Potential issue | 🟠 Major

Prevent areControlsVisible from drifting out of sync.

Line 473 toggles areControlsVisible even when no profile is available and showInputControls(...) never runs. This can desync state from actual UI visibility.

💡 Proposed fix
 QuickMenuAction.INPUT_CONTROLS -> {
+    var didToggle = false
     if (areControlsVisible) {
         PostHog.capture(event = "onscreen_controller_disabled")
         hideInputControls()
+        didToggle = true
     } else {
         PostHog.capture(event = "onscreen_controller_enabled")
         val manager = PluviaApp.inputControlsManager
         val profiles = manager?.getProfiles(false) ?: listOf()
         if (profiles.isNotEmpty()) {
             // Use current profile (custom or Profile 0)
             val profileIdStr = container.getExtra("profileId", "0")
             val profileId = profileIdStr.toIntOrNull() ?: 0
             val targetProfile = if (profileId != 0) {
                 manager?.getProfile(profileId)
             } else {
                 null
             } ?: manager?.getProfile(0) ?: profiles.getOrNull(2) ?: profiles.first()

             showInputControls(targetProfile, xServerView!!.getxServer().winHandler, container)
+            didToggle = true
         }
     }
-    areControlsVisible = !areControlsVisible
+    if (didToggle) {
+        areControlsVisible = !areControlsVisible
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt` around
lines 452 - 474, The handler for QuickMenuAction.INPUT_CONTROLS currently flips
areControlsVisible unconditionally which can desync when no input profile
exists; change the logic to only update areControlsVisible when the UI actually
changes: set areControlsVisible = false immediately after calling
hideInputControls(), and set areControlsVisible = true only after
showInputControls(...) successfully runs (i.e., inside the profiles.isNotEmpty()
branch where you call showInputControls with targetProfile and winHandler).
Reference symbols: QuickMenuAction.INPUT_CONTROLS, areControlsVisible,
hideInputControls(), showInputControls(...), PluviaApp.inputControlsManager,
manager.getProfiles(false), container.getExtra("profileId", ...),
xServerView!!.getxServer().winHandler.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/app/gamenative/ui/component/QuickMenu.kt`:
- Around line 239-248: The LaunchedEffect in QuickMenu.kt currently ignores the
Boolean result of firstItemFocusRequester.requestFocus() and always returns, so
retries only happen on exceptions; change the logic in the LaunchedEffect
(inside QuickMenu/LaunchedEffect) to capture the Boolean from
firstItemFocusRequester.requestFocus(), only exit the LaunchedEffect when it
returns true, and otherwise delay and retry (up to the existing repeat count),
while preserving the existing try/catch for exceptions that also trigger a
delay.

In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt`:
- Around line 476-479: The flag keepPausedForEditor is being set before
confirming the editor/dialog actually opened, causing the game to remain paused
if profile resolution fails; change the flow in the
QuickMenuAction.EDIT_CONTROLS handler (and the similar handler around the other
occurrence) so you first attempt to resolve the profile and open the controls
editor/dialog (call that resolution/open function used in the existing code),
check its success result or thrown exception, and only set keepPausedForEditor =
true after the open call returns success; if open fails, ensure you do not set
the flag and run the existing dismiss/resume logic so gameplay continues.
- Line 470: The QuickMenu action handlers currently use the non-null asserted
xServerView!! (e.g., the call to showInputControls(targetProfile,
xServerView!!.getxServer().winHandler, container) and similar uses at the other
handlers), which can NPE if the view isn’t ready; change each handler to safely
handle a null xServerView by either early-returning or disabling the action when
xServerView is null, and use safe calls to access getxServer() and winHandler
(for example, guard with if (xServerView == null) { /* noop or show error */;
return } or use xServerView?.let { vs -> showInputControls(targetProfile,
vs.getxServer().winHandler, container) }), updating all occurrences (the
handlers that reference xServerView, showInputControls, getxServer, and
winHandler) so the UI cannot crash when the menu is tapped before
initialization.
- Around line 636-639: The quick menu branch currently returns false in the
onKeyEvent and onMotionEvent handlers (when showQuickMenu && isGamepad), which
lets Android continue dispatching events to MainActivity and gameplay; change
those branches in XServerScreen's onKeyEvent and onMotionEvent to return true to
mark the events consumed so they do not propagate further. Locate the
conditional using showQuickMenu && isGamepad in the onKeyEvent and onMotionEvent
functions and replace the false return with true while keeping the surrounding
comment about Compose focus handling.

---

Duplicate comments:
In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt`:
- Around line 452-474: The handler for QuickMenuAction.INPUT_CONTROLS currently
flips areControlsVisible unconditionally which can desync when no input profile
exists; change the logic to only update areControlsVisible when the UI actually
changes: set areControlsVisible = false immediately after calling
hideInputControls(), and set areControlsVisible = true only after
showInputControls(...) successfully runs (i.e., inside the profiles.isNotEmpty()
branch where you call showInputControls with targetProfile and winHandler).
Reference symbols: QuickMenuAction.INPUT_CONTROLS, areControlsVisible,
hideInputControls(), showInputControls(...), PluviaApp.inputControlsManager,
manager.getProfiles(false), container.getExtra("profileId", ...),
xServerView!!.getxServer().winHandler.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 721052f and 2ff602f.

📒 Files selected for processing (2)
  • app/src/main/java/app/gamenative/ui/component/QuickMenu.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt

Comment on lines +239 to +248
LaunchedEffect(isVisible, menuItems.size) {
if (isVisible) {
try {
firstItemFocusRequester.requestFocus()
} catch (_: Exception) {
// Focus request may fail if composition is not ready
repeat(3) {
try {
firstItemFocusRequester.requestFocus()
return@LaunchedEffect
} catch (_: Exception) {
// Focus request may fail if composition is not ready.
delay(80)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

FocusRequester.requestFocus() Jetpack Compose return value boolean failure

💡 Result:

FocusRequester.requestFocus() does return a Boolean (in newer Compose it’s fun requestFocus(...): Boolean). It returns true if focus was successfully requested, or false if the focus request was canceled. [1]

What “failure” usually means in practice:

  • The request was canceled by focus rules (for example, focusProperties { enter/exit = Cancel }, or the target can’t be focused / isn’t focusable). In that case you’ll typically just get false. [1][2]
  • The FocusRequester isn’t attached to any node yet (you forgot Modifier.focusRequester(...), the modifier order is wrong, or you’re calling it before the node exists—common with subcomposition like Scaffold). That often throws IllegalStateException: FocusRequester is not initialized… rather than returning false. [3]
  • You called it at the wrong time (during composition / before layout). Google’s guidance is to request focus in response to an event or from a side-effect (LaunchedEffect), and ensure the requester is associated first. [2][4]

How to debug/handle it:

  • Check the boolean:
    val ok = focusRequester.requestFocus()
  • Also observe what actually happened (since false doesn’t tell you why):
    Modifier.onFocusChanged { state -> /* state.isFocused */ }
  • Ensure the basics:
    • val focusRequester = remember { FocusRequester() } [3]
    • target has Modifier.focusRequester(focusRequester) and is actually focusable (e.g., TextField, or add focusTarget()/focusable() where appropriate) [2]
    • call requestFocus() from LaunchedEffect/event after the node is in the hierarchy (and inside Scaffold content if Scaffold is involved). [3]

Sources: [1] FocusRequester API (requestFocus returns Boolean, false if canceled) (developer.android.com), [2] “Change focus behavior” (how to request focus; focus properties can prevent focus) (developer.android.com), [3] IllegalStateException: FocusRequester is not initialized… explanation and common fixes (stackoverflow.com), [4] Compose focus codelab warning that calling requestFocus() without being associated can crash (developer.android.com).

Citations:


🏁 Script executed:

# Search for the QuickMenu.kt file and examine the code around lines 239-248
find . -name "QuickMenu.kt" -type f

Repository: utkarshdalal/GameNative

Length of output: 127


🏁 Script executed:

cat -n app/src/main/java/app/gamenative/ui/component/QuickMenu.kt | sed -n '235,255p'

Repository: utkarshdalal/GameNative

Length of output: 703


Check the Boolean return value of requestFocus() to properly retry focus requests.

At line 243–244, requestFocus() returns a Boolean but the code ignores it and always returns immediately. This skips retries when focus is denied (returns false) without throwing an exception. Only exception-triggered retries work, leaving cancellations unhandled.

Proposed fix
 LaunchedEffect(isVisible, menuItems.size) {
     if (isVisible) {
         repeat(3) {
             try {
-                firstItemFocusRequester.requestFocus()
-                return@LaunchedEffect
+                val focused = firstItemFocusRequester.requestFocus()
+                if (focused) {
+                    return@LaunchedEffect
+                }
             } catch (_: Exception) {
                 // Focus request may fail if composition is not ready.
-                delay(80)
             }
+            delay(80)
         }
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
LaunchedEffect(isVisible, menuItems.size) {
if (isVisible) {
try {
firstItemFocusRequester.requestFocus()
} catch (_: Exception) {
// Focus request may fail if composition is not ready
repeat(3) {
try {
firstItemFocusRequester.requestFocus()
return@LaunchedEffect
} catch (_: Exception) {
// Focus request may fail if composition is not ready.
delay(80)
}
LaunchedEffect(isVisible, menuItems.size) {
if (isVisible) {
repeat(3) {
try {
val focused = firstItemFocusRequester.requestFocus()
if (focused) {
return@LaunchedEffect
}
} catch (_: Exception) {
// Focus request may fail if composition is not ready.
}
delay(80)
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/ui/component/QuickMenu.kt` around lines 239
- 248, The LaunchedEffect in QuickMenu.kt currently ignores the Boolean result
of firstItemFocusRequester.requestFocus() and always returns, so retries only
happen on exceptions; change the logic in the LaunchedEffect (inside
QuickMenu/LaunchedEffect) to capture the Boolean from
firstItemFocusRequester.requestFocus(), only exit the LaunchedEffect when it
returns true, and otherwise delay and retry (up to the existing repeat count),
while preserving the existing try/catch for exceptions that also trigger a
delay.

NavigationDialog.ACTION_EDIT_CONTROLS -> {
PostHog.capture(event = "edit_controls_in_game")
keepPausedForEditor = true
showInputControls(targetProfile, xServerView!!.getxServer().winHandler, container)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid !! in QuickMenu action handlers to prevent crash-on-tap paths.

Lines 470, 545, and 573 use xServerView!!. If the menu is opened before xServerView is ready, selecting those actions can crash with NPE.

💡 Proposed fix
 val onQuickMenuItemSelected: (Int) -> Unit = { itemId ->
+    val winHandler = xServerView?.getxServer()?.winHandler
     when (itemId) {
         QuickMenuAction.INPUT_CONTROLS -> {
             ...
-            showInputControls(targetProfile, xServerView!!.getxServer().winHandler, container)
+            if (winHandler != null) {
+                showInputControls(targetProfile, winHandler, container)
+            }
         }
         QuickMenuAction.EDIT_CONTROLS -> {
             ...
-            showInputControls(activeProfile, xServerView!!.getxServer().winHandler, container)
+            if (winHandler != null) {
+                showInputControls(activeProfile, winHandler, container)
+            }
         }
         QuickMenuAction.EXIT_GAME -> {
             ...
-            exit(xServerView!!.getxServer().winHandler, PluviaApp.xEnvironment, frameRating, currentAppInfo, container, onExit, navigateBack)
+            exit(winHandler, PluviaApp.xEnvironment, frameRating, currentAppInfo, container, onExit, navigateBack)
         }
     }
 }

Also applies to: 545-545, 573-573

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt` at line
470, The QuickMenu action handlers currently use the non-null asserted
xServerView!! (e.g., the call to showInputControls(targetProfile,
xServerView!!.getxServer().winHandler, container) and similar uses at the other
handlers), which can NPE if the view isn’t ready; change each handler to safely
handle a null xServerView by either early-returning or disabling the action when
xServerView is null, and use safe calls to access getxServer() and winHandler
(for example, guard with if (xServerView == null) { /* noop or show error */;
return } or use xServerView?.let { vs -> showInputControls(targetProfile,
vs.getxServer().winHandler, container) }), updating all occurrences (the
handlers that reference xServerView, showInputControls, getxServer, and
winHandler) so the UI cannot crash when the menu is tapped before
initialization.

Comment on lines +476 to 479
QuickMenuAction.EDIT_CONTROLS -> {
PostHog.capture(event = "edit_controls_in_game")
keepPausedForEditor = true

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Gate keepPausedForEditor on successful editor/dialog entry.

keepPausedForEditor is set to true before confirming edit UI can actually open. If no profile can be resolved, dismiss logic won’t resume the game because it thinks an editor is active.

💡 Proposed fix
 QuickMenuAction.EDIT_CONTROLS -> {
     PostHog.capture(event = "edit_controls_in_game")
-    keepPausedForEditor = true
+    keepPausedForEditor = false
     ...
     if (activeProfile != null) {
+        keepPausedForEditor = true
         ...
     }
 }

 QuickMenuAction.EDIT_PHYSICAL_CONTROLLER -> {
     PostHog.capture(event = "edit_physical_controller_from_menu")
-    keepPausedForEditor = true
+    // Set true only after profile/dialog preconditions are satisfied.
     showPhysicalControllerDialog = true
 }

Also applies to: 551-555

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt` around
lines 476 - 479, The flag keepPausedForEditor is being set before confirming the
editor/dialog actually opened, causing the game to remain paused if profile
resolution fails; change the flow in the QuickMenuAction.EDIT_CONTROLS handler
(and the similar handler around the other occurrence) so you first attempt to
resolve the profile and open the controls editor/dialog (call that
resolution/open function used in the existing code), check its success result or
thrown exception, and only set keepPausedForEditor = true after the open call
returns success; if open fails, ensure you do not set the flag and run the
existing dismiss/resume logic so gameplay continues.

Comment on lines +636 to +639
if (showQuickMenu && isGamepad) {
// Let Compose focus system handle gamepad navigation/selection while menu is visible.
false
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the XServerScreen.kt file around the mentioned lines
fd XServerScreen.kt --type f -x cat -n {} | head -800 | tail -200

Repository: utkarshdalal/GameNative

Length of output: 11315


🏁 Script executed:

# Let's also search for dispatchKeyEvent and dispatchGenericMotionEvent patterns
rg -nP --type=kt 'dispatchKeyEvent|dispatchGenericMotionEvent' -C 5

Repository: utkarshdalal/GameNative

Length of output: 93


🏁 Script executed:

# Check AndroidEvent enum/class structure
rg -nP --type=kt 'sealed.*class AndroidEvent|object AndroidEvent|AndroidEvent\.(KeyEvent|MotionEvent)'

Repository: utkarshdalal/GameNative

Length of output: 93


🏁 Script executed:

# Look for how the return value from this method is actually used
rg -nP --type=kt 'onKeyEvent|onMotionEvent' -C 5

Repository: utkarshdalal/GameNative

Length of output: 93


🏁 Script executed:

# Search for PluviaApp.events definition and how it handles Boolean returns
rg -n 'PluviaApp\.events' | head -20

Repository: utkarshdalal/GameNative

Length of output: 3256


🏁 Script executed:

# Find the events class/object definition
fd . --type f -name '*.kt' | xargs grep -l 'class.*events\|object.*events' | head -20

Repository: utkarshdalal/GameNative

Length of output: 297


🏁 Script executed:

# Look for EventBus or similar patterns
rg -n 'EventBus\|events\.on\|events\.off' -A 3 | head -100

Repository: utkarshdalal/GameNative

Length of output: 49


🏁 Script executed:

# Check the AndroidEvent class structure
fd . --type f -name '*.kt' | xargs grep -l 'sealed.*AndroidEvent\|class AndroidEvent'

Repository: utkarshdalal/GameNative

Length of output: 299


🏁 Script executed:

# Find PluviaApp definition
fd PluviaApp --type f | head -5

Repository: utkarshdalal/GameNative

Length of output: 112


🏁 Script executed:

# Search for class PluviaApp or object PluviaApp
grep -r "class PluviaApp\|object PluviaApp" --include="*.kt" | head -10

Repository: utkarshdalal/GameNative

Length of output: 157


🏁 Script executed:

# Look for the events property in PluviaApp
grep -r "val events\|var events" --include="*.kt" | grep -i app | head -20

Repository: utkarshdalal/GameNative

Length of output: 168


🏁 Script executed:

# Find EventBus or similar event system implementation
fd EventBus --type f

Repository: utkarshdalal/GameNative

Length of output: 49


🏁 Script executed:

# Search for event bus/emitter implementation patterns
grep -r "fun emit\|fun on<\|fun off<" --include="*.kt" | head -30

Repository: utkarshdalal/GameNative

Length of output: 252


🏁 Script executed:

# Read EventDispatcher implementation
cat app/src/main/java/app/gamenative/events/EventDispatcher.kt

Repository: utkarshdalal/GameNative

Length of output: 3221


🏁 Script executed:

# Find where AndroidEvent.KeyEvent and MotionEvent are emitted
grep -r "emit.*AndroidEvent\.KeyEvent\|emit.*AndroidEvent\.MotionEvent" --include="*.kt" --include="*.java"

Repository: utkarshdalal/GameNative

Length of output: 353


🏁 Script executed:

# Look for emit calls with resultAggregator
grep -r "emit.*resultAggregator" --include="*.kt" --include="*.java"

Repository: utkarshdalal/GameNative

Length of output: 249


🏁 Script executed:

# Find where these events originate - look for dispatchKeyEvent in XServerView or similar
grep -r "dispatchKeyEvent\|dispatchGenericMotionEvent" --include="*.kt" --include="*.java" | head -20

Repository: utkarshdalal/GameNative

Length of output: 1224


🏁 Script executed:

# Search for AndroidEvent definition to understand the event types
grep -r "sealed.*class AndroidEvent\|sealed interface AndroidEvent" --include="*.kt" -A 20

Repository: utkarshdalal/GameNative

Length of output: 49


🏁 Script executed:

# Read MainActivity.kt to find the resultAggregator logic
grep -B 5 -A 15 'emit(AndroidEvent.KeyEvent' app/src/main/java/app/gamenative/MainActivity.kt

Repository: utkarshdalal/GameNative

Length of output: 989


🏁 Script executed:

# Also get the MotionEvent emit logic
grep -B 5 -A 15 'emit(AndroidEvent.MotionEvent' app/src/main/java/app/gamenative/MainActivity.kt

Repository: utkarshdalal/GameNative

Length of output: 915


🏁 Script executed:

# Check if there's any Compose focus handling for the QuickMenu that might consume the input
grep -r "QuickMenu\|showQuickMenu" app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt -B 3 -A 3 | head -80

Repository: utkarshdalal/GameNative

Length of output: 2809


Gamepad input may still reach gameplay handlers when QuickMenu is open.

When showQuickMenu && isGamepad is true, returning false signals the event was not consumed to MainActivity. MainActivity then calls super.dispatchKeyEvent(), allowing the event to continue in the Android dispatch chain and potentially reach other handlers including gameplay code. Returning false relies on Compose's focus system to consume the input, but that operates separately from Android's key event dispatch. To guarantee QuickMenu-only handling, return true to consume the event and prevent further propagation.

This applies to both onKeyEvent (lines 636–639) and onMotionEvent (lines 656–659).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt` around
lines 636 - 639, The quick menu branch currently returns false in the onKeyEvent
and onMotionEvent handlers (when showQuickMenu && isGamepad), which lets Android
continue dispatching events to MainActivity and gameplay; change those branches
in XServerScreen's onKeyEvent and onMotionEvent to return true to mark the
events consumed so they do not propagate further. Locate the conditional using
showQuickMenu && isGamepad in the onKeyEvent and onMotionEvent functions and
replace the false return with true while keeping the surrounding comment about
Compose focus handling.

@utkarshdalal utkarshdalal merged commit af08614 into utkarshdalal:master Mar 2, 2026
3 checks passed
@utkarshdalal utkarshdalal deleted the feat/xserver-quickmenu-sidepanel branch March 2, 2026 04:41
ObfuscatedVoid pushed a commit to ObfuscatedVoid/GameNative that referenced this pull request Mar 4, 2026
…alal#686)

* ui(xserver): switch in-game back menu to QuickMenu side panel

* feat: add controller driven nav to new opt menu

* fix: dont bold focused text to avoid wrapping

* chore: akshually left is better

* fixed button presses and pausing in new sidebar

---------

Co-authored-by: Utkarsh Dalal <utkarsh.dalal@toptal.com>
Co-authored-by: Utkarsh Dalal <dalal.utkarsh@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants