Skip to content

[Feature] Long-Press Start to Toggle Mouse-Mode - #272

Open
mpotrykus wants to merge 5 commits into
TheElixZammuto:masterfrom
mpotrykus:hold-start-mouse
Open

[Feature] Long-Press Start to Toggle Mouse-Mode#272
mpotrykus wants to merge 5 commits into
TheElixZammuto:masterfrom
mpotrykus:hold-start-mouse

Conversation

@mpotrykus

@mpotrykus mpotrykus commented May 5, 2026

Copy link
Copy Markdown
Contributor

Saw a mention on #185 about how the xbox version does not have the "long press start to toggle mouse mode" and realized I also miss that.


This PR adds:

  • Long-press start to toggle mouse-mode
  • Toast service for StreamPage
  • Input fix for combo-handling? (testing)

Summary by CodeRabbit

  • New Features
    • Added toast notification system displaying temporary status messages at the bottom of the stream page with smooth fade-in/fade-out animations.
    • Implemented menu button long-press functionality (600ms configurable) to toggle mouse mode with visual confirmation via toast notification feedback.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR implements Menu button long-press detection with toast notification feedback and mouse mode toggling. The gamepad combo state machine gains a MenuLongPressWaiting state with injection timing logic; StreamPage adds a toast notification UI with animation; ProcessInput integrates the long-press trigger to toggle mouse mode and display transient feedback.

Changes

Menu Long-Press Gamepad Combo with Toast

Layer / File(s) Summary
State Machine Data Structures
State/GamepadState.h
ComboState adds MenuLongPressWaiting; GamepadComboState adds menuLongPressFired and QPC timestamps for menuInjectionStartQpc and viewInjectionStartQpc; ComboResult adds menuLongPressTriggered flag; GetComboResult signature accepts optional menuLongPressMs parameter (default 600).
State Machine Implementation
State/GamepadState.h
Reset() initializes new combo-tracking fields; GetComboResult logic now records injection start timestamps on transitions, sustains injected taps for reliability, transitions to MenuLongPressWaiting when Menu held past comboTimeoutMs, and triggers injection on Menu release before long-press fires.
Toast Notification UI Definition
Pages/StreamPage.xaml
Adds ToastStoryboard resource animating ToastView opacity with easing and completion handler; adds bottom-anchored ToastView Border (initially Collapsed) with dim gray background and embedded ToastText TextBlock.
Toast Implementation and API
Pages/StreamPage.xaml.h, Pages/StreamPage.xaml.cpp
Exposes public ShowToast(message) and SetMouseMode(enabled) methods, getter properties m_toastView and m_toastText; implements ShowToast to update text, show view, and run animation; implements ToastStoryboard_Completed handler to hide view.
Input Processing Integration
Streaming/moonlight_xbox_dxMain.cpp
ProcessInput() checks menuLongPressTriggered; on trigger, toggles mouse mode via SetMouseMode, shows toast with "Mouse Mode: On/Off", clears input packet, and continues to next gamepad.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

A long-press Menu summons the mouse mode's dance,
While toast floats up to show the circumstance,
Injection timing guides the button's way,
State machines waltz through night and day—
🎮✨ Rabbit hops with joy at the control flow's play!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature addition: a long-press Start button to toggle mouse mode, which is the primary objective across all changed files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

@mpotrykus

mpotrykus commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

@andygrundman I THINK I might have found an improvement for combo-handling while creating this (ok, claude found the bug, I fixed it).

With this, I am able to spam 'select' or 'start' as quickly as I want, without interfering with the combos.

Still testing, but thought you might be interested.

@mpotrykus mpotrykus changed the title Hold start mouse [Feat] Long-Press Start to Toggle Mouse-Mode May 5, 2026
@mpotrykus mpotrykus changed the title [Feat] Long-Press Start to Toggle Mouse-Mode [Feature] Long-Press Start to Toggle Mouse-Mode May 5, 2026
@andygrundman

Copy link
Copy Markdown
Collaborator

This gets a big fat no from me, I hate long press start. Using the menu is much faster and more elegant. I also think that overloading hold-start is more risky than only taking select+start.

I'm adding the same select+start menu to moonlight-qt in my Mac branch, it will have an ImGui similar popup menu because it's so handy.

@mpotrykus

mpotrykus commented May 6, 2026

Copy link
Copy Markdown
Contributor Author

Using the menu is much faster and more elegant.

I would argue the opposite. The rest, I'll leave to preference.

That being said. You should at least review the input fixes to improve your current select+start handling.

@andygrundman

Copy link
Copy Markdown
Collaborator

Is there something wrong with the current 50ms select+start handling? I can't tell in this diff because it's mostly about the start button. I also don't understand what this comment means at all: "Sustain any injected taps for 100ms so the host gets reliable frame coverage"

@mpotrykus

Copy link
Copy Markdown
Contributor Author

The fix does two things:

  • Some presses in combo-handling could be missed between frames, this sustains them until the next read
  • Clear the state on a new press, so combos are non-blocking

With the Select+Start, there was the issue where it was blocking those keys until the poll length was filled. That's how we ended up on 50ms, because it was short enough to work as a combo, but not enough to hinder the actual use of the button. But this does still add a 50ms blocker on those buttons. Not a big deal, but it's there.

So far from testing, this fix completely removes the block from combo keys and allows you to spam them as quickly as you want (why you would want to spam select or start is beyond me, but to each their own).

So no, the current handling is fine. This is just an improvement to make it non-blocking.

@andygrundman

Copy link
Copy Markdown
Collaborator

You have to block the buttons in order to use them in a combo, don't you? What does non-blocking mean in this context? I don't want to allow a start press to get sent to the host until I'm sure there is no associated select button press. You're right there is no reason to spam these 2 buttons which is why they're a good choice.

In qt I've implemented this using ImGui's button handling, maybe there is something to be learned from the way it works there.

if (ImGui::Shortcut(ImGuiKey_GamepadStart | ImGuiKey_GamepadBack, ImGuiInputFlags_RouteGlobal) {
    panelOpen = !panelOpen;
}
if (ImGui::Shortcut(ImGuiKey_Space | ImGuiMod_Ctrl | ImGuiMod_Alt | ImGuiMod_Shift, ImGuiInputFlags_RouteGlobal)) {
    panelOpen = !panelOpen;
}

There is one big difference: in ImGui this check is made inline on the render thread which runs at various framerates. Possibly as low as 16fps or at max refresh rate in display-locked mode. On Xbox, the input thread runs independently and always at 500fps. This is why it's so difficult to get your 2 buttons to hit on the same "frame". I need to use the qt version a lot more to see how it feels when done the ImGui way. It doesn't do anything to hold buttons but the lower framerate may make it easier to hit both together. I also added Ctrl-Alt-Shift-Space to bring up the quick menu and as with any of the 4-button combos this is likely to send the first keys to the host. No one has tried this yet so I don't know how it will feel to people.

@mpotrykus

Copy link
Copy Markdown
Contributor Author

You do need to wait, but you do not need to block.

This new handling will:

  • see "first of expected combo hit...listen"
  • timeout limit reached?
    • combo not achieved, inject original button press back to host
  • new button hit. Does "new button" complete combo?
    • yes: fire combo
    • no: unblock and send "new button"

So all input is now sent "as expected", but the listening is still there to achieve a combo. If the combo is achieved, no buttons are sent, except the combo. Like we want.

@mpotrykus

Copy link
Copy Markdown
Contributor Author

Been running games with heavy reliance on start/select with no blocking issues. I'm going to set this PR as active, but up to you all if you actually want the feature. Let me know if you want me to separate the combo-input fix to another PR.

@mpotrykus
mpotrykus marked this pull request as ready for review May 10, 2026 11:37

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
State/GamepadState.h (1)

357-368: 💤 Low value

Optional: include the new combo fields in DumpState.

DumpState still only logs comboState, viewPressed, menuPressed, startTime. Given the new long-press / injection-window logic is timing-sensitive and tricky to reason about (per the PR thread), adding menuLongPressFired, menuInjectionStartQpc, and viewInjectionStartQpc to the log line will pay off when a user reports a misfire.

♻️ Suggested addition
 		moonlight_xbox_dx::Utils::Logf(
-		    "GamepadState[localId: %d, hostId: %d] buttons: %s %s axes: %d %d, %d %d, triggers: %d %d, combo{ state: %d, viewPressed: %d, menuPressed: %d, startTime: %d }\n",
+		    "GamepadState[localId: %d, hostId: %d] buttons: %s %s axes: %d %d, %d %d, triggers: %d %d, combo{ state: %d, viewPressed: %d, menuPressed: %d, startTime: %lld, menuLongPressFired: %d, menuInj: %lld, viewInj: %lld }\n",
 		    localId, hostId,
 		    buttons,
 		    isGuideButtonDown.load() ? "Guide" : "",
 		    ltX, ltY, rtX, rtY,
 		    lTrig, rTrig,
-		    combo.comboState, combo.viewPressed, combo.menuPressed, combo.startTime);
+		    combo.comboState, combo.viewPressed, combo.menuPressed,
+		    (long long)combo.startTime, combo.menuLongPressFired,
+		    (long long)combo.menuInjectionStartQpc, (long long)combo.viewInjectionStartQpc);
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@State/GamepadState.h` around lines 357 - 368, DumpState currently logs only
combo.comboState, combo.viewPressed, combo.menuPressed, and combo.startTime;
update the moonlight_xbox_dx::Utils::Logf call inside DumpState to also include
combo.menuLongPressFired, combo.menuInjectionStartQpc, and
combo.viewInjectionStartQpc by adding corresponding format specifiers to the
format string and passing those fields as additional arguments after
combo.startTime so the new long-press/injection-window state is visible when
dumping the GamepadState.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Pages/StreamPage.xaml`:
- Around line 115-126: ToastView is positioned with a fixed Margin="0,56" and
VerticalAlignment="Bottom" so it can be clipped by TV overscan; update the
layout to honor State.ScreenMargin by either moving ToastView inside the
SwapChainPanel or applying Margin="{x:Bind State.ScreenMargin}" to ToastView
(and then keep any extra 56px offset inside an inner element if you still want a
small buffer). Refer to the ToastView element, the SwapChainPanel sibling, and
State.ScreenMargin when making the change.

---

Nitpick comments:
In `@State/GamepadState.h`:
- Around line 357-368: DumpState currently logs only combo.comboState,
combo.viewPressed, combo.menuPressed, and combo.startTime; update the
moonlight_xbox_dx::Utils::Logf call inside DumpState to also include
combo.menuLongPressFired, combo.menuInjectionStartQpc, and
combo.viewInjectionStartQpc by adding corresponding format specifiers to the
format string and passing those fields as additional arguments after
combo.startTime so the new long-press/injection-window state is visible when
dumping the GamepadState.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b05ba62e-007b-4933-826e-59f886212df9

📥 Commits

Reviewing files that changed from the base of the PR and between 6beda9f and ab41e66.

📒 Files selected for processing (5)
  • Pages/StreamPage.xaml
  • Pages/StreamPage.xaml.cpp
  • Pages/StreamPage.xaml.h
  • State/GamepadState.h
  • Streaming/moonlight_xbox_dxMain.cpp

Comment thread Pages/StreamPage.xaml
Comment on lines +115 to +126
<Border x:Name="ToastView"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Margin="0,56"
Padding="20,10,20,10"
CornerRadius="6"
Visibility="Collapsed">
<Border.Background>
<SolidColorBrush Color="DimGray" />
</Border.Background>
<TextBlock x:Name="ToastText" Foreground="White" FontSize="12" />
</Border>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Toast may be clipped by TV overscan — ToastView doesn't honor State.ScreenMargin.

ToastView is a sibling of SwapChainPanel inside the outer Grid, but only SwapChainPanel applies Margin="{x:Bind State.ScreenMargin}". With VerticalAlignment="Bottom" and Margin="0,56", the toast sits 56 px from the raw window edge. On Xbox displays with overscan that's roughly 54 px per side at 1080p and ~108 px at 4K — so on 4K TVs with default overscan the toast is likely clipped.

Either move ToastView inside SwapChainPanel or wrap the contents of the outer Grid with the screen margin.

♻️ Suggested fix (apply title-safe margin to ToastView)
-        <Border x:Name="ToastView"
-                HorizontalAlignment="Center"
-                VerticalAlignment="Bottom"
-                Margin="0,56"
-                Padding="20,10,20,10"
-                CornerRadius="6"
-                Visibility="Collapsed">
+        <Border x:Name="ToastView"
+                HorizontalAlignment="Center"
+                VerticalAlignment="Bottom"
+                Margin="{x:Bind State.ScreenMargin}"
+                Padding="20,10,20,10"
+                CornerRadius="6"
+                Visibility="Collapsed">
             <Border.Background>
                 <SolidColorBrush Color="DimGray" />
             </Border.Background>
             <TextBlock x:Name="ToastText" Foreground="White" FontSize="12" />
         </Border>

(If you still want a small offset above the safe-area edge, wrap the toast text in another inner element with the 56 px bottom padding.)

📝 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
<Border x:Name="ToastView"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Margin="0,56"
Padding="20,10,20,10"
CornerRadius="6"
Visibility="Collapsed">
<Border.Background>
<SolidColorBrush Color="DimGray" />
</Border.Background>
<TextBlock x:Name="ToastText" Foreground="White" FontSize="12" />
</Border>
<Border x:Name="ToastView"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Margin="{x:Bind State.ScreenMargin}"
Padding="20,10,20,10"
CornerRadius="6"
Visibility="Collapsed">
<Border.Background>
<SolidColorBrush Color="DimGray" />
</Border.Background>
<TextBlock x:Name="ToastText" Foreground="White" FontSize="12" />
</Border>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Pages/StreamPage.xaml` around lines 115 - 126, ToastView is positioned with a
fixed Margin="0,56" and VerticalAlignment="Bottom" so it can be clipped by TV
overscan; update the layout to honor State.ScreenMargin by either moving
ToastView inside the SwapChainPanel or applying Margin="{x:Bind
State.ScreenMargin}" to ToastView (and then keep any extra 56px offset inside an
inner element if you still want a small buffer). Refer to the ToastView element,
the SwapChainPanel sibling, and State.ScreenMargin when making the change.

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