@ -0,0 +1,16 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<Application |
|||
x:Class="ControlGallery.WinUI.App" |
|||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:local="using:ControlGallery.WinUI"> |
|||
<Application.Resources> |
|||
<ResourceDictionary> |
|||
<ResourceDictionary.MergedDictionaries> |
|||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" /> |
|||
<!-- Other merged dictionaries here --> |
|||
</ResourceDictionary.MergedDictionaries> |
|||
<!-- Other app resources here --> |
|||
</ResourceDictionary> |
|||
</Application.Resources> |
|||
</Application> |
|||
@ -0,0 +1,31 @@ |
|||
using global::Avalonia; |
|||
using global::Avalonia.Skia; |
|||
using global::Avalonia.Win32; |
|||
using WinUIApplication = Microsoft.UI.Xaml.Application; |
|||
using LaunchActivatedEventArgs = Microsoft.UI.Xaml.LaunchActivatedEventArgs; |
|||
using Window = Microsoft.UI.Xaml.Window; |
|||
|
|||
namespace ControlGallery.WinUI |
|||
{ |
|||
public partial class App : WinUIApplication |
|||
{ |
|||
private Window? _window; |
|||
|
|||
public App() |
|||
{ |
|||
InitializeComponent(); |
|||
} |
|||
|
|||
protected override void OnLaunched(LaunchActivatedEventArgs args) |
|||
{ |
|||
AppBuilder.Configure<ControlCatalog.App>() |
|||
.UseWin32() |
|||
.UseSkia() |
|||
.UseHarfBuzz() |
|||
.SetupWithoutStarting(); |
|||
|
|||
_window = new MainWindow(); |
|||
_window.Activate(); |
|||
} |
|||
} |
|||
} |
|||
|
After Width: | Height: | Size: 432 B |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 637 B |
|
After Width: | Height: | Size: 283 B |
|
After Width: | Height: | Size: 456 B |
|
After Width: | Height: | Size: 2.0 KiB |
@ -0,0 +1,67 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<OutputType>WinExe</OutputType> |
|||
<TargetFramework>$(AvsCurrentWinUITargetFramework)</TargetFramework> |
|||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion> |
|||
<RootNamespace>ControlGallery.WinUI</RootNamespace> |
|||
<ApplicationManifest>app.manifest</ApplicationManifest> |
|||
<Platforms>x86;x64;ARM64</Platforms> |
|||
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> |
|||
<PublishProfile>win-$(Platform).pubxml</PublishProfile> |
|||
<UseWinUI>true</UseWinUI> |
|||
<WinUISDKReferences>false</WinUISDKReferences> |
|||
<EnableMsixTooling>true</EnableMsixTooling> |
|||
<Nullable>enable</Nullable> |
|||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<Content Include="Assets\SplashScreen.scale-200.png" /> |
|||
<Content Include="Assets\LockScreenLogo.scale-200.png" /> |
|||
<Content Include="Assets\Square150x150Logo.scale-200.png" /> |
|||
<Content Include="Assets\Square44x44Logo.scale-200.png" /> |
|||
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" /> |
|||
<Content Include="Assets\StoreLogo.png" /> |
|||
<Content Include="Assets\Wide310x150Logo.scale-200.png" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<Manifest Include="$(ApplicationManifest)" /> |
|||
</ItemGroup> |
|||
|
|||
<!-- |
|||
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging |
|||
Tools extension to be activated for this project even if the Windows App SDK Nuget |
|||
package has not yet been restored. |
|||
--> |
|||
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'"> |
|||
<ProjectCapability Include="Msix" /> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" /> |
|||
<PackageReference Include="Microsoft.WindowsAppSDK" /> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" /> |
|||
<ProjectReference Include="..\..\src\Windows\Avalonia.Win32\Avalonia.Win32.csproj" /> |
|||
<ProjectReference Include="..\..\src\Windows\Avalonia.WinUI\Avalonia.WinUI.csproj" /> |
|||
<ProjectReference Include="..\..\src\HarfBuzz\Avalonia.HarfBuzz\Avalonia.HarfBuzz.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<!-- |
|||
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution |
|||
Explorer "Package and Publish" context menu entry to be enabled for this project even if |
|||
the Windows App SDK Nuget package has not yet been restored. |
|||
--> |
|||
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'"> |
|||
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu> |
|||
</PropertyGroup> |
|||
|
|||
<!-- Publish Properties --> |
|||
<PropertyGroup> |
|||
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun> |
|||
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun> |
|||
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed> |
|||
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed> |
|||
</PropertyGroup> |
|||
</Project> |
|||
@ -0,0 +1,15 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<Window |
|||
x:Class="ControlGallery.WinUI.MainWindow" |
|||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:avalonia="using:Avalonia.WinUI" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
|||
mc:Ignorable="d" |
|||
Title="ControlGallery.WinUI"> |
|||
|
|||
<Grid> |
|||
<avalonia:AvaloniaSwapChainPanel x:Name="AvaloniaPanel" /> |
|||
</Grid> |
|||
</Window> |
|||
@ -0,0 +1,11 @@ |
|||
namespace ControlGallery.WinUI |
|||
{ |
|||
public sealed partial class MainWindow : Microsoft.UI.Xaml.Window |
|||
{ |
|||
public MainWindow() |
|||
{ |
|||
InitializeComponent(); |
|||
AvaloniaPanel.Content = new ControlCatalog.MainView(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
|
|||
<Package |
|||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" |
|||
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest" |
|||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" |
|||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" |
|||
xmlns:systemai="http://schemas.microsoft.com/appx/manifest/systemai/windows10" |
|||
IgnorableNamespaces="uap rescap systemai"> |
|||
|
|||
<Identity |
|||
Name="2787a268-3e4a-4dd9-b385-8f677d254cb7" |
|||
Publisher="CN=drasticactions" |
|||
Version="1.0.0.0" /> |
|||
|
|||
<mp:PhoneIdentity PhoneProductId="2787a268-3e4a-4dd9-b385-8f677d254cb7" PhonePublisherId="00000000-0000-0000-0000-000000000000"/> |
|||
|
|||
<Properties> |
|||
<DisplayName>ControlGallery.WinUI</DisplayName> |
|||
<PublisherDisplayName>drasticactions</PublisherDisplayName> |
|||
<Logo>Assets\StoreLogo.png</Logo> |
|||
</Properties> |
|||
|
|||
<Dependencies> |
|||
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.26226.0" /> |
|||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.26226.0" /> |
|||
</Dependencies> |
|||
|
|||
<Resources> |
|||
<Resource Language="x-generate"/> |
|||
</Resources> |
|||
|
|||
<Applications> |
|||
<Application Id="App" |
|||
Executable="$targetnametoken$.exe" |
|||
EntryPoint="$targetentrypoint$"> |
|||
<uap:VisualElements |
|||
DisplayName="ControlGallery.WinUI" |
|||
Description="ControlGallery.WinUI" |
|||
BackgroundColor="transparent" |
|||
Square150x150Logo="Assets\Square150x150Logo.png" |
|||
Square44x44Logo="Assets\Square44x44Logo.png"> |
|||
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" /> |
|||
<uap:SplashScreen Image="Assets\SplashScreen.png" /> |
|||
</uap:VisualElements> |
|||
</Application> |
|||
</Applications> |
|||
|
|||
<Capabilities> |
|||
<rescap:Capability Name="runFullTrust" /> |
|||
<systemai:Capability Name="systemAIModels"/> |
|||
</Capabilities> |
|||
</Package> |
|||
@ -0,0 +1,10 @@ |
|||
{ |
|||
"profiles": { |
|||
"ControlGallery.WinUI (Package)": { |
|||
"commandName": "MsixPackage" |
|||
}, |
|||
"ControlGallery.WinUI (Unpackaged)": { |
|||
"commandName": "Project" |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> |
|||
<assemblyIdentity version="1.0.0.0" name="ControlGallery.WinUI.app"/> |
|||
|
|||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> |
|||
<application> |
|||
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10. |
|||
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation. |
|||
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest --> |
|||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" /> |
|||
</application> |
|||
</compatibility> |
|||
|
|||
<application xmlns="urn:schemas-microsoft-com:asm.v3"> |
|||
<windowsSettings> |
|||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness> |
|||
</windowsSettings> |
|||
</application> |
|||
</assembly> |
|||
@ -0,0 +1,29 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<TargetFramework>$(AvsCurrentWinUITargetFramework)</TargetFramework> |
|||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion> |
|||
<UseWinUI>true</UseWinUI> |
|||
<WinUISDKReferences>false</WinUISDKReferences> |
|||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> |
|||
<EnableRuntimeMarshalling>true</EnableRuntimeMarshalling> |
|||
<Nullable>enable</Nullable> |
|||
</PropertyGroup> |
|||
<ItemGroup> |
|||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" /> |
|||
<PackageReference Include="Microsoft.WindowsAppSDK" /> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" /> |
|||
<ProjectReference Include="..\Avalonia.Win32\Avalonia.Win32.csproj" /> |
|||
</ItemGroup> |
|||
<Import Project="..\..\..\build\NullableEnable.props" /> |
|||
<Import Project="..\..\..\build\DevAnalyzers.props" /> |
|||
<Import Project="..\..\..\build\TrimmingEnable.props" /> |
|||
<PropertyGroup Label="Warnings"> |
|||
<NoWarn>$(NoWarn);CA1416</NoWarn> |
|||
</PropertyGroup> |
|||
<ItemGroup> |
|||
<SupportedPlatform Remove="@(SupportedPlatform)" /> |
|||
<SupportedPlatform Include="Windows" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -0,0 +1,427 @@ |
|||
using System; |
|||
using System.Runtime.InteropServices; |
|||
using global::Avalonia; |
|||
using global::Avalonia.Controls.Embedding; |
|||
using global::Avalonia.Input; |
|||
using global::Avalonia.Input.Raw; |
|||
using Microsoft.UI.Xaml; |
|||
using Microsoft.UI.Xaml.Controls; |
|||
using Microsoft.UI.Xaml.Input; |
|||
using AvControl = global::Avalonia.Controls.Control; |
|||
using AvSize = global::Avalonia.Size; |
|||
using AvPoint = global::Avalonia.Point; |
|||
using AvVector = global::Avalonia.Vector; |
|||
using AvRect = global::Avalonia.Rect; |
|||
|
|||
namespace Avalonia.WinUI; |
|||
|
|||
public partial class AvaloniaSwapChainPanel : SwapChainPanel |
|||
{ |
|||
private SwapChainGlSurface? _glSurface; |
|||
private SwapChainPanelTopLevelImpl? _topLevelImpl; |
|||
private EmbeddableControlRoot? _root; |
|||
private AvControl? _content; |
|||
private readonly MouseDevice _mouseDevice = new(); |
|||
private readonly TouchDevice _touchDevice = new(); |
|||
private readonly PenDevice _penDevice = new(releasePointerOnPenUp: false); |
|||
private PixelSize _cachedPixelSize = new(1, 1); |
|||
private double _cachedScaling = 1.0; |
|||
|
|||
public AvaloniaSwapChainPanel() |
|||
{ |
|||
IsTabStop = true; |
|||
|
|||
Loaded += OnLoaded; |
|||
Unloaded += OnUnloaded; |
|||
SizeChanged += OnSizeChanged; |
|||
CompositionScaleChanged += OnCompositionScaleChanged; |
|||
|
|||
PointerPressed += OnPointerPressed; |
|||
PointerMoved += OnPointerMoved; |
|||
PointerReleased += OnPointerReleased; |
|||
PointerWheelChanged += OnPointerWheelChanged; |
|||
PointerCanceled += OnPointerCanceled; |
|||
KeyDown += OnKeyDown; |
|||
KeyUp += OnKeyUp; |
|||
CharacterReceived += OnCharacterReceived; |
|||
} |
|||
|
|||
public AvControl? Content |
|||
{ |
|||
get => _content; |
|||
set |
|||
{ |
|||
_content = value; |
|||
if (_root is not null) |
|||
_root.Content = value; |
|||
} |
|||
} |
|||
|
|||
private void UpdateCachedSize() |
|||
{ |
|||
var w = Math.Max(1, (int)(ActualWidth * CompositionScaleX)); |
|||
var h = Math.Max(1, (int)(ActualHeight * CompositionScaleY)); |
|||
_cachedPixelSize = new PixelSize(w, h); |
|||
_cachedScaling = CompositionScaleX; |
|||
} |
|||
|
|||
private PixelSize GetPixelSize() => _cachedPixelSize; |
|||
|
|||
private double GetScaling() => _cachedScaling; |
|||
|
|||
private void OnLoaded(object sender, RoutedEventArgs e) |
|||
{ |
|||
if (_glSurface is not null) |
|||
return; |
|||
|
|||
UpdateCachedSize(); |
|||
|
|||
// Create the GL surface — swap chain creation is deferred to CreateGlRenderTarget
|
|||
// where we have the actual rendering context's D3D device
|
|||
_glSurface = new SwapChainGlSurface(GetPixelSize, GetScaling, OnSwapChainCreated); |
|||
_topLevelImpl = new SwapChainPanelTopLevelImpl(_glSurface); |
|||
_topLevelImpl.ClientSize = new AvSize(ActualWidth, ActualHeight); |
|||
_topLevelImpl.RenderScaling = CompositionScaleX; |
|||
|
|||
// Create and start the EmbeddableControlRoot
|
|||
_root = new EmbeddableControlRoot(_topLevelImpl); |
|||
_root.Content = _content; |
|||
_root.Prepare(); |
|||
_root.StartRendering(); |
|||
} |
|||
|
|||
private unsafe void OnSwapChainCreated(IntPtr swapChainPtr) |
|||
{ |
|||
// Called from the render thread when the swap chain is first created.
|
|||
// Set it on the panel via ISwapChainPanelNative COM interop.
|
|||
DispatcherQueue.TryEnqueue(() => |
|||
{ |
|||
var panelUnknown = Marshal.GetIUnknownForObject(this); |
|||
try |
|||
{ |
|||
var iid = new Guid("63aad0b8-7c24-40ff-85a8-640d944cc325"); |
|||
Marshal.QueryInterface(panelUnknown, in iid, out var nativePtr); |
|||
if (nativePtr != IntPtr.Zero) |
|||
{ |
|||
try |
|||
{ |
|||
var vtable = *(IntPtr**)nativePtr; |
|||
var setSwapChain = (delegate* unmanaged[Stdcall]<IntPtr, IntPtr, int>)vtable[3]; |
|||
var hr = setSwapChain(nativePtr, swapChainPtr); |
|||
Marshal.ThrowExceptionForHR(hr); |
|||
} |
|||
finally |
|||
{ |
|||
Marshal.Release(nativePtr); |
|||
} |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
Marshal.Release(panelUnknown); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private void OnUnloaded(object sender, RoutedEventArgs e) |
|||
{ |
|||
_root?.StopRendering(); |
|||
_root?.Dispose(); |
|||
_root = null; |
|||
_topLevelImpl = null; |
|||
_glSurface?.DisposeSwapChain(); |
|||
_glSurface = null; |
|||
_touchDevice.Dispose(); |
|||
_penDevice.Dispose(); |
|||
} |
|||
|
|||
private void OnSizeChanged(object sender, SizeChangedEventArgs e) |
|||
{ |
|||
UpdateCachedSize(); |
|||
if (_topLevelImpl is not null) |
|||
_topLevelImpl.ClientSize = new AvSize(e.NewSize.Width, e.NewSize.Height); |
|||
} |
|||
|
|||
private void OnCompositionScaleChanged(SwapChainPanel sender, object args) |
|||
{ |
|||
UpdateCachedSize(); |
|||
if (_topLevelImpl is not null) |
|||
_topLevelImpl.RenderScaling = CompositionScaleX; |
|||
} |
|||
|
|||
// Input forwarding
|
|||
|
|||
private IPointerDevice GetPointerDevice(PointerRoutedEventArgs e) |
|||
{ |
|||
return e.Pointer.PointerDeviceType switch |
|||
{ |
|||
Microsoft.UI.Input.PointerDeviceType.Touch => _touchDevice, |
|||
Microsoft.UI.Input.PointerDeviceType.Pen => _penDevice, |
|||
_ => _mouseDevice |
|||
}; |
|||
} |
|||
|
|||
private RawPointerPoint CreateRawPointerPoint(PointerRoutedEventArgs e) |
|||
{ |
|||
var point = e.GetCurrentPoint(this); |
|||
var props = point.Properties; |
|||
var pos = point.Position; |
|||
|
|||
var rawPoint = new RawPointerPoint |
|||
{ |
|||
Position = new AvPoint(pos.X, pos.Y), |
|||
Pressure = props.Pressure, |
|||
Twist = props.Twist, |
|||
XTilt = props.XTilt, |
|||
YTilt = props.YTilt, |
|||
}; |
|||
|
|||
if (props.ContactRect is { Width: > 0 } or { Height: > 0 }) |
|||
{ |
|||
var cr = props.ContactRect; |
|||
rawPoint.ContactRect = new AvRect(cr.X, cr.Y, cr.Width, cr.Height); |
|||
} |
|||
|
|||
return rawPoint; |
|||
} |
|||
|
|||
private RawPointerEventArgs CreatePointerArgs( |
|||
IInputDevice device, ulong timestamp, IInputRoot inputRoot, |
|||
RawPointerEventType type, RawPointerPoint point, |
|||
RawInputModifiers modifiers, uint pointerId) |
|||
{ |
|||
return device is TouchDevice |
|||
? new RawTouchEventArgs(device, timestamp, inputRoot, type, point, modifiers, pointerId) |
|||
: new RawPointerEventArgs(device, timestamp, inputRoot, type, point, modifiers) |
|||
{ |
|||
RawPointerId = pointerId |
|||
}; |
|||
} |
|||
|
|||
private ulong GetTimestamp(PointerRoutedEventArgs e) |
|||
{ |
|||
// WinUI PointerPoint.Timestamp is in microseconds; Avalonia expects milliseconds.
|
|||
return e.GetCurrentPoint(this).Timestamp / 1000; |
|||
} |
|||
|
|||
private RawInputModifiers GetPointerModifiers(PointerRoutedEventArgs e) |
|||
{ |
|||
var point = e.GetCurrentPoint(this); |
|||
var props = point.Properties; |
|||
var mods = WinUIKeyInterop.ModifiersFromVirtualKeyModifiers(e.KeyModifiers); |
|||
if (props.IsLeftButtonPressed) |
|||
mods |= RawInputModifiers.LeftMouseButton; |
|||
if (props.IsRightButtonPressed) |
|||
mods |= RawInputModifiers.RightMouseButton; |
|||
if (props.IsMiddleButtonPressed) |
|||
mods |= RawInputModifiers.MiddleMouseButton; |
|||
|
|||
if (e.Pointer.PointerDeviceType == Microsoft.UI.Input.PointerDeviceType.Pen) |
|||
{ |
|||
if (props.IsBarrelButtonPressed) |
|||
mods |= RawInputModifiers.PenBarrelButton; |
|||
if (props.IsEraser) |
|||
mods |= RawInputModifiers.PenEraser; |
|||
if (props.IsInverted) |
|||
mods |= RawInputModifiers.PenInverted; |
|||
} |
|||
|
|||
return mods; |
|||
} |
|||
|
|||
private void OnPointerPressed(object sender, PointerRoutedEventArgs e) |
|||
{ |
|||
if (_topLevelImpl?.Input is not { } input || _topLevelImpl.InputRoot is not { } inputRoot) |
|||
return; |
|||
|
|||
var device = GetPointerDevice(e); |
|||
var timestamp = GetTimestamp(e); |
|||
var rawPoint = CreateRawPointerPoint(e); |
|||
var modifiers = GetPointerModifiers(e); |
|||
var pointerId = e.Pointer.PointerId; |
|||
|
|||
RawPointerEventType type; |
|||
if (device is TouchDevice) |
|||
{ |
|||
type = RawPointerEventType.TouchBegin; |
|||
} |
|||
else |
|||
{ |
|||
var point = e.GetCurrentPoint(this); |
|||
var props = point.Properties; |
|||
if (props.IsLeftButtonPressed) |
|||
type = RawPointerEventType.LeftButtonDown; |
|||
else if (props.IsRightButtonPressed) |
|||
type = RawPointerEventType.RightButtonDown; |
|||
else if (props.IsMiddleButtonPressed) |
|||
type = RawPointerEventType.MiddleButtonDown; |
|||
else |
|||
return; |
|||
} |
|||
|
|||
Focus(FocusState.Pointer); |
|||
CapturePointer(e.Pointer); |
|||
|
|||
input(CreatePointerArgs(device, timestamp, inputRoot, type, rawPoint, modifiers, pointerId)); |
|||
e.Handled = true; |
|||
} |
|||
|
|||
private void OnPointerMoved(object sender, PointerRoutedEventArgs e) |
|||
{ |
|||
if (_topLevelImpl?.Input is not { } input || _topLevelImpl.InputRoot is not { } inputRoot) |
|||
return; |
|||
|
|||
var device = GetPointerDevice(e); |
|||
var timestamp = GetTimestamp(e); |
|||
var rawPoint = CreateRawPointerPoint(e); |
|||
var modifiers = GetPointerModifiers(e); |
|||
var pointerId = e.Pointer.PointerId; |
|||
|
|||
var type = device is TouchDevice ? RawPointerEventType.TouchUpdate : RawPointerEventType.Move; |
|||
|
|||
input(CreatePointerArgs(device, timestamp, inputRoot, type, rawPoint, modifiers, pointerId)); |
|||
} |
|||
|
|||
private void OnPointerReleased(object sender, PointerRoutedEventArgs e) |
|||
{ |
|||
if (_topLevelImpl?.Input is not { } input || _topLevelImpl.InputRoot is not { } inputRoot) |
|||
return; |
|||
|
|||
var device = GetPointerDevice(e); |
|||
var timestamp = GetTimestamp(e); |
|||
var rawPoint = CreateRawPointerPoint(e); |
|||
var modifiers = GetPointerModifiers(e); |
|||
var pointerId = e.Pointer.PointerId; |
|||
|
|||
RawPointerEventType type; |
|||
if (device is TouchDevice) |
|||
{ |
|||
type = RawPointerEventType.TouchEnd; |
|||
} |
|||
else |
|||
{ |
|||
var point = e.GetCurrentPoint(this); |
|||
var props = point.Properties; |
|||
switch (props.PointerUpdateKind) |
|||
{ |
|||
case Microsoft.UI.Input.PointerUpdateKind.LeftButtonReleased: |
|||
type = RawPointerEventType.LeftButtonUp; |
|||
break; |
|||
case Microsoft.UI.Input.PointerUpdateKind.RightButtonReleased: |
|||
type = RawPointerEventType.RightButtonUp; |
|||
break; |
|||
case Microsoft.UI.Input.PointerUpdateKind.MiddleButtonReleased: |
|||
type = RawPointerEventType.MiddleButtonUp; |
|||
break; |
|||
default: |
|||
return; |
|||
} |
|||
} |
|||
|
|||
ReleasePointerCapture(e.Pointer); |
|||
input(CreatePointerArgs(device, timestamp, inputRoot, type, rawPoint, modifiers, pointerId)); |
|||
e.Handled = true; |
|||
} |
|||
|
|||
private void OnPointerWheelChanged(object sender, PointerRoutedEventArgs e) |
|||
{ |
|||
if (_topLevelImpl?.Input is not { } input || _topLevelImpl.InputRoot is not { } inputRoot) |
|||
return; |
|||
|
|||
var device = GetPointerDevice(e); |
|||
var timestamp = GetTimestamp(e); |
|||
var point = e.GetCurrentPoint(this); |
|||
var delta = point.Properties.MouseWheelDelta; |
|||
var pos = point.Position; |
|||
|
|||
input(new RawMouseWheelEventArgs(device as MouseDevice ?? _mouseDevice, timestamp, inputRoot, |
|||
new AvPoint(pos.X, pos.Y), new AvVector(0, delta), GetPointerModifiers(e))); |
|||
e.Handled = true; |
|||
} |
|||
|
|||
private void OnPointerCanceled(object sender, PointerRoutedEventArgs e) |
|||
{ |
|||
if (_topLevelImpl?.Input is not { } input || _topLevelImpl.InputRoot is not { } inputRoot) |
|||
return; |
|||
|
|||
var device = GetPointerDevice(e); |
|||
var timestamp = GetTimestamp(e); |
|||
var rawPoint = CreateRawPointerPoint(e); |
|||
var modifiers = GetPointerModifiers(e); |
|||
var pointerId = e.Pointer.PointerId; |
|||
|
|||
var type = device is TouchDevice ? RawPointerEventType.TouchCancel : RawPointerEventType.LeaveWindow; |
|||
|
|||
input(CreatePointerArgs(device, timestamp, inputRoot, type, rawPoint, modifiers, pointerId)); |
|||
e.Handled = true; |
|||
} |
|||
|
|||
private void OnKeyDown(object sender, KeyRoutedEventArgs e) |
|||
{ |
|||
if (_topLevelImpl?.Input is not { } input || _topLevelImpl.InputRoot is not { } inputRoot) |
|||
return; |
|||
|
|||
var key = WinUIKeyInterop.KeyFromVirtualKey(e.Key); |
|||
if (key != Key.None) |
|||
{ |
|||
var keyboard = GetKeyboardDevice(); |
|||
if (keyboard is null) return; |
|||
input(new RawKeyEventArgs(keyboard, (ulong)Environment.TickCount64, inputRoot, |
|||
RawKeyEventType.KeyDown, key, GetCurrentModifiers(), |
|||
PhysicalKey.None, null)); |
|||
e.Handled = true; |
|||
} |
|||
} |
|||
|
|||
private void OnKeyUp(object sender, KeyRoutedEventArgs e) |
|||
{ |
|||
if (_topLevelImpl?.Input is not { } input || _topLevelImpl.InputRoot is not { } inputRoot) |
|||
return; |
|||
|
|||
var key = WinUIKeyInterop.KeyFromVirtualKey(e.Key); |
|||
if (key != Key.None) |
|||
{ |
|||
var keyboard = GetKeyboardDevice(); |
|||
if (keyboard is null) return; |
|||
input(new RawKeyEventArgs(keyboard, (ulong)Environment.TickCount64, inputRoot, |
|||
RawKeyEventType.KeyUp, key, GetCurrentModifiers(), |
|||
PhysicalKey.None, null)); |
|||
e.Handled = true; |
|||
} |
|||
} |
|||
|
|||
private void OnCharacterReceived(UIElement sender, CharacterReceivedRoutedEventArgs e) |
|||
{ |
|||
if (_topLevelImpl?.Input is not { } input || _topLevelImpl.InputRoot is not { } inputRoot) |
|||
return; |
|||
|
|||
var keyboard = GetKeyboardDevice(); |
|||
if (keyboard is null) return; |
|||
|
|||
var ch = e.Character; |
|||
if (!char.IsControl(ch) || ch == '\r' || ch == '\n' || ch == '\t') |
|||
{ |
|||
input(new RawTextInputEventArgs(keyboard, (ulong)Environment.TickCount64, inputRoot, |
|||
new string(ch, 1))); |
|||
e.Handled = true; |
|||
} |
|||
} |
|||
|
|||
private static IKeyboardDevice? GetKeyboardDevice() |
|||
=> AvaloniaLocator.Current.GetService<IKeyboardDevice>(); |
|||
|
|||
private static RawInputModifiers GetCurrentModifiers() |
|||
{ |
|||
var mods = RawInputModifiers.None; |
|||
var ctrlState = Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.Control); |
|||
if (ctrlState.HasFlag(Windows.UI.Core.CoreVirtualKeyStates.Down)) |
|||
mods |= RawInputModifiers.Control; |
|||
var shiftState = Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.Shift); |
|||
if (shiftState.HasFlag(Windows.UI.Core.CoreVirtualKeyStates.Down)) |
|||
mods |= RawInputModifiers.Shift; |
|||
var altState = Microsoft.UI.Input.InputKeyboardSource.GetKeyStateForCurrentThread(Windows.System.VirtualKey.Menu); |
|||
if (altState.HasFlag(Windows.UI.Core.CoreVirtualKeyStates.Down)) |
|||
mods |= RawInputModifiers.Alt; |
|||
return mods; |
|||
} |
|||
} |
|||
@ -0,0 +1,248 @@ |
|||
using System; |
|||
using System.Runtime.InteropServices; |
|||
using global::Avalonia; |
|||
using global::Avalonia.OpenGL; |
|||
using global::Avalonia.OpenGL.Egl; |
|||
using global::Avalonia.OpenGL.Surfaces; |
|||
using global::Avalonia.Platform; |
|||
using global::Avalonia.Win32.DirectX; |
|||
using global::Avalonia.Win32.OpenGl.Angle; |
|||
using MicroCom.Runtime; |
|||
|
|||
namespace Avalonia.WinUI; |
|||
|
|||
internal unsafe class SwapChainGlSurface : EglGlPlatformSurfaceBase |
|||
{ |
|||
// QI for IDXGISwapChain2 fails on some Windows builds even though
|
|||
// IDXGISwapChain3/4 succeed. Use IDXGISwapChain3 which inherits from
|
|||
// IDXGISwapChain2 and has SetMatrixTransform at the same vtable slot.
|
|||
private static readonly Guid IDXGISwapChain3Guid = new("94d99bdb-f1f8-4ab0-b236-7da0170edab1"); |
|||
|
|||
[DllImport("dxgi.dll", ExactSpelling = true)] |
|||
private static extern int CreateDXGIFactory2(uint Flags, in Guid riid, out IntPtr ppFactory); |
|||
|
|||
private readonly Func<PixelSize> _getSizeFunc; |
|||
private readonly Func<double> _getScalingFunc; |
|||
private readonly Action<IntPtr> _setSwapChainCallback; |
|||
private IDXGISwapChain1? _swapChain; |
|||
private IntPtr _swapChain3Ptr; |
|||
|
|||
public SwapChainGlSurface( |
|||
Func<PixelSize> getSizeFunc, |
|||
Func<double> getScalingFunc, |
|||
Action<IntPtr> setSwapChainCallback) |
|||
{ |
|||
_getSizeFunc = getSizeFunc; |
|||
_getScalingFunc = getScalingFunc; |
|||
_setSwapChainCallback = setSwapChainCallback; |
|||
} |
|||
|
|||
public override IGlPlatformSurfaceRenderTarget CreateGlRenderTarget(IGlContext context) |
|||
{ |
|||
var eglContext = (EglContext)context; |
|||
|
|||
if (_swapChain is null) |
|||
{ |
|||
_swapChain = CreateSwapChain(eglContext); |
|||
|
|||
var swapChainPtr = MicroComRuntime.GetNativeIntPtr(_swapChain); |
|||
var guid = IDXGISwapChain3Guid; |
|||
var qiHr = Marshal.QueryInterface(swapChainPtr, in guid, out _swapChain3Ptr); |
|||
if (qiHr != 0 || _swapChain3Ptr == IntPtr.Zero) |
|||
throw new InvalidOperationException( |
|||
$"QI for IDXGISwapChain3 failed: HR=0x{qiHr:X8}, ptr={_swapChain3Ptr}"); |
|||
|
|||
SetInverseScaleTransform(_getScalingFunc()); |
|||
|
|||
_setSwapChainCallback(swapChainPtr); |
|||
} |
|||
|
|||
return new SwapChainGlRenderTarget(eglContext, _swapChain, _getSizeFunc, _getScalingFunc, _swapChain3Ptr, this); |
|||
} |
|||
|
|||
[StructLayout(LayoutKind.Sequential)] |
|||
internal struct DXGI_MATRIX_3X2_F |
|||
{ |
|||
public float _11, _12; |
|||
public float _21, _22; |
|||
public float _31, _32; |
|||
} |
|||
|
|||
internal void SetInverseScaleTransform(double scaling) |
|||
{ |
|||
if (_swapChain3Ptr == IntPtr.Zero || scaling <= 0) |
|||
return; |
|||
|
|||
var inverseScale = new DXGI_MATRIX_3X2_F |
|||
{ |
|||
_11 = 1.0f / (float)scaling, |
|||
_22 = 1.0f / (float)scaling |
|||
}; |
|||
|
|||
// IDXGISwapChain2::SetMatrixTransform vtable slot:
|
|||
// IUnknown(3) + IDXGIObject(4) + IDXGIDeviceSubObject(1) +
|
|||
// IDXGISwapChain(10) + IDXGISwapChain1(11) +
|
|||
// SetSourceSize, GetSourceSize, SetMaximumFrameLatency,
|
|||
// GetMaximumFrameLatency, GetFrameLatencyWaitableObject = 5
|
|||
// → SetMatrixTransform is at slot 34
|
|||
var vtable = *(IntPtr**)_swapChain3Ptr; |
|||
var setMatrixTransform = (delegate* unmanaged[Stdcall]<IntPtr, DXGI_MATRIX_3X2_F*, int>)vtable[34]; |
|||
var hr = setMatrixTransform(_swapChain3Ptr, &inverseScale); |
|||
Marshal.ThrowExceptionForHR(hr); |
|||
} |
|||
|
|||
private IDXGISwapChain1 CreateSwapChain(EglContext eglContext) |
|||
{ |
|||
var eglDisplay = (AngleWin32EglDisplay)eglContext.Display; |
|||
var d3dDevicePtr = eglDisplay.GetDirect3DDevice(); |
|||
var d3dDevice = MicroComRuntime.CreateProxyFor<IUnknown>(d3dDevicePtr, false); |
|||
|
|||
IDXGIDevice dxgiDevice; |
|||
using (d3dDevice) |
|||
dxgiDevice = d3dDevice.QueryInterface<IDXGIDevice>(); |
|||
|
|||
Guid factoryGuid = MicroComRuntime.GetGuidFor(typeof(IDXGIFactory2)); |
|||
var hr = CreateDXGIFactory2(0, in factoryGuid, out var factoryPtr); |
|||
Marshal.ThrowExceptionForHR(hr); |
|||
var dxgiFactory = MicroComRuntime.CreateProxyFor<IDXGIFactory2>(factoryPtr, true); |
|||
|
|||
var pixelSize = _getSizeFunc(); |
|||
var desc = new DXGI_SWAP_CHAIN_DESC1 |
|||
{ |
|||
Format = DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM, |
|||
SampleDesc = new DXGI_SAMPLE_DESC { Count = 1, Quality = 0 }, |
|||
BufferUsage = DxgiRenderTarget.DXGI_USAGE_RENDER_TARGET_OUTPUT, |
|||
BufferCount = 2, |
|||
SwapEffect = DXGI_SWAP_EFFECT.DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL, |
|||
AlphaMode = DXGI_ALPHA_MODE.DXGI_ALPHA_MODE_PREMULTIPLIED, |
|||
Width = (uint)pixelSize.Width, |
|||
Height = (uint)pixelSize.Height, |
|||
Flags = 0 |
|||
}; |
|||
|
|||
var swapChain = dxgiFactory.CreateSwapChainForComposition(dxgiDevice, &desc, null); |
|||
|
|||
dxgiFactory.Dispose(); |
|||
dxgiDevice.Dispose(); |
|||
|
|||
return swapChain; |
|||
} |
|||
|
|||
public void DisposeSwapChain() |
|||
{ |
|||
if (_swapChain3Ptr != IntPtr.Zero) |
|||
{ |
|||
Marshal.Release(_swapChain3Ptr); |
|||
_swapChain3Ptr = IntPtr.Zero; |
|||
} |
|||
_swapChain?.Dispose(); |
|||
_swapChain = null; |
|||
} |
|||
} |
|||
|
|||
internal unsafe class SwapChainGlRenderTarget : EglPlatformSurfaceRenderTargetBase |
|||
{ |
|||
private static readonly Guid ID3D11Texture2DGuid = Guid.Parse("6F15AAF2-D208-4E89-9AB4-489535D34F9C"); |
|||
|
|||
private readonly IDXGISwapChain1 _swapChain; |
|||
private readonly Func<PixelSize> _getSizeFunc; |
|||
private readonly Func<double> _getScalingFunc; |
|||
private readonly IntPtr _swapChain3Ptr; |
|||
private readonly SwapChainGlSurface _owner; |
|||
|
|||
private IUnknown? _renderTexture; |
|||
private EglSurface? _surface; |
|||
private PixelSize _lastSize; |
|||
private double _lastScaling; |
|||
|
|||
public SwapChainGlRenderTarget( |
|||
EglContext context, |
|||
IDXGISwapChain1 swapChain, |
|||
Func<PixelSize> getSizeFunc, |
|||
Func<double> getScalingFunc, |
|||
IntPtr swapChain3Ptr, |
|||
SwapChainGlSurface owner) : base(context) |
|||
{ |
|||
_swapChain = swapChain; |
|||
_getSizeFunc = getSizeFunc; |
|||
_getScalingFunc = getScalingFunc; |
|||
_swapChain3Ptr = swapChain3Ptr; |
|||
_owner = owner; |
|||
} |
|||
|
|||
public override IGlPlatformSurfaceRenderingSession BeginDrawCore(IRenderTarget.RenderTargetSceneInfo sceneInfo) |
|||
{ |
|||
var contextLock = Context.EnsureCurrent(); |
|||
var success = false; |
|||
try |
|||
{ |
|||
var size = _getSizeFunc(); |
|||
var scaling = _getScalingFunc(); |
|||
|
|||
if (scaling != _lastScaling) |
|||
{ |
|||
_owner.SetInverseScaleTransform(scaling); |
|||
_lastScaling = scaling; |
|||
} |
|||
|
|||
if (size != _lastSize) |
|||
{ |
|||
_surface?.Dispose(); |
|||
_surface = null; |
|||
_renderTexture?.Dispose(); |
|||
_renderTexture = null; |
|||
|
|||
_swapChain.ResizeBuffers(2, |
|||
(ushort)size.Width, |
|||
(ushort)size.Height, |
|||
DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM, |
|||
0); |
|||
|
|||
_lastSize = size; |
|||
} |
|||
|
|||
if (_renderTexture is null) |
|||
{ |
|||
_surface?.Dispose(); |
|||
_surface = null; |
|||
|
|||
Guid textureGuid = ID3D11Texture2DGuid; |
|||
_renderTexture = MicroComRuntime.CreateProxyFor<IUnknown>( |
|||
_swapChain.GetBuffer(0, &textureGuid), true); |
|||
} |
|||
|
|||
if (_surface is null) |
|||
{ |
|||
_surface = ((AngleWin32EglDisplay)Context.Display).WrapDirect3D11Texture( |
|||
MicroComRuntime.GetNativeIntPtr(_renderTexture), |
|||
0, 0, size.Width, size.Height); |
|||
} |
|||
|
|||
var res = base.BeginDraw(_surface, size, scaling, () => |
|||
{ |
|||
_swapChain.Present(1, 0); |
|||
contextLock?.Dispose(); |
|||
}, true); |
|||
success = true; |
|||
return res; |
|||
} |
|||
finally |
|||
{ |
|||
if (!success) |
|||
{ |
|||
_surface?.Dispose(); |
|||
_surface = null; |
|||
_renderTexture?.Dispose(); |
|||
_renderTexture = null; |
|||
contextLock.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public override void Dispose() |
|||
{ |
|||
base.Dispose(); |
|||
_surface?.Dispose(); |
|||
_renderTexture?.Dispose(); |
|||
} |
|||
} |
|||
@ -0,0 +1,100 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using global::Avalonia; |
|||
using global::Avalonia.Controls; |
|||
using global::Avalonia.Input; |
|||
using global::Avalonia.Input.Raw; |
|||
using global::Avalonia.OpenGL.Surfaces; |
|||
using global::Avalonia.Platform; |
|||
using global::Avalonia.Platform.Surfaces; |
|||
using global::Avalonia.Rendering.Composition; |
|||
using Size = global::Avalonia.Size; |
|||
using Point = global::Avalonia.Point; |
|||
using Rect = global::Avalonia.Rect; |
|||
using PixelPoint = global::Avalonia.PixelPoint; |
|||
|
|||
namespace Avalonia.WinUI; |
|||
|
|||
internal class SwapChainPanelTopLevelImpl : ITopLevelImpl |
|||
{ |
|||
private readonly IGlPlatformSurface _glSurface; |
|||
private Size _clientSize; |
|||
private double _scaling = 1.0; |
|||
|
|||
public SwapChainPanelTopLevelImpl(IGlPlatformSurface glSurface) |
|||
{ |
|||
_glSurface = glSurface; |
|||
var platformGraphics = AvaloniaLocator.Current.GetService<IPlatformGraphics>(); |
|||
Compositor = new Compositor(platformGraphics); |
|||
} |
|||
|
|||
public Size ClientSize |
|||
{ |
|||
get => _clientSize; |
|||
set |
|||
{ |
|||
_clientSize = value; |
|||
Resized?.Invoke(value, WindowResizeReason.Unspecified); |
|||
} |
|||
} |
|||
|
|||
public double RenderScaling |
|||
{ |
|||
get => _scaling; |
|||
set |
|||
{ |
|||
_scaling = value; |
|||
ScalingChanged?.Invoke(value); |
|||
} |
|||
} |
|||
|
|||
public double DesktopScaling => _scaling; |
|||
|
|||
public IPlatformHandle? Handle => null; |
|||
|
|||
public Compositor Compositor { get; } |
|||
|
|||
public IPlatformRenderSurface[] Surfaces => [_glSurface]; |
|||
|
|||
public Action<RawInputEventArgs>? Input { get; set; } |
|||
|
|||
public Action<Rect>? Paint { get; set; } |
|||
|
|||
public Action<Size, WindowResizeReason>? Resized { get; set; } |
|||
|
|||
public Action<double>? ScalingChanged { get; set; } |
|||
|
|||
public Action<WindowTransparencyLevel>? TransparencyLevelChanged { get; set; } |
|||
|
|||
public Action? Closed { get; set; } |
|||
|
|||
public Action? LostFocus { get; set; } |
|||
|
|||
public WindowTransparencyLevel TransparencyLevel => WindowTransparencyLevel.None; |
|||
|
|||
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new(1, 1, 1); |
|||
|
|||
public IInputRoot? InputRoot { get; private set; } |
|||
|
|||
public void SetInputRoot(IInputRoot inputRoot) => InputRoot = inputRoot; |
|||
|
|||
public Point PointToClient(PixelPoint point) => point.ToPoint(_scaling); |
|||
|
|||
public PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, _scaling); |
|||
|
|||
public void SetCursor(ICursorImpl? cursor) { } |
|||
|
|||
// Uses overlays instead of popups.
|
|||
public IPopupImpl? CreatePopup() => null; |
|||
|
|||
public void SetTransparencyLevelHint(IReadOnlyList<WindowTransparencyLevel> transparencyLevels) { } |
|||
|
|||
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { } |
|||
|
|||
public object? TryGetFeature(Type featureType) => null; |
|||
|
|||
public void Dispose() |
|||
{ |
|||
Closed?.Invoke(); |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
using global::Avalonia.Input; |
|||
using global::Avalonia.Input.Raw; |
|||
using global::Avalonia.Win32.Input; |
|||
using Windows.System; |
|||
|
|||
namespace Avalonia.WinUI; |
|||
|
|||
internal static class WinUIKeyInterop |
|||
{ |
|||
public static Key KeyFromVirtualKey(VirtualKey virtualKey) |
|||
{ |
|||
// WinUI VirtualKey enum values match Win32 VK_ constants.
|
|||
// Pass keyData=0; modifier disambiguation (L/R shift etc.) won't work
|
|||
// via scan codes, but WinUI doesn't distinguish those anyway.
|
|||
return KeyInterop.KeyFromVirtualKey((int)virtualKey, 0); |
|||
} |
|||
|
|||
public static RawInputModifiers ModifiersFromVirtualKeyModifiers(VirtualKeyModifiers modifiers) |
|||
{ |
|||
var result = RawInputModifiers.None; |
|||
if (modifiers.HasFlag(VirtualKeyModifiers.Control)) |
|||
result |= RawInputModifiers.Control; |
|||
if (modifiers.HasFlag(VirtualKeyModifiers.Shift)) |
|||
result |= RawInputModifiers.Shift; |
|||
if (modifiers.HasFlag(VirtualKeyModifiers.Menu)) |
|||
result |= RawInputModifiers.Alt; |
|||
if (modifiers.HasFlag(VirtualKeyModifiers.Windows)) |
|||
result |= RawInputModifiers.Meta; |
|||
return result; |
|||
} |
|||
} |
|||