mirror of
https://github.com/microsoft/microsoft-ui-xaml.git
synced 2026-10-28 08:25:50 +08:00
Add opt-in GamepadKeyRouting XAML optional change for gamepad navigation (#11853)
## Fixes Closes #63117176 https://task.ms/63117176 ## PR Type - [ ] Bugfix - [x] Feature - [ ] Code style update (formatting, renaming) - [ ] Refactoring (no functional changes, no API changes) - [ ] Build related changes - [ ] Documentation content changes - [ ] Other (please describe): ## Description Adds an opt-in `GamepadKeyRouting` XAML optional change that lets WinUI 3 apps respond to gamepad input using XAML's existing keyboard navigation, restoring behavior that System XAML provided out of the box. When enabled, `Application.Start()` calls the OS API `Windows.UI.Input.GamepadKeyRoutingConfiguration.TrySetKeyRoutingEnabled(true)`, which routes gamepad input into the process as keyboard virtual keys. XY focus navigation, Enter/Space to invoke, and Esc to dismiss then work from a gamepad with no app-specific code. Because the OS API requires UniversalApiContract 19 (Windows 11, 24H2) — newer than the 17763 downlevel floor this repo targets — the call lives in a new light-up translation unit (`GamepadKeyRoutingLightup.cpp`) compiled without the PCH and with the contract version undefined, following the existing `DXamlCoreLightup.cpp` pattern. ### Current Behavior A WinUI 3 app receives no keyboard-equivalent input from a gamepad. Gamepad navigation that worked in System XAML requires apps to implement their own input handling. There is no `XamlChangeId` for gamepad routing. ### New Behavior `Microsoft.UI.Xaml.Settings.XamlChangeId` gains a `GamepadKeyRouting` value (`63117108`). It is off by default. Apps opt in either via the project property: <EnabledXamlOptionalChanges>GamepadKeyRouting</EnabledXamlOptionalChanges> or by calling `XamlOptionalChanges.EnableChange(XamlChangeId.GamepadKeyRouting)` before `Application.Start()`. When enabled, `FrameworkApplicationFactory::StartImpl` enables routing for the process. Applied only when the windowing model is `ClassicDesktop` or `Universal` — i.e. a full WinUI app. The OS setting is process-wide, so XAML islands deliberately never reach this call and stay at the OS default. Apps can opt back out at runtime via `TrySetKeyRoutingEnabled(false)` after `Application.Start()`. Failure is non-fatal at every step. The code gates on `ApiInformation` (contract present, type present), then `IsSupported()`, then honors the `TrySetKeyRoutingEnabled` return value. On downlevel OS, unsupported hardware, or OS refusal it returns `S_OK` and the app runs normally without gamepad navigation. Also included: - `Application.Start()` now locks the optional-changes set before reading it, so app code cannot alter selected behavior once startup has begun. - Windows SDK for C++ projects bumped `10.0.22621.755` → `10.0.26100.9169` across all `packages*.config`, required for the contract 19 headers. ## Customer Impact User-facing and additive. Gamepad users get working navigation with no code which is a gap closed versus System XAML. Gracefully degrades: an opted-in app on Windows earlier than 11 24H2, or on hardware where the API is unsupported, behaves exactly as it does today. ## Regression Potential - [ ] Low risk — isolated change, limited scope - [x] Medium risk — touches shared components or public APIs - [ ] High risk — architectural or breaking API change 1. **Windows SDK bump** (`10.0.22621.755` → `10.0.26100.9169`) 1. **`Application.Start()` startup path** is shared by every WinUI app. Mitigating: the new OS call is confined to a separate TU, reached only when opted in, and every failure path returns `S_OK`. Test infra enables `GamepadKeyRouting` by default for test runs, so existing suites exercise the enabled path. ## How Has This Been Tested? - Manually validated end-to-end with WinUI Gallery deployed as an MSIX with `<EnabledXamlOptionalChanges>GamepadKeyRouting</EnabledXamlOptionalChanges>`: gamepad navigation confirmed working. - Tested on an older OS without Windows.UI.Input.GamepadKeyRoutingConfiguration. WinUIGallery app works as expected without gamepad navigation enabled. - Built and verified the opposite configuration (`<DisabledXamlOptionalChanges>`), confirming the XAML compiler emits `XamlOptionalChanges.DisableChange(XamlChangeId.GamepadKeyRouting)` into the generated `Main()` and that the app runs normally with routing off. - [x] I have performed a self-review of my own code - [ ] I have added tests to cover my changes - [x] Existing tests builds, will run a test pass on the PR
This commit is contained in:
1 parent
c35ac99f88
commit
5b75cd05e6
22 files changed
+363
-68
No files matched your search
+81
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
#include "pch.h"
|
||||
#include "GamepadKeyRoutingState.h"
|
||||
|
||||
// GamepadKeyRoutingConfiguration ships in UniversalApiContract 19, which this app's C++/WinRT
|
||||
// projection does not know about: samples intentionally build against the 17763 contract metadata
|
||||
// (see eng\lightup.targets). The Windows SDK ABI headers are not subject to that pin and do
|
||||
// declare the type, so this talks to the ABI interface directly. Those headers also introduce a
|
||||
// global ::Windows namespace that collides with the using-directives in MainWindow.cpp, which is
|
||||
// why this lives in its own translation unit.
|
||||
#undef WINDOWS_FOUNDATION_UNIVERSALAPICONTRACT_VERSION
|
||||
#include <roapi.h>
|
||||
#include <windows.foundation.metadata.h>
|
||||
#include <windows.ui.input.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
winrt::hstring RuntimeClassName(wchar_t const* name)
|
||||
{
|
||||
return winrt::hstring{ name };
|
||||
}
|
||||
}
|
||||
|
||||
namespace WinUICppDesktopSampleApp
|
||||
{
|
||||
std::wstring GetGamepadKeyRoutingState()
|
||||
{
|
||||
namespace wf_metadata = ABI::Windows::Foundation::Metadata;
|
||||
namespace wui = ABI::Windows::UI::Input;
|
||||
|
||||
auto const apiInformationName = RuntimeClassName(L"Windows.Foundation.Metadata.ApiInformation");
|
||||
winrt::com_ptr<wf_metadata::IApiInformationStatics> apiInformation;
|
||||
if (FAILED(RoGetActivationFactory(
|
||||
static_cast<HSTRING>(winrt::get_abi(apiInformationName)),
|
||||
__uuidof(wf_metadata::IApiInformationStatics),
|
||||
apiInformation.put_void())))
|
||||
{
|
||||
return L"Unavailable";
|
||||
}
|
||||
|
||||
auto const contractName = RuntimeClassName(L"Windows.Foundation.UniversalApiContract");
|
||||
boolean isContractPresent = false;
|
||||
if (FAILED(apiInformation->IsApiContractPresentByMajor(
|
||||
static_cast<HSTRING>(winrt::get_abi(contractName)),
|
||||
19,
|
||||
&isContractPresent)) ||
|
||||
!isContractPresent)
|
||||
{
|
||||
return L"Unavailable";
|
||||
}
|
||||
|
||||
auto const configurationName = RuntimeClassName(L"Windows.UI.Input.GamepadKeyRoutingConfiguration");
|
||||
winrt::com_ptr<wui::IGamepadKeyRoutingConfigurationStatics> gamepadKeyRouting;
|
||||
if (FAILED(RoGetActivationFactory(
|
||||
static_cast<HSTRING>(winrt::get_abi(configurationName)),
|
||||
__uuidof(wui::IGamepadKeyRoutingConfigurationStatics),
|
||||
gamepadKeyRouting.put_void())))
|
||||
{
|
||||
return L"Unavailable";
|
||||
}
|
||||
|
||||
// IsSupported() reports the same OS velocity feature that gates the routing request
|
||||
// itself, so a false here means the machine cannot route gamepad input at all. That is
|
||||
// not a product failure, and the test treats it as inconclusive.
|
||||
boolean isSupported = false;
|
||||
if (FAILED(gamepadKeyRouting->IsSupported(&isSupported)) || !isSupported)
|
||||
{
|
||||
return L"Unavailable";
|
||||
}
|
||||
|
||||
boolean isEnabled = false;
|
||||
if (FAILED(gamepadKeyRouting->get_IsKeyRoutingEnabled(&isEnabled)))
|
||||
{
|
||||
return L"Unavailable";
|
||||
}
|
||||
|
||||
return isEnabled ? L"Enabled" : L"Disabled";
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace WinUICppDesktopSampleApp
|
||||
{
|
||||
// Returns the process-wide gamepad key routing state as "Enabled", "Disabled", or
|
||||
// "Unavailable". See GamepadKeyRoutingState.cpp for why this lives in its own file.
|
||||
std::wstring GetGamepadKeyRoutingState();
|
||||
}
|
||||
+4
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
#include "pch.h"
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include "ShutdownOrderValidation.h"
|
||||
#include "GamepadKeyRoutingState.h"
|
||||
|
||||
using namespace winrt;
|
||||
using namespace Microsoft::UI::Xaml;
|
||||
@@ -36,6 +37,8 @@ namespace winrt::WinUICppDesktopSampleApp::implementation
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
textBlockGamepadKeyRouting().Text(::WinUICppDesktopSampleApp::GetGamepadKeyRoutingState());
|
||||
|
||||
SetBackground(m_currentBackground);
|
||||
|
||||
// sets up window chrome with the provided custom title bar
|
||||
|
||||
+4
@@ -20,6 +20,10 @@
|
||||
</StackPanel>
|
||||
<!-- Custom title bar end block -->
|
||||
<TextBlock Text="WinUICppDesktopSampleApp" FontSize="20" Margin="2"/>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="GamepadKeyRouting:" VerticalAlignment="Center" Margin="2"/>
|
||||
<TextBlock x:Name="textBlockGamepadKeyRouting" AutomationProperties.Name="textBlockGamepadKeyRouting" VerticalAlignment="Center" Margin="2"/>
|
||||
</StackPanel>
|
||||
<ContentDialog x:Name="contentDialogWindowChromeTest"
|
||||
PrimaryButtonText="Accept" IsPrimaryButtonEnabled="False"
|
||||
CloseButtonText="Cancel">
|
||||
|
||||
+3
-1
@@ -49,7 +49,7 @@
|
||||
<ApplicationTypeRevision>10.0</ApplicationTypeRevision>
|
||||
|
||||
<!-- Test specifying Enabled/Disabled optional changes -->
|
||||
<EnabledXamlOptionalChanges>_Reserved</EnabledXamlOptionalChanges>
|
||||
<EnabledXamlOptionalChanges>_Reserved,GamepadKeyRouting</EnabledXamlOptionalChanges>
|
||||
<DisabledXamlOptionalChanges>_Reserved</DisabledXamlOptionalChanges>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
@@ -137,6 +137,7 @@
|
||||
<DependentUpon>MarkupWindow.xaml</DependentUpon>
|
||||
</ClInclude>
|
||||
<ClInclude Include="ShutdownOrderValidation.h" />
|
||||
<ClInclude Include="GamepadKeyRoutingState.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ApplicationDefinition Include="App.xaml" />
|
||||
@@ -186,6 +187,7 @@
|
||||
</ClCompile>
|
||||
<ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
|
||||
<ClCompile Include="ShutdownOrderValidation.cpp" />
|
||||
<ClCompile Include="GamepadKeyRoutingState.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Midl Include="App.idl">
|
||||
|
||||
@@ -90,6 +90,30 @@ namespace Microsoft.UI.Xaml.Tests.MUXControls.InteractionTests
|
||||
WinUISampleAppTestsUtils.SelectCheckbox("checkBox");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Description("Validate that enabling the GamepadKeyRouting XamlOptionalChange makes the framework turn on gamepad key routing in the OS.")]
|
||||
public void GamepadKeyRoutingIsEnabledAtStartupTest()
|
||||
{
|
||||
// WinUICppDesktopSampleApp sets <EnabledXamlOptionalChanges>...GamepadKeyRouting</EnabledXamlOptionalChanges>,
|
||||
// so Application::Start() should have called the OS gamepad key routing API before the
|
||||
// main window was created. The app sampled the OS state at that point; here we read back
|
||||
// what it saw. This guards against codeflow or merge issues silently dropping the call.
|
||||
var textBlockAsUIObject = FindElement.ByName("textBlockGamepadKeyRouting");
|
||||
Verify.IsNotNull(textBlockAsUIObject);
|
||||
string state = new TextBlock(textBlockAsUIObject).DocumentText;
|
||||
Log.Comment($"textBlockGamepadKeyRouting={state}");
|
||||
|
||||
if (state == "Unavailable")
|
||||
{
|
||||
// The OS on this machine cannot route gamepad input, so there is nothing for the
|
||||
// framework to have enabled and there is nothing to verify.
|
||||
Log.Warning("Gamepad key routing is not supported on this OS - skipping verification.");
|
||||
return;
|
||||
}
|
||||
|
||||
Verify.AreEqual("Enabled", state);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[Description("Basic tests of a few MUX controls: TextBlock, CheckBox, Button, ToggleButton, Flyout.")]
|
||||
[TestProperty("Ignore", "True")] // http://task.ms/45771651
|
||||
|
||||
@@ -64,6 +64,7 @@ namespace MUXControlsTestApp.Utilities
|
||||
Verify.IsTrue(XamlOptionalChanges.EnableChange(XamlChangeId.OptimizeApplyStyles));
|
||||
Verify.IsTrue(XamlOptionalChanges.EnableChange(XamlChangeId.DefaultStyleOptimizations));
|
||||
Verify.IsTrue(XamlOptionalChanges.EnableChange(XamlChangeId.DeferContextFlyoutInit));
|
||||
Verify.IsTrue(XamlOptionalChanges.EnableChange(XamlChangeId.GamepadKeyRouting));
|
||||
}
|
||||
|
||||
public void UpdateXamlOptionalChanges()
|
||||
@@ -170,9 +171,23 @@ namespace MUXControlsTestApp.Utilities
|
||||
{
|
||||
changeId = XamlChangeId.DeferContextFlyoutInit;
|
||||
}
|
||||
else if (string.Equals(name, "GamepadKeyRouting", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
changeId = XamlChangeId.GamepadKeyRouting;
|
||||
}
|
||||
|
||||
Verify.AreNotEqual(changeId, XamlChangeId._Reserved, "Unknown XamlChangeId: " + name);
|
||||
|
||||
if (changeId == XamlChangeId.GamepadKeyRouting && !enabled)
|
||||
{
|
||||
// Gamepad key routing is applied to the process by Application.Start(), which has
|
||||
// already run by the time test setup reads this property. Clearing the optional
|
||||
// change here only updates the bit, leaving the process still routing gamepad input
|
||||
// as keys, so the test would silently run against the wrong configuration.
|
||||
Verify.Fail("XamlOptionalChanges test override cannot disable GamepadKeyRouting: the " +
|
||||
"process-wide routing setting is already applied by Application.Start().");
|
||||
}
|
||||
|
||||
xamlOptionalChanges.Add(changeId, enabled);
|
||||
pos = sepPos + 1;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,12 @@ In System XAML (UWP XAML), this gamepad-driven navigation was on by default —
|
||||
for free without any opt-in. WinUI 3 XAML does not turn it on automatically, so today the
|
||||
same app loses gamepad navigation unless it (or its host) explicitly enables it. We consider
|
||||
that gap a regression from System XAML, and this spec describes closing it by having the
|
||||
WinUI framework enable gamepad key routing on by default, the way System XAML did.
|
||||
WinUI framework enable gamepad key routing during XAML startup, the way System XAML did.
|
||||
|
||||
Because this changes existing behavior, the framework's enable is gated behind a
|
||||
**XAML optional change**, `XamlChangeId.GamepadKeyRouting`. An app opts in explicitly today;
|
||||
the change becomes the default in a later release. See
|
||||
[Opting in with XamlOptionalChanges](#opting-in-with-xamloptionalchanges).
|
||||
|
||||
Whether gamepad input is delivered to an app as virtual keys is controlled by an existing
|
||||
operating-system API, `Windows.UI.Input.GamepadKeyRoutingConfiguration`. This is a
|
||||
@@ -21,8 +26,9 @@ process-wide setting: when key routing is enabled, gamepad input arrives as virt
|
||||
for the whole process; when it is disabled, it does not.
|
||||
|
||||
Today an app (or host) has to turn this on itself. This spec describes enabling it inside
|
||||
the WinUI framework, so that a full WinUI app gets gamepad-driven navigation on by default,
|
||||
while still allowing the app to opt out.
|
||||
the WinUI framework, so that a full WinUI app gets gamepad-driven navigation without writing
|
||||
input code — initially by enabling the `GamepadKeyRouting` optional change, and by default in
|
||||
a later release — while still allowing the app to opt out.
|
||||
|
||||
The framework enables key routing only for a **full WinUI app** — one that starts XAML by
|
||||
calling `Application.Start()`. A process that merely embeds WinUI as a **XAML Island** is
|
||||
@@ -30,15 +36,43 @@ deliberately left alone: the framework does not flip the process-wide switch for
|
||||
host's non-XAML input code is unaffected. A host that wants routing can still enable it
|
||||
itself.
|
||||
|
||||
This spec does not introduce a new API. `GamepadKeyRoutingConfiguration` already ships
|
||||
This spec introduces no new *runtime class*. `GamepadKeyRoutingConfiguration` already ships
|
||||
in the OS (`Windows.Foundation.UniversalApiContract`, version 19). The work here is a
|
||||
framework-internal call to that existing API at the right point in XAML startup, plus
|
||||
guidance for how an app opts out. The API surface is reproduced in
|
||||
[API Details](#api-details) for reference only.
|
||||
framework-internal call to that existing API at the right point in XAML startup, gated by a
|
||||
new `XamlChangeId` enum value, plus guidance for how an app opts in and out. The surfaces are
|
||||
listed in [API Details](#api-details).
|
||||
|
||||
# Conceptual pages (How To)
|
||||
|
||||
## Gamepad navigation is on by default
|
||||
## Opting in with XamlOptionalChanges
|
||||
|
||||
Enabling gamepad key routing changes how an existing app receives input, so it is delivered
|
||||
as a **XAML optional change** rather than switched on unconditionally. The change id is
|
||||
`XamlChangeId.GamepadKeyRouting`.
|
||||
|
||||
An app opts in with the `EnabledXamlOptionalChanges` MSBuild property, which makes the XAML
|
||||
compiler emit the enable call into your generated `App` entry point, before
|
||||
`Application.Start()` runs:
|
||||
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<EnabledXamlOptionalChanges>GamepadKeyRouting</EnabledXamlOptionalChanges>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
Optional changes are locked when XAML initializes, so enabling the change after
|
||||
`Application.Start()` is too late — see
|
||||
[Where the framework makes the call](#where-the-framework-makes-the-call).
|
||||
|
||||
Planned rollout:
|
||||
|
||||
| Release | State |
|
||||
| --- | --- |
|
||||
| 2.x | Available, disabled by default (explicit opt-in as above) |
|
||||
| 3.x | Enabled by default; apps can still opt out |
|
||||
| 4.x | Always enabled; the optional change is deprecated |
|
||||
|
||||
## Gamepad navigation once the change is enabled
|
||||
|
||||
When your full WinUI app starts XAML — which it does by calling `Application.Start()` from
|
||||
its generated entry point — the framework enables gamepad key routing for the process.
|
||||
@@ -47,7 +81,7 @@ keys) is delivered to your app as keyboard virtual keys. This means WinUI's norm
|
||||
and focus behaviors — XY focus navigation, `Enter`/`Space` to invoke, `Esc` to dismiss, and
|
||||
similar — respond to the gamepad automatically, with no additional code in your app.
|
||||
|
||||
If your app does nothing special, this is the recommended experience: you get gamepad
|
||||
If your app does nothing further, this is the recommended experience: you get gamepad
|
||||
navigation for free, and if your app has no interest in gamepad input it can simply ignore
|
||||
those virtual keys.
|
||||
|
||||
@@ -87,9 +121,9 @@ setting. So the framework has to be deliberate about *which processes* it turns
|
||||
|
||||
It enables routing only from the **full-app entry point**, `Application.Start()`. A normal
|
||||
WinUI app reaches that method from its generated entry point, owns its process, and gets
|
||||
gamepad navigation on by default — process-wide is exactly what it wants.
|
||||
A process that embeds WinUI only as a **XAML Island** never calls `Application.Start()`, so
|
||||
the framework does not auto-enable routing for it.
|
||||
gamepad navigation once the optional change is enabled — process-wide is exactly what it
|
||||
wants. A process that embeds WinUI only as a **XAML Island** never calls `Application.Start()`,
|
||||
so the framework does not auto-enable routing for it, regardless of the optional change.
|
||||
|
||||
One residual point worth stating plainly: within a full WinUI app the setting is still
|
||||
process-wide, so if that app *also* hosts non-XAML UI in the same process, that UI's input
|
||||
@@ -108,17 +142,18 @@ The framework makes the call once per process, from the full-app entry point
|
||||
```cpp
|
||||
// Inside Application.Start() (FrameworkApplicationFactory::StartImpl), on the branch that
|
||||
// starts a full WinUI app. Island hosts don't reach here, so their process is left alone.
|
||||
// IsApiContractPresent checks the OS actually has the API (it ships in a newer contract than
|
||||
// WinUI's minimum OS); IsSupported checks the feature is usable on this device.
|
||||
if (Windows::Foundation::Metadata::ApiInformation::IsApiContractPresent(
|
||||
L"Windows.Foundation.UniversalApiContract", 19) &&
|
||||
Windows::UI::Input::GamepadKeyRoutingConfiguration::IsSupported())
|
||||
// The optional change must have been enabled before Application.Start(); the contract check
|
||||
// confirms the OS actually has the API, which ships in a newer contract than WinUI's minimum OS.
|
||||
if (OptionalChangeState::IsGamepadKeyRoutingEnabled())
|
||||
{
|
||||
Windows::UI::Input::GamepadKeyRoutingConfiguration::TrySetKeyRoutingEnabled(true);
|
||||
if (ApiInformation::IsApiContractPresentByMajor(L"Windows.Foundation.UniversalApiContract", 19))
|
||||
{
|
||||
GamepadKeyRoutingConfiguration::TrySetKeyRoutingEnabled(true);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Your app does not need to do anything to get this behavior.
|
||||
Your app does not need to do anything beyond enabling the optional change.
|
||||
|
||||
## Opting out from your app
|
||||
|
||||
@@ -135,9 +170,8 @@ public partial class App : Application
|
||||
|
||||
// Opt out of gamepad-to-key routing; the framework enabled it during XAML init.
|
||||
// IsApiContractPresent guards OS versions without the API (the type ships in a newer
|
||||
// contract than WinUI's minimum OS); IsSupported guards devices where it's unusable.
|
||||
if (Windows.Foundation.Metadata.ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 19) &&
|
||||
Windows.UI.Input.GamepadKeyRoutingConfiguration.IsSupported())
|
||||
// contract than WinUI's minimum OS).
|
||||
if (Windows.Foundation.Metadata.ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 19))
|
||||
{
|
||||
Windows.UI.Input.GamepadKeyRoutingConfiguration.TrySetKeyRoutingEnabled(false);
|
||||
}
|
||||
@@ -160,10 +194,9 @@ App::App()
|
||||
{
|
||||
// Opt out of gamepad-to-key routing; the framework enabled it during XAML init.
|
||||
// IsApiContractPresent guards OS versions without the API (the type ships in a newer
|
||||
// contract than WinUI's minimum OS); IsSupported guards devices where it's unusable.
|
||||
// contract than WinUI's minimum OS).
|
||||
if (winrt::Windows::Foundation::Metadata::ApiInformation::IsApiContractPresent(
|
||||
L"Windows.Foundation.UniversalApiContract", 19) &&
|
||||
winrt::Windows::UI::Input::GamepadKeyRoutingConfiguration::IsSupported())
|
||||
L"Windows.Foundation.UniversalApiContract", 19))
|
||||
{
|
||||
winrt::Windows::UI::Input::GamepadKeyRoutingConfiguration::TrySetKeyRoutingEnabled(false);
|
||||
}
|
||||
@@ -172,13 +205,30 @@ App::App()
|
||||
|
||||
# API Pages
|
||||
|
||||
_Spec note: This feature introduces no new WinUI API. It calls the existing OS API
|
||||
[`Windows.UI.Input.GamepadKeyRoutingConfiguration`](https://learn.microsoft.com/uwp/api/windows.ui.input.gamepadkeyroutingconfiguration).
|
||||
The existing surface is listed in [API Details](#api-details) for reviewer convenience._
|
||||
_Spec note: This feature introduces no new WinUI runtime class. It adds one value to the
|
||||
existing `Microsoft.UI.Xaml.XamlChangeId` enum, and calls the existing OS API
|
||||
[`Windows.UI.Input.GamepadKeyRoutingConfiguration`](https://learn.microsoft.com/uwp/api/windows.ui.input.gamepadkeyroutingconfiguration)._
|
||||
|
||||
# API Details
|
||||
|
||||
```c# (but really MIDL3)
|
||||
namespace Microsoft.UI.Xaml
|
||||
{
|
||||
enum XamlChangeId
|
||||
{
|
||||
_Reserved,
|
||||
IconNoGridOptimization = 61276805,
|
||||
OptimizeApplyStyles = 61697456,
|
||||
DefaultStyleOptimizations = 60995620,
|
||||
DeferContextFlyoutInit = 61098986,
|
||||
|
||||
// New in this work. Experimental until it completes API review.
|
||||
[feature(Feature_ExperimentalApi)]
|
||||
[contract(Microsoft.UI.Xaml.WinUIContract, 12)]
|
||||
GamepadKeyRouting = 63117108,
|
||||
};
|
||||
}
|
||||
|
||||
namespace Windows.UI.Input
|
||||
{
|
||||
// Existing OS API (Windows.Foundation.UniversalApiContract, version 19).
|
||||
@@ -206,8 +256,9 @@ namespace Windows.UI.Input
|
||||
The enable is placed at the **full-app entry point**: `Application.Start()`, whose
|
||||
implementation is `FrameworkApplicationFactory::StartImpl` in
|
||||
`/dxaml/xcp/dxaml/lib/FrameworkApplication_Partial.cpp`. `StartImpl` reads the app's
|
||||
windowing model and dispatches to `StartDesktop()` (WinUI Desktop) or `StartUWP()` (WinUI
|
||||
UWP). A full WinUI app reaches this method from its generated entry point; a process that
|
||||
windowing model and dispatches to `StartDesktop()` (WinUI Desktop) or `StartUWP()` (retained
|
||||
only for tests that have not been ported; WinUI does not support the UWP windowing model).
|
||||
A full WinUI app reaches this method from its generated entry point; a process that
|
||||
only embeds WinUI as a XAML Island never does. Because the enable lives here, only full
|
||||
WinUI apps turn routing on — anything that starts XAML without going through
|
||||
`Application.Start()` leaves the process-wide setting untouched.
|
||||
@@ -229,25 +280,18 @@ _Check_return_ HRESULT FrameworkApplicationFactory::StartImpl(_In_opt_ xaml::IAp
|
||||
IFC_RETURN(E_FAIL);
|
||||
}
|
||||
|
||||
if (policy == AppPolicyWindowingModel_ClassicDesktop ||
|
||||
policy == AppPolicyWindowingModel_Universal)
|
||||
{
|
||||
// Full WinUI app: enable gamepad-to-key routing for the process. Island hosts never
|
||||
// reach here (they start via WindowsXamlManager.InitializeForCurrentThread()), so the
|
||||
// host process is left untouched.
|
||||
// IsApiContractPresent: the OS has the type (it ships in UniversalApiContract 19,
|
||||
// newer than WinUI's minimum OS, so on downlevel OS the type is absent and even
|
||||
// calling IsSupported() would throw). IsSupported: usable on this device.
|
||||
if (winrt::Windows::Foundation::Metadata::ApiInformation::IsApiContractPresent(
|
||||
L"Windows.Foundation.UniversalApiContract", 19) &&
|
||||
winrt::Windows::UI::Input::GamepadKeyRoutingConfiguration::IsSupported())
|
||||
{
|
||||
winrt::Windows::UI::Input::GamepadKeyRoutingConfiguration::TrySetKeyRoutingEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (policy == AppPolicyWindowingModel_ClassicDesktop)
|
||||
{
|
||||
if (OptionalChangeState::IsGamepadKeyRoutingEnabled())
|
||||
{
|
||||
// Full WinUI app: enable gamepad-to-key routing for the process, so XAML's
|
||||
// keyboard navigation responds to a gamepad the way it did in System XAML.
|
||||
// Island hosts never reach here (they start via
|
||||
// WindowsXamlManager.InitializeForCurrentThread()), so the host process is
|
||||
// left untouched.
|
||||
VERIFYHR(EnableGamepadKeyRouting());
|
||||
}
|
||||
|
||||
return FrameworkApplication::StartDesktop();
|
||||
}
|
||||
else if (policy == AppPolicyWindowingModel_Universal)
|
||||
@@ -259,25 +303,42 @@ _Check_return_ HRESULT FrameworkApplicationFactory::StartImpl(_In_opt_ xaml::IAp
|
||||
}
|
||||
```
|
||||
|
||||
`ApiInformation::IsApiContractPresent(..., 19)` guards OS versions that predate the API
|
||||
(where the type isn't registered and calling any member — including `IsSupported()` — would
|
||||
throw), and `IsSupported()` guards devices where the feature exists but is unusable.
|
||||
## Why the call lives in a light-up translation unit
|
||||
|
||||
`FrameworkApplication_Partial.cpp` will need `#include <winrt/Windows.UI.Input.h>` and
|
||||
`#include <winrt/Windows.Foundation.Metadata.h>`.
|
||||
`GamepadKeyRoutingConfiguration` requires `UniversalApiContract` 19, but WinUI normally
|
||||
compiles against contract 7 (Windows build 17763, its downlevel limit), so the type is not
|
||||
even declared in a normal translation unit. `EnableGamepadKeyRouting()` therefore lives in
|
||||
`DXamlCoreLightup.cpp`, which is compiled without the PCH and with
|
||||
`WINDOWS_FOUNDATION_UNIVERSALAPICONTRACT_VERSION` undefined so the SDK headers expose their
|
||||
latest contract. It is declared in `GamepadKeyRoutingLightup.h`, which `StartImpl` includes.
|
||||
A `static_assert` in that file fails the build if the SDK in use predates contract 19.
|
||||
|
||||
At run time the OS may still be older than the SDK, so the implementation checks
|
||||
`ApiInformation::IsApiContractPresentByMajor("Windows.Foundation.UniversalApiContract", 19)`
|
||||
before activating the type. It does **not** call `IsSupported()` first:
|
||||
`TrySetKeyRoutingEnabled` always returns `S_OK`, reporting refusal through its out-parameter,
|
||||
and internally gates on the same feature state that `IsSupported()` reports. A refusal is not
|
||||
treated as an error — gamepad navigation is an enhancement, and the app is fully usable
|
||||
without it.
|
||||
|
||||
## Startup ordering (why app opt-out works)
|
||||
|
||||
On the desktop path the sequence is:
|
||||
|
||||
1. `Application.Start()` → `StartImpl` — the framework's `TrySetKeyRoutingEnabled(true)` runs
|
||||
here, before dispatching to `StartDesktop()`.
|
||||
2. `StartDesktop()` brings up the framework.
|
||||
3. Construct the app's `Application` object (`App` ctor).
|
||||
4. `OnLaunched` → app creates its first window.
|
||||
5. Run the message loop.
|
||||
1. The generated entry point calls `XamlOptionalChanges.EnableChange(GamepadKeyRouting)` (if
|
||||
the app opted in) — this must happen before the next step.
|
||||
2. `Application.Start()` → `StartImpl` — if the optional change is enabled, the framework's
|
||||
`TrySetKeyRoutingEnabled(true)` runs here, before dispatching to `StartDesktop()`.
|
||||
3. `StartDesktop()` brings up the framework, which locks optional changes during
|
||||
`DXamlCore::Initialize()`.
|
||||
4. Construct the app's `Application` object (`App` ctor).
|
||||
5. `OnLaunched` → app creates its first window.
|
||||
6. Run the message loop.
|
||||
|
||||
The framework's enable (step 1) runs before any app code (step 3 onward) and never runs
|
||||
The framework's enable (step 2) runs before any app code (step 4 onward) and never runs
|
||||
again. So to turn gamepad navigation off, an app just calls the setter with `false` from its
|
||||
`App` constructor or `OnLaunched` — that later write is the last one and sticks for the
|
||||
process.
|
||||
process. Note that by step 4 the optional changes are already locked, so opting out at that
|
||||
point means calling `GamepadKeyRoutingConfiguration.TrySetKeyRoutingEnabled(false)` directly;
|
||||
calling `XamlOptionalChanges.DisableChange()` would both fail and, even if it succeeded, have
|
||||
no effect on the OS setting that was already applied.
|
||||
@@ -2117,6 +2117,10 @@ std::vector<std::pair<xaml_settings::XamlChangeId, bool>> GetXamlOptionalChanges
|
||||
{
|
||||
changeId = xaml_settings::XamlChangeId_DeferContextFlyoutInit;
|
||||
}
|
||||
else if (_wcsicmp(name.c_str(), L"GamepadKeyRouting") == 0)
|
||||
{
|
||||
changeId = xaml_settings::XamlChangeId_GamepadKeyRouting;
|
||||
}
|
||||
|
||||
if (changeId == xaml_settings::XamlChangeId__Reserved)
|
||||
{
|
||||
@@ -2197,6 +2201,7 @@ void WindowHelper::InitializeXamlCore(_In_ xaml_markup::IXamlMetadataProvider* c
|
||||
optionalChangesStatics->EnableChange(xaml_settings::XamlChangeId_OptimizeApplyStyles, &mutated);
|
||||
optionalChangesStatics->EnableChange(xaml_settings::XamlChangeId_DefaultStyleOptimizations, &mutated);
|
||||
optionalChangesStatics->EnableChange(xaml_settings::XamlChangeId_DeferContextFlyoutInit, &mutated);
|
||||
optionalChangesStatics->EnableChange(xaml_settings::XamlChangeId_GamepadKeyRouting, &mutated);
|
||||
|
||||
// Apply per-test overrides from XamlOptionalChanges test data.
|
||||
for (const auto& [changeId, enabled] : changeOverrides)
|
||||
|
||||
@@ -2212,6 +2212,7 @@ namespace DirectUI
|
||||
OptimizeApplyStyles = 61697456,
|
||||
DefaultStyleOptimizations = 60995620,
|
||||
DeferContextFlyoutInit = 61098986,
|
||||
GamepadKeyRouting = 63117108,
|
||||
};
|
||||
DEFINE_ENUM_FLAG_OPERATORS(XamlChangeId);
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace OptionalChangeState
|
||||
constexpr int BitIndex_OptimizeApplyStyles = 1;
|
||||
constexpr int BitIndex_DefaultStyleOptimizations = 2;
|
||||
constexpr int BitIndex_DeferContextFlyoutInit = 3;
|
||||
constexpr int BitIndex_GamepadKeyRouting = 4;
|
||||
|
||||
inline bool IsOptionalChangeEnabled(int bitIndex)
|
||||
{
|
||||
@@ -54,4 +55,9 @@ namespace OptionalChangeState
|
||||
{
|
||||
return IsOptionalChangeEnabled(BitIndex_DeferContextFlyoutInit) || IsPerfOptInEnabled();
|
||||
}
|
||||
|
||||
inline bool IsGamepadKeyRoutingEnabled()
|
||||
{
|
||||
return IsOptionalChangeEnabled(BitIndex_GamepadKeyRouting);
|
||||
}
|
||||
}
|
||||
@@ -520,6 +520,9 @@ namespace Microsoft.UI.Xaml.Settings
|
||||
OptimizeApplyStyles = 61697456,
|
||||
DefaultStyleOptimizations = 60995620,
|
||||
DeferContextFlyoutInit = 61098986,
|
||||
[feature(Feature_ExperimentalApi)]
|
||||
[contract(Microsoft.UI.Xaml.WinUIContract, 12)]
|
||||
GamepadKeyRouting = 63117108,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2364,4 +2367,3 @@ namespace Microsoft.UI.Xaml.Settings
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
#include "DXamlCore.h"
|
||||
#include <WRLHelper.h>
|
||||
#include "Callback.h"
|
||||
#include "GamepadKeyRoutingLightup.h"
|
||||
#include <windows.foundation.metadata.h>
|
||||
#include <windows.ui.input.h>
|
||||
|
||||
using namespace WRLHelper;
|
||||
using namespace DirectUI;
|
||||
@@ -131,3 +134,40 @@ void DXamlCore::RemoveAnimationsEnabledChangedHandler()
|
||||
m_animationsEnabledChangedToken.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// GamepadKeyRoutingConfiguration requires UniversalApiContract 19 (Windows 11, version 24H2).
|
||||
static_assert(
|
||||
WINDOWS_FOUNDATION_UNIVERSALAPICONTRACT_VERSION >= 0x130000,
|
||||
"The Windows SDK in use predates UniversalApiContract 19 and does not declare "
|
||||
"Windows.UI.Input.GamepadKeyRoutingConfiguration. Update the SDK version this repo builds against.");
|
||||
|
||||
_Check_return_ HRESULT DirectUI::EnableGamepadKeyRouting()
|
||||
{
|
||||
ctl::ComPtr<wf::Metadata::IApiInformationStatics> apiInformationStatics;
|
||||
IFC_RETURN(ctl::GetActivationFactory(
|
||||
wrl_wrappers::HStringReference(RuntimeClass_Windows_Foundation_Metadata_ApiInformation).Get(),
|
||||
&apiInformationStatics));
|
||||
|
||||
boolean isContractPresent = false;
|
||||
IFC_RETURN(apiInformationStatics->IsApiContractPresentByMajor(
|
||||
wrl_wrappers::HStringReference(L"Windows.Foundation.UniversalApiContract").Get(),
|
||||
19,
|
||||
&isContractPresent));
|
||||
|
||||
if (!isContractPresent)
|
||||
{
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
ctl::ComPtr<wui::IGamepadKeyRoutingConfigurationStatics> gamepadKeyRoutingStatics;
|
||||
IFC_RETURN(ctl::GetActivationFactory(
|
||||
wrl_wrappers::HStringReference(RuntimeClass_Windows_UI_Input_GamepadKeyRoutingConfiguration).Get(),
|
||||
&gamepadKeyRoutingStatics));
|
||||
|
||||
// A refusal is not an error: gamepad navigation is an enhancement, and the app remains fully
|
||||
// usable without it.
|
||||
boolean wasApplied = false;
|
||||
IFC_RETURN(gamepadKeyRoutingStatics->TrySetKeyRoutingEnabled(true, &wasApplied));
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
@@ -26,6 +26,8 @@
|
||||
#include <DesktopWindowImpl.h>
|
||||
#include <Microsoft.UI.Dispatching.Interop.h>
|
||||
#include <Microsoft.Windows.ApplicationModel.Resources.h>
|
||||
#include "GamepadKeyRoutingLightup.h"
|
||||
#include <OptionalChangeState.h>
|
||||
|
||||
using namespace RuntimeFeatureBehavior;
|
||||
using namespace DirectUI;
|
||||
@@ -180,6 +182,16 @@ _Check_return_ HRESULT FrameworkApplicationFactory::StartImpl(_In_opt_ xaml::IAp
|
||||
|
||||
if (policy == AppPolicyWindowingModel_ClassicDesktop)
|
||||
{
|
||||
if (OptionalChangeState::IsGamepadKeyRoutingEnabled())
|
||||
{
|
||||
// Full WinUI app: enable gamepad-to-key routing for the process, so XAML's
|
||||
// keyboard navigation responds to a gamepad the way it did in System XAML.
|
||||
// Island hosts never reach here (they start via
|
||||
// WindowsXamlManager.InitializeForCurrentThread()), so the host process is
|
||||
// left untouched.
|
||||
VERIFYHR(EnableGamepadKeyRouting());
|
||||
}
|
||||
|
||||
return FrameworkApplication::StartDesktop();
|
||||
}
|
||||
else if (policy == AppPolicyWindowingModel_Universal)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT License. See LICENSE in the project root for license information.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace DirectUI
|
||||
{
|
||||
// Enables routing of gamepad input to this process as keyboard virtual keys, so that XAML's
|
||||
// existing keyboard-driven navigation (XY focus, Enter/Space to invoke, Esc to dismiss) responds
|
||||
// to a gamepad without any app-specific code. See docs\design-notes\gamepad-navigation.md.
|
||||
//
|
||||
// The underlying OS setting is process-wide, so this is called only for a full WinUI app, from
|
||||
// Application.Start(). A process that embeds XAML only as an island never reaches that call and
|
||||
// is deliberately left at the OS default. An app opts back out by calling
|
||||
// Windows.UI.Input.GamepadKeyRoutingConfiguration.TrySetKeyRoutingEnabled(false) any time after
|
||||
// Application.Start().
|
||||
//
|
||||
// Defined in a light-up translation unit because the OS API requires a newer UAP contract than
|
||||
// the one this build normally targets.
|
||||
_Check_return_ HRESULT EnableGamepadKeyRouting();
|
||||
}
|
||||
@@ -44,6 +44,8 @@ static int GetBitIndex(xaml_settings::XamlChangeId id)
|
||||
return OptionalChangeState::BitIndex_DefaultStyleOptimizations;
|
||||
case xaml_settings::XamlChangeId_DeferContextFlyoutInit:
|
||||
return OptionalChangeState::BitIndex_DeferContextFlyoutInit;
|
||||
case xaml_settings::XamlChangeId_GamepadKeyRouting:
|
||||
return OptionalChangeState::BitIndex_GamepadKeyRouting;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace Microsoft.UI.Xaml.Settings
|
||||
// through XamlOptionalChanges. Each value corresponds to a specific feature,
|
||||
// fix, or behavioral change documented in the WinUI release notes.
|
||||
[Contract(typeof(Microsoft.UI.Xaml.WinUIContract), 11)]
|
||||
[Platform("Feature_ExperimentalApi", typeof(Microsoft.UI.Xaml.WinUIContract), Microsoft.UI.Xaml.WinUIContract.Experimental)]
|
||||
[DXamlIdlGroup("coretypes2")]
|
||||
[TypeTable(IsExcludedFromDXaml = true, IsExcludedFromCore = true)]
|
||||
public enum XamlChangeId
|
||||
@@ -20,6 +21,8 @@ namespace Microsoft.UI.Xaml.Settings
|
||||
OptimizeApplyStyles = 61697456,
|
||||
DefaultStyleOptimizations = 60995620,
|
||||
DeferContextFlyoutInit = 61098986,
|
||||
[VelocityFeature("Feature_ExperimentalApi")]
|
||||
GamepadKeyRouting = 63117108,
|
||||
}
|
||||
|
||||
// Provides static methods to opt in to or out of individual breaking or
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Windows.SDK.cpp.arm64" version="10.0.22621.755" />
|
||||
<package id="Microsoft.Windows.SDK.cpp.arm64" version="10.0.26100.9169" />
|
||||
</packages>
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Windows.SDK.cpp.arm64" version="10.0.22621.755" />
|
||||
<package id="Microsoft.Windows.SDK.cpp.arm64" version="10.0.26100.9169" />
|
||||
</packages>
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
<!-- For more information see docs\build-versioning.md -->
|
||||
<packages>
|
||||
<!-- Windows SDK as a NuGet package for c++ projects -->
|
||||
<package id="Microsoft.Windows.SDK.cpp" version="10.0.22621.755" />
|
||||
<package id="Microsoft.Windows.SDK.cpp" version="10.0.26100.9169" />
|
||||
<!-- Windows SDK metadata for language projections, fixed at RS5 (WinAppSDK downlevel limit)-->
|
||||
<package id="Microsoft.Windows.SDK.Contracts" version="10.0.17763.1000" />
|
||||
<!-- Metapackage for all the necessary components of .Net Native -->
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Windows.SDK.cpp.x64" version="10.0.22621.755" />
|
||||
<package id="Microsoft.Windows.SDK.cpp.x64" version="10.0.26100.9169" />
|
||||
</packages>
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Windows.SDK.cpp.x86" version="10.0.22621.755" />
|
||||
<package id="Microsoft.Windows.SDK.cpp.x86" version="10.0.26100.9169" />
|
||||
</packages>
|
||||
Reference in new issue
Block a user