Browse Source

[WinUI] Create Avalonia.WinUI for initial WinUI embed support (#21430)

* Init 1

Init 2

Init 3

Init 4

Init 5

Add Windows SDK

* Add WinUI to slnx

* Update sample

* Add clipboard support

* Only handle text down if Avalonia didn't first

* Start WinUIEmbedSample.csproj

* Migrate from Avalonia.WinUI to Avalonia.Win32

* Input handling

* Update sample

* Hover

* Update sample

* IME Fixes

* Fix keyscan

* Update sample

* Add cursor support

* Update samples

* Dispose _mouseDevice

* Implement Drag Source (And fix logging)

* Update samples

* Update clipboard sample

* Fix Focus/Tabs

* Peer Proxy

* Cleanup

* Cleanup

* Fix proj for WinUI

* Refactor log

* Fix proj

* Remove internal using

* try/finally contextLock

* Remove systemAIModels

* Fix IME

* Cleanup

* More IME changes

* Update comment

* Update logic

* Handle Win32 Cursors

* Fix AOT

* Update description

* Remove test code

* Fix sample
pull/21537/head
Tim Miller 4 months ago
committed by GitHub
parent
commit
16cd5ff650
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 9
      Avalonia.slnx
  2. 2
      Directory.Packages.props
  3. 1
      build/TargetFrameworks.props
  4. 6
      dirs.proj
  5. 16
      samples/ControlGallery.WinUI/App.xaml
  6. 40
      samples/ControlGallery.WinUI/App.xaml.cs
  7. BIN
      samples/ControlGallery.WinUI/Assets/LockScreenLogo.scale-200.png
  8. BIN
      samples/ControlGallery.WinUI/Assets/SplashScreen.scale-200.png
  9. BIN
      samples/ControlGallery.WinUI/Assets/Square150x150Logo.scale-200.png
  10. BIN
      samples/ControlGallery.WinUI/Assets/Square44x44Logo.scale-200.png
  11. BIN
      samples/ControlGallery.WinUI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png
  12. BIN
      samples/ControlGallery.WinUI/Assets/StoreLogo.png
  13. BIN
      samples/ControlGallery.WinUI/Assets/Wide310x150Logo.scale-200.png
  14. 67
      samples/ControlGallery.WinUI/ControlGallery.WinUI.csproj
  15. 15
      samples/ControlGallery.WinUI/MainWindow.xaml
  16. 11
      samples/ControlGallery.WinUI/MainWindow.xaml.cs
  17. 51
      samples/ControlGallery.WinUI/Package.appxmanifest
  18. 10
      samples/ControlGallery.WinUI/Properties/launchSettings.json
  19. 19
      samples/ControlGallery.WinUI/app.manifest
  20. 14
      samples/WinUIEmbedSample/App.xaml
  21. 40
      samples/WinUIEmbedSample/App.xaml.cs
  22. 7
      samples/WinUIEmbedSample/AvaloniaApp.axaml
  23. 13
      samples/WinUIEmbedSample/AvaloniaApp.axaml.cs
  24. 228
      samples/WinUIEmbedSample/EmbeddedView.axaml
  25. 183
      samples/WinUIEmbedSample/EmbeddedView.axaml.cs
  26. 114
      samples/WinUIEmbedSample/MainWindow.xaml
  27. 175
      samples/WinUIEmbedSample/MainWindow.xaml.cs
  28. 51
      samples/WinUIEmbedSample/Package.appxmanifest
  29. 10
      samples/WinUIEmbedSample/Properties/launchSettings.json
  30. 66
      samples/WinUIEmbedSample/WinUIEmbedSample.csproj
  31. 16
      samples/WinUIEmbedSample/app.manifest
  32. 1
      src/Avalonia.Base/Avalonia.Base.csproj
  33. 5
      src/Avalonia.Base/Logging/LogArea.cs
  34. 1
      src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
  35. 252
      src/Windows/Avalonia.Win32/OpenGl/Angle/SwapChainGlSurface.cs
  36. 122
      src/Windows/Avalonia.Win32/SwapChainTopLevelImpl.cs
  37. 143
      src/Windows/Avalonia.WinUI/Automation/AvaloniaSwapChainPanelAutomationPeer.cs
  38. 29
      src/Windows/Avalonia.WinUI/Automation/AvaloniaToXamlPeerProxy.ExpandCollapse.cs
  39. 9
      src/Windows/Avalonia.WinUI/Automation/AvaloniaToXamlPeerProxy.Invoke.cs
  40. 16
      src/Windows/Avalonia.WinUI/Automation/AvaloniaToXamlPeerProxy.RangeValue.cs
  41. 33
      src/Windows/Avalonia.WinUI/Automation/AvaloniaToXamlPeerProxy.Scroll.cs
  42. 51
      src/Windows/Avalonia.WinUI/Automation/AvaloniaToXamlPeerProxy.Selection.cs
  43. 27
      src/Windows/Avalonia.WinUI/Automation/AvaloniaToXamlPeerProxy.Toggle.cs
  44. 13
      src/Windows/Avalonia.WinUI/Automation/AvaloniaToXamlPeerProxy.Value.cs
  45. 141
      src/Windows/Avalonia.WinUI/Automation/AvaloniaToXamlPeerProxy.cs
  46. 52
      src/Windows/Avalonia.WinUI/Automation/ControlTypeMap.cs
  47. 30
      src/Windows/Avalonia.WinUI/Avalonia.WinUI.csproj
  48. 915
      src/Windows/Avalonia.WinUI/AvaloniaSwapChainPanel.cs
  49. 91
      src/Windows/Avalonia.WinUI/WinUICursorFactory.cs
  50. 42
      src/Windows/Avalonia.WinUI/WinUIDragSource.cs
  51. 62
      src/Windows/Avalonia.WinUI/WinUIKeyInterop.cs
  52. 384
      src/Windows/Avalonia.WinUI/WinUITextInputMethod.cs

9
Avalonia.slnx

@ -65,6 +65,10 @@
<Project Path="samples/ControlCatalog.MacCatalyst/ControlCatalog.MacCatalyst.csproj" /> <Project Path="samples/ControlCatalog.MacCatalyst/ControlCatalog.MacCatalyst.csproj" />
<Project Path="samples/ControlCatalog.tvOS/ControlCatalog.tvOS.csproj" /> <Project Path="samples/ControlCatalog.tvOS/ControlCatalog.tvOS.csproj" />
<Project Path="samples/ControlCatalog/ControlCatalog.csproj" /> <Project Path="samples/ControlCatalog/ControlCatalog.csproj" />
<Project Path="samples/ControlGallery.WinUI/ControlGallery.WinUI.csproj">
<Platform Project="x64" />
<Deploy />
</Project>
<Project Path="samples/Generators.Sandbox/Generators.Sandbox.csproj" /> <Project Path="samples/Generators.Sandbox/Generators.Sandbox.csproj" />
<Project Path="samples/GpuInterop/GpuInterop.csproj" /> <Project Path="samples/GpuInterop/GpuInterop.csproj" />
<Project Path="samples/IntegrationTestApp/IntegrationTestApp.csproj" /> <Project Path="samples/IntegrationTestApp/IntegrationTestApp.csproj" />
@ -84,6 +88,10 @@
<Project Path="samples/SingleProjectSandbox/SingleProjectSandbox.csproj" /> <Project Path="samples/SingleProjectSandbox/SingleProjectSandbox.csproj" />
<Project Path="samples/TextTestApp/TextTestApp.csproj" /> <Project Path="samples/TextTestApp/TextTestApp.csproj" />
<Project Path="samples/VirtualizationDemo/VirtualizationDemo.csproj" /> <Project Path="samples/VirtualizationDemo/VirtualizationDemo.csproj" />
<Project Path="samples/WinUIEmbedSample/WinUIEmbedSample.csproj">
<Platform Project="x64" />
<Deploy />
</Project>
<Project Path="samples/XEmbedSample/XEmbedSample.csproj" /> <Project Path="samples/XEmbedSample/XEmbedSample.csproj" />
</Folder> </Folder>
<Folder Name="/Samples/Interop/"> <Folder Name="/Samples/Interop/">
@ -178,6 +186,7 @@
<Project Path="src/Windows/Avalonia.Win32.Automation/Avalonia.Win32.Automation.csproj" /> <Project Path="src/Windows/Avalonia.Win32.Automation/Avalonia.Win32.Automation.csproj" />
<Project Path="src/Windows/Avalonia.Win32.Interoperability/Avalonia.Win32.Interoperability.csproj" /> <Project Path="src/Windows/Avalonia.Win32.Interoperability/Avalonia.Win32.Interoperability.csproj" />
<Project Path="src/Windows/Avalonia.Win32/Avalonia.Win32.csproj" /> <Project Path="src/Windows/Avalonia.Win32/Avalonia.Win32.csproj" />
<Project Path="src/Windows/Avalonia.WinUI/Avalonia.WinUI.csproj" />
</Folder> </Folder>
<Project Path="nukebuild/_build.csproj"> <Project Path="nukebuild/_build.csproj">
<Build Project="false" /> <Build Project="false" />

2
Directory.Packages.props

@ -31,6 +31,8 @@
<PackageVersion Include="Microsoft.Reactive.Testing" Version="6.1.0" /> <PackageVersion Include="Microsoft.Reactive.Testing" Version="6.1.0" />
<PackageVersion Include="Microsoft.Testing.Extensions.TrxReport" Version="2.0.2" /> <PackageVersion Include="Microsoft.Testing.Extensions.TrxReport" Version="2.0.2" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.257" /> <PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.3.257" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7705" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.260209005" />
<PackageVersion Include="Mono.Cecil" Version="0.11.6" /> <PackageVersion Include="Mono.Cecil" Version="0.11.6" />
<PackageVersion Include="MonoMac.NetStandard" Version="0.0.4" /> <PackageVersion Include="MonoMac.NetStandard" Version="0.0.4" />
<PackageVersion Include="Moq" Version="4.20.72" /> <PackageVersion Include="Moq" Version="4.20.72" />

1
build/TargetFrameworks.props

@ -8,6 +8,7 @@
<AvsCurrentIOSTargetFramework>$(AvsCurrentTargetFramework)-ios26.0</AvsCurrentIOSTargetFramework> <AvsCurrentIOSTargetFramework>$(AvsCurrentTargetFramework)-ios26.0</AvsCurrentIOSTargetFramework>
<AvsCurrentTvOSTargetFramework>$(AvsCurrentTargetFramework)-tvos26.0</AvsCurrentTvOSTargetFramework> <AvsCurrentTvOSTargetFramework>$(AvsCurrentTargetFramework)-tvos26.0</AvsCurrentTvOSTargetFramework>
<AvsCurrentBrowserTargetFramework>$(AvsCurrentTargetFramework)-browser</AvsCurrentBrowserTargetFramework> <AvsCurrentBrowserTargetFramework>$(AvsCurrentTargetFramework)-browser</AvsCurrentBrowserTargetFramework>
<AvsCurrentWinUITargetFramework>$(AvsCurrentTargetFramework)-windows10.0.19041.0</AvsCurrentWinUITargetFramework>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(AvsSkipBuildingLegacyTargetFrameworks)' != 'True'"> <PropertyGroup Condition="'$(AvsSkipBuildingLegacyTargetFrameworks)' != 'True'">
<AvsLegacyTargetFrameworks>net8.0</AvsLegacyTargetFrameworks> <AvsLegacyTargetFrameworks>net8.0</AvsLegacyTargetFrameworks>

6
dirs.proj

@ -14,12 +14,18 @@
<ProjectReference Remove="samples/*.Android/*.csproj" /> <ProjectReference Remove="samples/*.Android/*.csproj" />
<ProjectReference Remove="samples/*.Browser/*.csproj" /> <ProjectReference Remove="samples/*.Browser/*.csproj" />
<ProjectReference Remove="samples/SingleProjectSandbox/*.csproj" /> <ProjectReference Remove="samples/SingleProjectSandbox/*.csproj" />
<!-- Exclude WinUI samples when Platform is not specified or is AnyCPU -->
<ProjectReference Remove="samples/*.WinUI/*.csproj" Condition="'$(Platform)' == '' Or '$(Platform)' == 'AnyCPU'" />
<ProjectReference Remove="samples/WinUIEmbedSample/*.csproj" Condition="'$(Platform)' == '' Or '$(Platform)' == 'AnyCPU'" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="!$([MSBuild]::IsOsPlatform('Windows'))"> <ItemGroup Condition="!$([MSBuild]::IsOsPlatform('Windows'))">
<!-- Build mobile backends only on Windows, where we have installed android workload --> <!-- Build mobile backends only on Windows, where we have installed android workload -->
<ProjectReference Remove="src/Android/**/*.*proj" /> <ProjectReference Remove="src/Android/**/*.*proj" />
<ProjectReference Remove="src/iOS/**/*.*proj" /> <ProjectReference Remove="src/iOS/**/*.*proj" />
<ProjectReference Remove="src/Windows/Avalonia.WinUI/**/*.*proj" />
<ProjectReference Remove="samples/*.WinUI/*.csproj" />
<ProjectReference Remove="samples/WinUIEmbedSample/*.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

16
samples/ControlGallery.WinUI/App.xaml

@ -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>

40
samples/ControlGallery.WinUI/App.xaml.cs

@ -0,0 +1,40 @@
using global::Avalonia;
using global::Avalonia.Controls;
using global::Avalonia.Controls.ApplicationLifetimes;
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;
internal static SingleViewLifetime Lifetime { get; } = new();
public App()
{
InitializeComponent();
}
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
AppBuilder.Configure<ControlCatalog.App>()
.UseWin32()
.UseSkia()
.UseHarfBuzz()
.SetupWithLifetime(Lifetime);
_window = new MainWindow();
_window.Activate();
}
}
internal sealed class SingleViewLifetime : ISingleViewApplicationLifetime
{
public Control? MainView { get; set; }
}
}

BIN
samples/ControlGallery.WinUI/Assets/LockScreenLogo.scale-200.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

BIN
samples/ControlGallery.WinUI/Assets/SplashScreen.scale-200.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
samples/ControlGallery.WinUI/Assets/Square150x150Logo.scale-200.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
samples/ControlGallery.WinUI/Assets/Square44x44Logo.scale-200.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

BIN
samples/ControlGallery.WinUI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

BIN
samples/ControlGallery.WinUI/Assets/StoreLogo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

BIN
samples/ControlGallery.WinUI/Assets/Wide310x150Logo.scale-200.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

67
samples/ControlGallery.WinUI/ControlGallery.WinUI.csproj

@ -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>

15
samples/ControlGallery.WinUI/MainWindow.xaml

@ -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>

11
samples/ControlGallery.WinUI/MainWindow.xaml.cs

@ -0,0 +1,11 @@
namespace ControlGallery.WinUI
{
public sealed partial class MainWindow : Microsoft.UI.Xaml.Window
{
public MainWindow()
{
InitializeComponent();
AvaloniaPanel.Content = App.Lifetime.MainView;
}
}
}

51
samples/ControlGallery.WinUI/Package.appxmanifest

@ -0,0 +1,51 @@
<?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"
IgnorableNamespaces="uap rescap">
<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" />
</Capabilities>
</Package>

10
samples/ControlGallery.WinUI/Properties/launchSettings.json

@ -0,0 +1,10 @@
{
"profiles": {
"ControlGallery.WinUI (Package)": {
"commandName": "MsixPackage"
},
"ControlGallery.WinUI (Unpackaged)": {
"commandName": "Project"
}
}
}

19
samples/ControlGallery.WinUI/app.manifest

@ -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>

14
samples/WinUIEmbedSample/App.xaml

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<Application
x:Class="WinUIEmbedSample.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:WinUIEmbedSample">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

40
samples/WinUIEmbedSample/App.xaml.cs

@ -0,0 +1,40 @@
using global::Avalonia;
using global::Avalonia.Controls;
using global::Avalonia.Controls.ApplicationLifetimes;
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 WinUIEmbedSample
{
public partial class App : WinUIApplication
{
private Window? _window;
internal static SingleViewLifetime Lifetime { get; } = new();
public App()
{
InitializeComponent();
}
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
AppBuilder.Configure<AvaloniaApp>()
.UseWin32()
.UseSkia()
.UseHarfBuzz()
.SetupWithLifetime(Lifetime);
_window = new MainWindow();
_window.Activate();
}
}
internal sealed class SingleViewLifetime : ISingleViewApplicationLifetime
{
public Control? MainView { get; set; }
}
}

7
samples/WinUIEmbedSample/AvaloniaApp.axaml

@ -0,0 +1,7 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="WinUIEmbedSample.AvaloniaApp">
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

13
samples/WinUIEmbedSample/AvaloniaApp.axaml.cs

@ -0,0 +1,13 @@
using global::Avalonia;
using global::Avalonia.Markup.Xaml;
using AvApplication = global::Avalonia.Application;
namespace WinUIEmbedSample;
public partial class AvaloniaApp : AvApplication
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
}

228
samples/WinUIEmbedSample/EmbeddedView.axaml

@ -0,0 +1,228 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
x:Class="WinUIEmbedSample.EmbeddedView">
<Border>
<ScrollViewer>
<StackPanel Margin="16" Spacing="8">
<TextBlock Text="Avalonia"
FontSize="22"
FontWeight="SemiBold"
Margin="0,0,0,4" />
<Button x:Name="AvButton"
Content="Avalonia button"
HorizontalAlignment="Stretch"
Click="OnAvButtonClick" />
<TextBlock x:Name="AvClickCount" Text="Clicked 0 times" />
<Slider x:Name="AvSlider"
Minimum="0"
Maximum="100"
Value="25" />
<TextBlock x:Name="AvSliderValue" Text="Slider: 25" />
<TextBox PlaceholderText="Type something…"
Text="Hello from Avalonia" />
<ToggleSwitch Content="ToggleSwitch" IsChecked="True" />
<TextBlock Text="Selection &amp; expand patterns"
FontSize="16"
FontWeight="SemiBold"
Margin="0,16,0,0" />
<ComboBox x:Name="AvComboBox"
PlaceholderText="Pick a color"
HorizontalAlignment="Stretch">
<ComboBoxItem Content="Red" />
<ComboBoxItem Content="Green" />
<ComboBoxItem Content="Blue" IsSelected="True" />
</ComboBox>
<ListBox x:Name="AvListBox"
Height="100"
SelectionMode="Single">
<ListBoxItem Content="Item 1" />
<ListBoxItem Content="Item 2" IsSelected="True" />
<ListBoxItem Content="Item 3" />
<ListBoxItem Content="Item 4" />
</ListBox>
<StackPanel Orientation="Horizontal" Spacing="8">
<RadioButton x:Name="AvRadioOne" GroupName="DemoGroup" Content="Option A" IsChecked="True" />
<RadioButton x:Name="AvRadioTwo" GroupName="DemoGroup" Content="Option B" />
<RadioButton x:Name="AvRadioThree" GroupName="DemoGroup" Content="Option C" />
</StackPanel>
<Expander x:Name="AvExpander" Header="Expandable section">
<StackPanel Spacing="4">
<TextBlock Text="Content inside the expander." />
<TextBlock Text="Used to verify ExpandCollapse pattern."
Opacity="0.7"
FontSize="12" />
</StackPanel>
</Expander>
<TextBlock Text="Keys"
FontSize="16"
FontWeight="SemiBold"
Margin="0,16,0,0" />
<Border BorderThickness="1"
CornerRadius="4"
Padding="8"
Focusable="True"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}">
<StackPanel Spacing="2">
<TextBlock x:Name="KeyReadout"
FontFamily="Cascadia Code,Consolas,monospace"
Text="(focus here and press a key)" />
</StackPanel>
</Border>
<TextBlock Text="Clipboard"
FontSize="16"
FontWeight="SemiBold"
Margin="0,16,0,0" />
<TextBox x:Name="ClipboardText"
Text="Hello clipboard" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Content="Copy" Click="OnClipboardCopy" />
<Button Content="Paste" Click="OnClipboardPaste" />
<TextBlock x:Name="ClipboardStatus"
VerticalAlignment="Center"
Opacity="0.7"
Text="" />
</StackPanel>
<TextBlock Text="Drag source"
FontSize="16"
FontWeight="SemiBold"
Margin="0,16,0,0" />
<Border x:Name="DragSourceBorder"
Height="40"
CornerRadius="4"
BorderThickness="1"
Padding="8"
Cursor="SizeAll"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}">
<TextBlock Text="Drag me — payload: &quot;Hello from Avalonia&quot;"
VerticalAlignment="Center" />
</Border>
<TextBlock Text="Drop target"
FontSize="16"
FontWeight="SemiBold"
Margin="0,16,0,0" />
<Border x:Name="DropTargetBorder"
Height="80"
CornerRadius="4"
BorderThickness="1"
Padding="8"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}">
<TextBlock x:Name="DropStatus"
Text="(drop something here)"
TextWrapping="Wrap"
VerticalAlignment="Center" />
</Border>
<TextBlock Text="Cursors"
FontSize="16"
FontWeight="SemiBold"
Margin="0,16,0,0" />
<TextBlock Text="Hover each tile — the system cursor should change to match the label."
TextWrapping="Wrap"
Opacity="0.7" />
<UniformGrid Columns="4" Rows="2">
<Border Margin="2" Padding="6" BorderThickness="1" CornerRadius="3"
Cursor="Ibeam"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}">
<TextBlock Text="IBeam" HorizontalAlignment="Center" />
</Border>
<Border Margin="2" Padding="6" BorderThickness="1" CornerRadius="3"
Cursor="Hand"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}">
<TextBlock Text="Hand" HorizontalAlignment="Center" />
</Border>
<Border Margin="2" Padding="6" BorderThickness="1" CornerRadius="3"
Cursor="Cross"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}">
<TextBlock Text="Cross" HorizontalAlignment="Center" />
</Border>
<Border Margin="2" Padding="6" BorderThickness="1" CornerRadius="3"
Cursor="Wait"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}">
<TextBlock Text="Wait" HorizontalAlignment="Center" />
</Border>
<Border Margin="2" Padding="6" BorderThickness="1" CornerRadius="3"
Cursor="SizeAll"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}">
<TextBlock Text="SizeAll" HorizontalAlignment="Center" />
</Border>
<Border Margin="2" Padding="6" BorderThickness="1" CornerRadius="3"
Cursor="SizeWestEast"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}">
<TextBlock Text="SizeWE" HorizontalAlignment="Center" />
</Border>
<Border Margin="2" Padding="6" BorderThickness="1" CornerRadius="3"
Cursor="Help"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}">
<TextBlock Text="Help" HorizontalAlignment="Center" />
</Border>
<Border Margin="2" Padding="6" BorderThickness="1" CornerRadius="3"
Cursor="No"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}">
<TextBlock Text="No" HorizontalAlignment="Center" />
</Border>
</UniformGrid>
<TextBlock Text="Hover"
FontSize="16"
FontWeight="SemiBold"
Margin="0,16,0,0" />
<Border x:Name="HoverPanel"
Height="60"
CornerRadius="4"
BorderThickness="1"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}">
<Border.Styles>
<Style Selector="Border#HoverPanel:pointerover">
<Setter Property="Background"
Value="{DynamicResource SystemControlBackgroundAccentBrush}" />
</Style>
</Border.Styles>
<TextBlock x:Name="HoverState"
Text="out"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<TextBlock Text="Ink"
FontSize="16"
FontWeight="SemiBold"
Margin="0,16,0,0" />
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
BorderThickness="1"
CornerRadius="4"
Height="240">
<Canvas x:Name="InkCanvas"
Background="Transparent"
ClipToBounds="True"
PointerPressed="OnInkPointerPressed"
PointerMoved="OnInkPointerMoved"
PointerReleased="OnInkPointerReleased" />
</Border>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Content="Clear" Click="OnClearInk" />
<TextBlock x:Name="InkStats"
VerticalAlignment="Center"
Opacity="0.7"
Text="0 points / 0 intermediate" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</Border>
</UserControl>

183
samples/WinUIEmbedSample/EmbeddedView.axaml.cs

@ -0,0 +1,183 @@
using System.Linq;
using global::Avalonia;
using global::Avalonia.Controls;
using global::Avalonia.Controls.Shapes;
using global::Avalonia.Input;
using global::Avalonia.Interactivity;
using global::Avalonia.Markup.Xaml;
using global::Avalonia.Markup.Xaml.MarkupExtensions;
using global::Avalonia.Media;
using global::Avalonia.Platform.Storage;
namespace WinUIEmbedSample;
public partial class EmbeddedView : UserControl
{
private int _clicks;
private int _pointCount;
private int _intermediateCount;
public EmbeddedView()
{
InitializeComponent();
AvSlider.PropertyChanged += OnSliderPropertyChanged;
HoverPanel.PointerEntered += (_, _) => HoverState.Text = "over";
HoverPanel.PointerExited += (_, _) => HoverState.Text = "out";
AddHandler(KeyDownEvent, OnKeyReadout, handledEventsToo: true);
DragDrop.SetAllowDrop(DropTargetBorder, true);
DropTargetBorder.AddHandler(DragDrop.DragOverEvent, OnDropDragOver);
DropTargetBorder.AddHandler(DragDrop.DropEvent, OnDrop);
DragSourceBorder.PointerPressed += OnDragSourcePointerPressed;
}
private async void OnClipboardCopy(object? sender, RoutedEventArgs e)
{
var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
if (clipboard is null)
{
ClipboardStatus.Text = "no clipboard";
return;
}
var data = new global::Avalonia.Input.DataTransfer();
data.Add(DataTransferItem.CreateText(ClipboardText.Text ?? ""));
await clipboard.SetDataAsync(data);
ClipboardStatus.Text = $"copied {(ClipboardText.Text ?? "").Length} chars";
}
private async void OnClipboardPaste(object? sender, RoutedEventArgs e)
{
var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
if (clipboard is null)
{
ClipboardStatus.Text = "no clipboard";
return;
}
using var data = await clipboard.TryGetDataAsync();
var text = data is null ? null : await data.TryGetTextAsync();
if (text is not null)
{
ClipboardText.Text = text;
ClipboardStatus.Text = $"pasted {text.Length} chars";
}
else
{
ClipboardStatus.Text = "no text on clipboard";
}
}
private async void OnDragSourcePointerPressed(object? sender, PointerPressedEventArgs e)
{
var data = new global::Avalonia.Input.DataTransfer();
data.Add(DataTransferItem.CreateText("Hello from Avalonia"));
try
{
await DragDrop.DoDragDropAsync(e, data, DragDropEffects.Copy | DragDropEffects.Move);
}
catch
{
// Ignore — drag may be cancelled or the source unavailable.
}
}
private void OnDropDragOver(object? sender, DragEventArgs e)
{
e.DragEffects = e.DragEffects & (DragDropEffects.Copy | DragDropEffects.Link | DragDropEffects.Move);
if (e.DragEffects == DragDropEffects.None)
e.DragEffects = DragDropEffects.Copy;
e.Handled = true;
}
private void OnDrop(object? sender, DragEventArgs e)
{
var dt = e.DataTransfer;
var files = dt.TryGetValues(DataFormat.File)?.ToArray() ?? System.Array.Empty<IStorageItem>();
var text = dt.TryGetValue(DataFormat.Text);
if (files.Length > 0)
DropStatus.Text = $"{files.Length} file(s):\n" +
string.Join('\n', files.Select(f => f.Path.LocalPath));
else if (!string.IsNullOrEmpty(text))
DropStatus.Text = $"text: {text}";
else
DropStatus.Text = "(unknown payload)";
e.Handled = true;
}
private void OnKeyReadout(object? sender, KeyEventArgs e)
{
var symbol = e.KeySymbol is { Length: > 0 } s ? $"\"{s}\"" : "null";
KeyReadout.Text =
$"Key={e.Key} Physical={e.PhysicalKey} Symbol={symbol}\n" +
$"Modifiers={e.KeyModifiers}";
}
private void OnAvButtonClick(object? sender, RoutedEventArgs e)
{
AvClickCount.Text = $"Clicked {++_clicks} times";
}
private void OnSliderPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == Slider.ValueProperty)
AvSliderValue.Text = $"Slider: {AvSlider.Value:F0}";
}
private void OnInkPointerPressed(object? sender, PointerPressedEventArgs e)
{
AddDot(e.GetPosition(InkCanvas), Brushes.Red);
_pointCount++;
e.Pointer.Capture(InkCanvas);
UpdateStats();
e.Handled = true;
}
private void OnInkPointerMoved(object? sender, PointerEventArgs e)
{
var intermediates = e.GetIntermediatePoints(InkCanvas);
for (var i = 0; i < intermediates.Count; i++)
AddDot(intermediates[i].Position, Brushes.Orange);
_intermediateCount += intermediates.Count;
AddDot(e.GetPosition(InkCanvas), Brushes.Green);
_pointCount++;
UpdateStats();
}
private void OnInkPointerReleased(object? sender, PointerReleasedEventArgs e)
{
AddDot(e.GetPosition(InkCanvas), Brushes.Blue);
_pointCount++;
UpdateStats();
e.Handled = true;
}
private void AddDot(Point position, IBrush brush)
{
const double size = 6;
var dot = new Ellipse
{
Width = size,
Height = size,
Fill = brush,
};
Canvas.SetLeft(dot, position.X - size / 2);
Canvas.SetTop(dot, position.Y - size / 2);
InkCanvas.Children.Add(dot);
}
private void OnClearInk(object? sender, RoutedEventArgs e)
{
InkCanvas.Children.Clear();
_pointCount = 0;
_intermediateCount = 0;
UpdateStats();
}
private void UpdateStats()
{
InkStats.Text = $"{_pointCount} points / {_intermediateCount} intermediate";
}
}

114
samples/WinUIEmbedSample/MainWindow.xaml

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="utf-8"?>
<Window
x:Class="WinUIEmbedSample.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="WinUI + Avalonia side by side">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Left: native WinUI controls -->
<Border Grid.Column="0" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<ScrollViewer Padding="16">
<StackPanel Spacing="8">
<TextBlock Text="WinUI"
FontSize="22"
FontWeight="SemiBold"
Margin="0,0,0,4" />
<Button x:Name="WinUiButton"
Content="WinUI button"
HorizontalAlignment="Stretch"
Click="WinUiButton_Click" />
<TextBlock x:Name="WinUiClickCount" Text="Clicked 0 times" />
<Slider x:Name="WinUiSlider"
Minimum="0"
Maximum="100"
Value="25"
ValueChanged="WinUiSlider_ValueChanged" />
<TextBlock x:Name="WinUiSliderValue" Text="Slider: 25" />
<TextBox PlaceholderText="Type something…"
Text="Hello from WinUI" />
<ToggleSwitch Header="ToggleSwitch" IsOn="True" />
<TextBlock Text="Drag source (native WinUI baseline)"
FontWeight="SemiBold"
Margin="0,16,0,0" />
<TextBlock Text="Drag the box below — payload: 'Hello from WinUI'. Drop into Notepad, this TextBox, or the Avalonia drop target. Use this to compare against the Avalonia drag source."
TextWrapping="Wrap"
Opacity="0.7"
FontSize="12" />
<Border x:Name="WinUiDragSource"
CanDrag="True"
Height="40"
BorderThickness="1"
CornerRadius="4"
Padding="8"
DragStarting="WinUiDragSource_DragStarting"
BorderBrush="{ThemeResource SystemControlForegroundBaseMediumLowBrush}"
Background="{ThemeResource SystemControlBackgroundAltHighBrush}">
<TextBlock Text="Drag me (native WinUI)"
VerticalAlignment="Center" />
</Border>
<TextBlock Text="Theme" Margin="0,8,0,0" />
<ComboBox x:Name="WinUiThemeCombo"
SelectedIndex="2"
HorizontalAlignment="Stretch"
SelectionChanged="WinUiThemeCombo_SelectionChanged">
<x:String>Light</x:String>
<x:String>Dark</x:String>
<x:String>System</x:String>
</ComboBox>
<TextBlock Text="Automation diagnostics"
FontWeight="SemiBold"
Margin="0,16,0,0" />
<TextBlock Text="Verifies the WinUI automation peer over the Avalonia content. Click Probe peer at different lifecycle points to confirm reachability; the scale readout updates live."
TextWrapping="Wrap"
Opacity="0.7"
FontSize="12" />
<Button x:Name="ProbePeerButton"
Content="Probe peer + log children"
HorizontalAlignment="Stretch"
Click="ProbePeerButton_Click" />
<Button x:Name="ProbeDeepButton"
Content="Probe deep (walk full tree)"
HorizontalAlignment="Stretch"
Click="ProbeDeepButton_Click" />
<Button x:Name="ProbeWorkerButton"
Content="Probe deep on worker thread"
HorizontalAlignment="Stretch"
Click="ProbeWorkerButton_Click" />
<TextBlock x:Name="ScaleReadout"
Text="Scales: (waiting for Loaded)"
FontFamily="Consolas"
FontSize="12"
TextWrapping="Wrap" />
<TextBox x:Name="DiagnosticsLog"
IsReadOnly="True"
AcceptsReturn="True"
TextWrapping="Wrap"
FontFamily="Consolas"
FontSize="11"
Height="160"
Text="" />
</StackPanel>
</ScrollViewer>
</Border>
<!-- Right: Avalonia content embedded via AvaloniaSwapChainPanel -->
<avalonia:AvaloniaSwapChainPanel Grid.Column="1" x:Name="AvaloniaPanel" />
</Grid>
</Window>

175
samples/WinUIEmbedSample/MainWindow.xaml.cs

@ -0,0 +1,175 @@
using System;
using System.Text;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using global::Avalonia.Styling;
using AvApplication = global::Avalonia.Application;
namespace WinUIEmbedSample
{
public sealed partial class MainWindow : Microsoft.UI.Xaml.Window
{
private int _clicks;
public MainWindow()
{
InitializeComponent();
App.Lifetime.MainView = new EmbeddedView();
AvaloniaPanel.Content = App.Lifetime.MainView;
// Probe the automation peer *before* the panel is loaded — answers the
// open question of whether GetChildren() works before _root.Prepare().
AppendDiagnostic("ctor: " + DescribePeer());
AvaloniaPanel.Loading += (_, _) => AppendDiagnostic("Loading: " + DescribePeer());
AvaloniaPanel.Loaded += (_, _) =>
{
AppendDiagnostic("Loaded: " + DescribePeer());
};
}
private string DescribePeer()
{
try
{
var peer = FrameworkElementAutomationPeer.CreatePeerForElement(AvaloniaPanel);
if (peer is null)
return "peer=null";
var children = peer.GetChildren();
var count = children?.Count ?? 0;
var sb = new StringBuilder();
sb.Append("peer=").Append(peer.GetType().Name)
.Append(" class=").Append(peer.GetClassName())
.Append(" childCount=").Append(count);
if (count > 0)
{
sb.Append(" first=[");
var first = children![0];
sb.Append(first.GetClassName()).Append('/').Append(first.GetAutomationControlType());
sb.Append("] firstChildren=").Append(first.GetChildren()?.Count ?? 0);
}
return sb.ToString();
}
catch (Exception ex)
{
return "threw: " + ex.GetType().Name + ": " + ex.Message;
}
}
private void ProbePeerButton_Click(object sender, RoutedEventArgs e)
{
AppendDiagnostic("Probe: " + DescribePeer());
}
private async void ProbeWorkerButton_Click(object sender, RoutedEventArgs e)
{
// Cache the peer on the UI thread (CreatePeerForElement is UI-thread bound).
var peer = FrameworkElementAutomationPeer.CreatePeerForElement(AvaloniaPanel);
// Walk on a worker thread — mimics how UIA queries (it calls from a separate thread).
var result = await System.Threading.Tasks.Task.Run(() =>
{
var sb = new StringBuilder();
sb.Append("Worker-thread walk (TID=").Append(Environment.CurrentManagedThreadId).Append("):\n");
try { WalkPeer(peer, 0, sb, maxDepth: 20); }
catch (Exception ex) { sb.Append("THREW: ").Append(ex); }
return sb.ToString();
});
AppendDiagnostic(result);
}
private void ProbeDeepButton_Click(object sender, RoutedEventArgs e)
{
try
{
var peer = FrameworkElementAutomationPeer.CreatePeerForElement(AvaloniaPanel);
var sb = new StringBuilder();
sb.Append("Deep walk:\n");
WalkPeer(peer, 0, sb, maxDepth: 20);
AppendDiagnostic(sb.ToString());
}
catch (Exception ex)
{
AppendDiagnostic("Deep probe threw: " + ex.Message);
}
}
private static void WalkPeer(AutomationPeer peer, int depth, StringBuilder sb, int maxDepth)
{
if (peer is null || depth > maxDepth)
return;
for (var i = 0; i < depth; i++) sb.Append(" ");
var className = peer.GetClassName();
var name = peer.GetName();
var type = peer.GetAutomationControlType();
var children = peer.GetChildren();
sb.Append('[').Append(className).Append(']')
.Append(" type=").Append(type)
.Append(" name=\"").Append(name).Append('"')
.Append(" children=").Append(children?.Count ?? 0)
.Append('\n');
if (children is null) return;
foreach (var child in children)
WalkPeer(child, depth + 1, sb, maxDepth);
}
private void AppendDiagnostic(string line)
{
if (DiagnosticsLog is null)
return;
DiagnosticsLog.Text += "[" + DateTime.Now.ToString("HH:mm:ss.fff") + "] " + line + "\n";
}
private void WinUiButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
WinUiClickCount.Text = $"Clicked {++_clicks} times";
}
private void WinUiDragSource_DragStarting(
Microsoft.UI.Xaml.UIElement sender,
Microsoft.UI.Xaml.DragStartingEventArgs e)
{
e.Data.SetText("Hello from WinUI");
e.AllowedOperations =
Windows.ApplicationModel.DataTransfer.DataPackageOperation.Copy |
Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move;
}
private void WinUiSlider_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)
{
if (WinUiSliderValue is not null)
WinUiSliderValue.Text = $"Slider: {e.NewValue:F0}";
}
private void WinUiThemeCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var selected = WinUiThemeCombo.SelectedIndex;
if (Content is FrameworkElement root)
{
root.RequestedTheme = selected switch
{
0 => ElementTheme.Light,
1 => ElementTheme.Dark,
_ => ElementTheme.Default
};
}
if (AvApplication.Current is { } avApp)
{
avApp.RequestedThemeVariant = selected switch
{
0 => ThemeVariant.Light,
1 => ThemeVariant.Dark,
_ => ThemeVariant.Default
};
}
}
}
}

51
samples/WinUIEmbedSample/Package.appxmanifest

@ -0,0 +1,51 @@
<?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"
IgnorableNamespaces="uap rescap">
<Identity
Name="9f5f4d4e-5b3a-4d7c-9c2a-1f0e1a2b3c4d"
Publisher="CN=AvaloniaUI"
Version="1.0.0.0" />
<mp:PhoneIdentity PhoneProductId="9f5f4d4e-5b3a-4d7c-9c2a-1f0e1a2b3c4d" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>WinUIEmbedSample</DisplayName>
<PublisherDisplayName>AvaloniaUI</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="WinUIEmbedSample"
Description="Side-by-side WinUI and Avalonia controls"
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" />
</Capabilities>
</Package>

10
samples/WinUIEmbedSample/Properties/launchSettings.json

@ -0,0 +1,10 @@
{
"profiles": {
"WinUIEmbedSample (Package)": {
"commandName": "MsixPackage"
},
"WinUIEmbedSample (Unpackaged)": {
"commandName": "Project"
}
}
}

66
samples/WinUIEmbedSample/WinUIEmbedSample.csproj

@ -0,0 +1,66 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>$(AvsCurrentWinUITargetFramework)</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>WinUIEmbedSample</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>
<IncludeAvaloniaGenerators>true</IncludeAvaloniaGenerators>
</PropertyGroup>
<ItemGroup>
<Content Include="..\ControlGallery.WinUI\Assets\SplashScreen.scale-200.png" Link="Assets\SplashScreen.scale-200.png" />
<Content Include="..\ControlGallery.WinUI\Assets\LockScreenLogo.scale-200.png" Link="Assets\LockScreenLogo.scale-200.png" />
<Content Include="..\ControlGallery.WinUI\Assets\Square150x150Logo.scale-200.png" Link="Assets\Square150x150Logo.scale-200.png" />
<Content Include="..\ControlGallery.WinUI\Assets\Square44x44Logo.scale-200.png" Link="Assets\Square44x44Logo.scale-200.png" />
<Content Include="..\ControlGallery.WinUI\Assets\Square44x44Logo.targetsize-24_altform-unplated.png" Link="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="..\ControlGallery.WinUI\Assets\StoreLogo.png" Link="Assets\StoreLogo.png" />
<Content Include="..\ControlGallery.WinUI\Assets\Wide310x150Logo.scale-200.png" Link="Assets\Wide310x150Logo.scale-200.png" />
</ItemGroup>
<ItemGroup>
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<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="..\..\packages\Avalonia\Avalonia.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.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" />
<ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
</ItemGroup>
<Import Project="..\..\build\SourceGenerators.props" />
<Import Project="..\..\build\BuildTargets.targets" />
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
</PropertyGroup>
<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>

16
samples/WinUIEmbedSample/app.manifest

@ -0,0 +1,16 @@
<?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="WinUIEmbedSample.app"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<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>

1
src/Avalonia.Base/Avalonia.Base.csproj

@ -50,6 +50,7 @@
<InternalsVisibleTo Include="Avalonia.Skia.UnitTests, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Skia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.UnitTests, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Win32, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Win32, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.WinUI, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Android, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Android, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.iOS, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.iOS, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Tizen, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Tizen, PublicKey=$(AvaloniaPublicKey)" />

5
src/Avalonia.Base/Logging/LogArea.cs

@ -49,6 +49,11 @@ namespace Avalonia.Logging
/// The log event comes from Win32 Platform. /// The log event comes from Win32 Platform.
/// </summary> /// </summary>
public const string Win32Platform = nameof(Win32Platform); public const string Win32Platform = nameof(Win32Platform);
/// <summary>
/// The log event comes from the WinUI Platform.
/// </summary>
public const string WinUIPlatform = nameof(WinUIPlatform);
/// <summary> /// <summary>
/// The log event comes from X11 Platform. /// The log event comes from X11 Platform.

1
src/Windows/Avalonia.Win32/Avalonia.Win32.csproj

@ -34,6 +34,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<InternalsVisibleTo Include="Avalonia.Win32.Interoperability, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="Avalonia.Win32.Interoperability, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.WinUI, PublicKey=$(AvaloniaPublicKey)" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<!-- By default, any projects supports Windows, Linux, MacOS platforms. --> <!-- By default, any projects supports Windows, Linux, MacOS platforms. -->

252
src/Windows/Avalonia.Win32/OpenGl/Angle/SwapChainGlSurface.cs

@ -0,0 +1,252 @@
using System;
using System.Runtime.InteropServices;
using Avalonia.OpenGL;
using Avalonia.OpenGL.Egl;
using Avalonia.OpenGL.Surfaces;
using Avalonia.Platform;
using Avalonia.Win32.DirectX;
using MicroCom.Runtime;
namespace Avalonia.Win32.OpenGl.Angle;
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, () =>
{
try
{
_swapChain.Present(1, 0);
}
finally
{
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();
}
}

122
src/Windows/Avalonia.Win32/SwapChainTopLevelImpl.cs

@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.OpenGL.Surfaces;
using Avalonia.Platform;
using Avalonia.Platform.Surfaces;
using Avalonia.Rendering.Composition;
namespace Avalonia.Win32;
/// <summary>
/// A minimal <see cref="ITopLevelImpl"/> that hosts an Avalonia content tree on top of an
/// externally managed swap-chain surface (e.g. a WinUI <c>SwapChainPanel</c> or any other
/// host that supplies an <see cref="IGlPlatformSurface"/>). Sizing, scaling and input
/// pumping are driven by the host.
/// </summary>
internal class SwapChainTopLevelImpl : ITopLevelImpl
{
private readonly IGlPlatformSurface _glSurface;
private Size _clientSize;
private double _scaling = 1.0;
public SwapChainTopLevelImpl(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);
/// <summary>
/// Raised when Avalonia requests a cursor change. The host is responsible
/// for translating the (host-supplied) <see cref="ICursorImpl"/> into a
/// native cursor and applying it to its surface.
/// </summary>
public Action<ICursorImpl?>? CursorChanged { get; set; }
public void SetCursor(ICursorImpl? cursor) => CursorChanged?.Invoke(cursor);
// Uses overlays instead of popups.
public IPopupImpl? CreatePopup() => null;
public void SetTransparencyLevelHint(IReadOnlyList<WindowTransparencyLevel> transparencyLevels) { }
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { }
/// <summary>
/// Optional IME implementation provided by the host.
/// </summary>
public ITextInputMethodImpl? TextInputMethod { get; set; }
public object? TryGetFeature(Type featureType)
{
if (featureType == typeof(IClipboard))
return AvaloniaLocator.Current.GetService<IClipboard>();
if (featureType == typeof(ITextInputMethodImpl))
return TextInputMethod;
return null;
}
public void Dispose()
{
Closed?.Invoke();
}
}

143
src/Windows/Avalonia.WinUI/Automation/AvaloniaSwapChainPanelAutomationPeer.cs

@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using global::Avalonia.Automation.Provider;
using Microsoft.UI.Xaml.Automation.Peers;
using AvPeer = global::Avalonia.Automation.Peers.AutomationPeer;
using AvControlPeer = global::Avalonia.Automation.Peers.ControlAutomationPeer;
using XamlAutomationPeer = Microsoft.UI.Xaml.Automation.Peers.AutomationPeer;
using XamlPoint = global::Windows.Foundation.Point;
using AvPoint = global::Avalonia.Point;
using AvRect = global::Avalonia.Rect;
namespace Avalonia.WinUI.Automation;
internal sealed partial class AvaloniaSwapChainPanelAutomationPeer : FrameworkElementAutomationPeer
{
private readonly AvaloniaSwapChainPanel _panel;
private IEmbeddedRootProvider? _embeddedRoot;
private AvPeer? _embeddedRootPeer;
public AvaloniaSwapChainPanelAutomationPeer(AvaloniaSwapChainPanel panel) : base(panel)
{
_panel = panel;
}
internal AvaloniaSwapChainPanel Panel => _panel;
protected override string GetClassNameCore() => nameof(AvaloniaSwapChainPanel);
protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Pane;
protected override IList<XamlAutomationPeer> GetChildrenCore()
{
var list = base.GetChildrenCore() ?? new List<XamlAutomationPeer>();
if (TryGetEmbeddedRootPeer() is { } rootPeer)
{
var proxy = AvaloniaToXamlPeerProxy.GetOrCreate(rootPeer, this);
proxy.SetParent(this);
list.Add(proxy);
}
return list;
}
protected override object GetFocusedElementCore()
{
EnsureSubscribed();
var focused = _embeddedRoot?.GetFocus();
return focused is null
? base.GetFocusedElementCore()
: AvaloniaToXamlPeerProxy.GetOrCreate(focused, this);
}
protected override object GetElementFromPointCore(XamlPoint point)
{
EnsureSubscribed();
var hit = _embeddedRoot?.GetPeerFromPoint(new AvPoint(point.X, point.Y));
return hit is null
? base.GetElementFromPointCore(point)
: AvaloniaToXamlPeerProxy.GetOrCreate(hit, this);
}
/// <summary>
/// Translate an Avalonia rectangle (in embedded-root coordinates) to screen pixels.
/// </summary>
internal (double x, double y, double width, double height) TransformAvaloniaRectToScreen(AvRect rect)
{
var transform = _panel.TransformToVisual(null);
var topLeft = transform.TransformPoint(new XamlPoint(rect.X, rect.Y));
var bottomRight = transform.TransformPoint(new XamlPoint(rect.Right, rect.Bottom));
var xamlRoot = _panel.XamlRoot;
var scale = xamlRoot?.RasterizationScale ?? 1.0;
var x = topLeft.X * scale;
var y = topLeft.Y * scale;
var w = (bottomRight.X - topLeft.X) * scale;
var h = (bottomRight.Y - topLeft.Y) * scale;
if (xamlRoot?.ContentIslandEnvironment is { } island)
{
var hwnd = Microsoft.UI.Win32Interop.GetWindowFromWindowId(island.AppWindowId);
if (hwnd != IntPtr.Zero)
{
var pt = new POINT { x = 0, y = 0 };
if (ClientToScreen(hwnd, ref pt))
{
x += pt.x;
y += pt.y;
}
}
}
return (x, y, w, h);
}
private AvPeer? TryGetEmbeddedRootPeer()
{
EnsureSubscribed();
return _embeddedRootPeer;
}
private void EnsureSubscribed()
{
if (_embeddedRootPeer is not null)
return;
var root = _panel.GetEmbeddedRootForAutomation();
if (root is null)
return;
_embeddedRootPeer = AvControlPeer.CreatePeerForElement(root);
_embeddedRoot = _embeddedRootPeer.GetProvider<IEmbeddedRootProvider>();
if (_embeddedRoot is not null)
_embeddedRoot.FocusChanged += OnEmbeddedFocusChanged;
}
private void OnEmbeddedFocusChanged(object? sender, EventArgs e)
{
if (!ListenerExists(AutomationEvents.AutomationFocusChanged))
return;
var focused = _embeddedRoot?.GetFocus();
if (focused is null)
return;
var proxy = AvaloniaToXamlPeerProxy.GetOrCreate(focused, this);
proxy.RaiseAutomationEvent(AutomationEvents.AutomationFocusChanged);
}
[StructLayout(LayoutKind.Sequential)]
private struct POINT
{
public int x;
public int y;
}
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool ClientToScreen(IntPtr hWnd, ref POINT lpPoint);
}

29
src/Windows/Avalonia.WinUI/Automation/AvaloniaToXamlPeerProxy.ExpandCollapse.cs

@ -0,0 +1,29 @@
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Automation.Provider;
using AvExpand = global::Avalonia.Automation.Provider.IExpandCollapseProvider;
using AvExpandState = global::Avalonia.Automation.ExpandCollapseState;
namespace Avalonia.WinUI.Automation;
internal sealed partial class AvaloniaToXamlPeerProxy : IExpandCollapseProvider
{
ExpandCollapseState IExpandCollapseProvider.ExpandCollapseState
{
get
{
var provider = _peer.GetProvider<AvExpand>();
if (provider is null)
return ExpandCollapseState.LeafNode;
return provider.ExpandCollapseState switch
{
AvExpandState.Collapsed => ExpandCollapseState.Collapsed,
AvExpandState.Expanded => ExpandCollapseState.Expanded,
AvExpandState.PartiallyExpanded => ExpandCollapseState.PartiallyExpanded,
_ => ExpandCollapseState.LeafNode,
};
}
}
void IExpandCollapseProvider.Expand() => _peer.GetProvider<AvExpand>()?.Expand();
void IExpandCollapseProvider.Collapse() => _peer.GetProvider<AvExpand>()?.Collapse();
}

9
src/Windows/Avalonia.WinUI/Automation/AvaloniaToXamlPeerProxy.Invoke.cs

@ -0,0 +1,9 @@
using Microsoft.UI.Xaml.Automation.Provider;
using AvInvoke = global::Avalonia.Automation.Provider.IInvokeProvider;
namespace Avalonia.WinUI.Automation;
internal sealed partial class AvaloniaToXamlPeerProxy : IInvokeProvider
{
void IInvokeProvider.Invoke() => _peer.GetProvider<AvInvoke>()?.Invoke();
}

16
src/Windows/Avalonia.WinUI/Automation/AvaloniaToXamlPeerProxy.RangeValue.cs

@ -0,0 +1,16 @@
using Microsoft.UI.Xaml.Automation.Provider;
using AvRange = global::Avalonia.Automation.Provider.IRangeValueProvider;
namespace Avalonia.WinUI.Automation;
internal sealed partial class AvaloniaToXamlPeerProxy : IRangeValueProvider
{
bool IRangeValueProvider.IsReadOnly => _peer.GetProvider<AvRange>()?.IsReadOnly ?? true;
double IRangeValueProvider.Minimum => _peer.GetProvider<AvRange>()?.Minimum ?? 0;
double IRangeValueProvider.Maximum => _peer.GetProvider<AvRange>()?.Maximum ?? 0;
double IRangeValueProvider.Value => _peer.GetProvider<AvRange>()?.Value ?? 0;
double IRangeValueProvider.LargeChange => _peer.GetProvider<AvRange>()?.LargeChange ?? 0;
double IRangeValueProvider.SmallChange => _peer.GetProvider<AvRange>()?.SmallChange ?? 0;
void IRangeValueProvider.SetValue(double value) => _peer.GetProvider<AvRange>()?.SetValue(value);
}

33
src/Windows/Avalonia.WinUI/Automation/AvaloniaToXamlPeerProxy.Scroll.cs

@ -0,0 +1,33 @@
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Automation.Provider;
using AvScroll = global::Avalonia.Automation.Provider.IScrollProvider;
using AvScrollAmount = global::Avalonia.Automation.Provider.ScrollAmount;
namespace Avalonia.WinUI.Automation;
internal sealed partial class AvaloniaToXamlPeerProxy : IScrollProvider, IScrollItemProvider
{
bool IScrollProvider.HorizontallyScrollable => _peer.GetProvider<AvScroll>()?.HorizontallyScrollable ?? false;
double IScrollProvider.HorizontalScrollPercent => _peer.GetProvider<AvScroll>()?.HorizontalScrollPercent ?? -1;
double IScrollProvider.HorizontalViewSize => _peer.GetProvider<AvScroll>()?.HorizontalViewSize ?? 100;
bool IScrollProvider.VerticallyScrollable => _peer.GetProvider<AvScroll>()?.VerticallyScrollable ?? false;
double IScrollProvider.VerticalScrollPercent => _peer.GetProvider<AvScroll>()?.VerticalScrollPercent ?? -1;
double IScrollProvider.VerticalViewSize => _peer.GetProvider<AvScroll>()?.VerticalViewSize ?? 100;
void IScrollProvider.Scroll(ScrollAmount horizontalAmount, ScrollAmount verticalAmount)
=> _peer.GetProvider<AvScroll>()?.Scroll(ToAvalonia(horizontalAmount), ToAvalonia(verticalAmount));
void IScrollProvider.SetScrollPercent(double horizontalPercent, double verticalPercent)
=> _peer.GetProvider<AvScroll>()?.SetScrollPercent(horizontalPercent, verticalPercent);
void IScrollItemProvider.ScrollIntoView() => _peer.BringIntoView();
private static AvScrollAmount ToAvalonia(ScrollAmount amount) => amount switch
{
ScrollAmount.LargeDecrement => AvScrollAmount.LargeDecrement,
ScrollAmount.SmallDecrement => AvScrollAmount.SmallDecrement,
ScrollAmount.LargeIncrement => AvScrollAmount.LargeIncrement,
ScrollAmount.SmallIncrement => AvScrollAmount.SmallIncrement,
_ => AvScrollAmount.NoAmount,
};
}

51
src/Windows/Avalonia.WinUI/Automation/AvaloniaToXamlPeerProxy.Selection.cs

@ -0,0 +1,51 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Automation.Provider;
using AvPeer = global::Avalonia.Automation.Peers.AutomationPeer;
using AvSelection = global::Avalonia.Automation.Provider.ISelectionProvider;
using AvSelectionItem = global::Avalonia.Automation.Provider.ISelectionItemProvider;
namespace Avalonia.WinUI.Automation;
internal sealed partial class AvaloniaToXamlPeerProxy : ISelectionProvider, ISelectionItemProvider
{
bool ISelectionProvider.CanSelectMultiple => _peer.GetProvider<AvSelection>()?.CanSelectMultiple ?? false;
bool ISelectionProvider.IsSelectionRequired => _peer.GetProvider<AvSelection>()?.IsSelectionRequired ?? false;
IRawElementProviderSimple[] ISelectionProvider.GetSelection()
{
var provider = _peer.GetProvider<AvSelection>();
if (provider is null)
return System.Array.Empty<IRawElementProviderSimple>();
var list = new List<IRawElementProviderSimple>();
foreach (var avChild in provider.GetSelection() ?? (IReadOnlyList<AvPeer>)System.Array.Empty<AvPeer>())
{
var proxy = GetOrCreate(avChild, _host);
if (ProviderFromPeer(proxy) is IRawElementProviderSimple simple)
list.Add(simple);
}
return list.ToArray();
}
bool ISelectionItemProvider.IsSelected => _peer.GetProvider<AvSelectionItem>()?.IsSelected ?? false;
IRawElementProviderSimple? ISelectionItemProvider.SelectionContainer
{
get
{
var container = _peer.GetProvider<AvSelectionItem>()?.SelectionContainer;
if (container is AvPeer containerPeer)
{
var proxy = GetOrCreate(containerPeer, _host);
return ProviderFromPeer(proxy);
}
return null;
}
}
void ISelectionItemProvider.AddToSelection() => _peer.GetProvider<AvSelectionItem>()?.AddToSelection();
void ISelectionItemProvider.RemoveFromSelection() => _peer.GetProvider<AvSelectionItem>()?.RemoveFromSelection();
void ISelectionItemProvider.Select() => _peer.GetProvider<AvSelectionItem>()?.Select();
}

27
src/Windows/Avalonia.WinUI/Automation/AvaloniaToXamlPeerProxy.Toggle.cs

@ -0,0 +1,27 @@
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Automation.Provider;
using AvToggle = global::Avalonia.Automation.Provider.IToggleProvider;
using AvToggleState = global::Avalonia.Automation.Provider.ToggleState;
namespace Avalonia.WinUI.Automation;
internal sealed partial class AvaloniaToXamlPeerProxy : IToggleProvider
{
ToggleState IToggleProvider.ToggleState
{
get
{
var provider = _peer.GetProvider<AvToggle>();
if (provider is null)
return ToggleState.Indeterminate;
return provider.ToggleState switch
{
AvToggleState.Off => ToggleState.Off,
AvToggleState.On => ToggleState.On,
_ => ToggleState.Indeterminate,
};
}
}
void IToggleProvider.Toggle() => _peer.GetProvider<AvToggle>()?.Toggle();
}

13
src/Windows/Avalonia.WinUI/Automation/AvaloniaToXamlPeerProxy.Value.cs

@ -0,0 +1,13 @@
using Microsoft.UI.Xaml.Automation.Provider;
using AvValue = global::Avalonia.Automation.Provider.IValueProvider;
namespace Avalonia.WinUI.Automation;
internal sealed partial class AvaloniaToXamlPeerProxy : IValueProvider
{
bool IValueProvider.IsReadOnly => _peer.GetProvider<AvValue>()?.IsReadOnly ?? true;
string IValueProvider.Value => _peer.GetProvider<AvValue>()?.Value ?? string.Empty;
void IValueProvider.SetValue(string value) => _peer.GetProvider<AvValue>()?.SetValue(value);
}

141
src/Windows/Avalonia.WinUI/Automation/AvaloniaToXamlPeerProxy.cs

@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using global::Avalonia.Controls.Automation.Peers;
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Automation.Peers;
using AvPeer = global::Avalonia.Automation.Peers.AutomationPeer;
using XamlAutomationPeer = Microsoft.UI.Xaml.Automation.Peers.AutomationPeer;
using XamlRect = global::Windows.Foundation.Rect;
namespace Avalonia.WinUI.Automation;
/// <summary>
/// Wraps an Avalonia <see cref="AvPeer"/> so it can be exposed to Microsoft UI Automation
/// through WinUI XAML's automation infrastructure. Mirrors the role of
/// <c>Avalonia.Win32.Automation.AutomationNode</c> but for WinUI hosts.
/// </summary>
internal sealed partial class AvaloniaToXamlPeerProxy : XamlAutomationPeer
{
private static readonly ConditionalWeakTable<AvPeer, AvaloniaToXamlPeerProxy> s_cache = new();
private static long s_nextId;
private readonly AvaloniaSwapChainPanelAutomationPeer _host;
private readonly AvPeer _peer;
private readonly bool _isEmbeddedRoot;
private readonly string _uniqueId;
private AvaloniaToXamlPeerProxy(AvPeer peer, AvaloniaSwapChainPanelAutomationPeer host)
{
_peer = peer;
_host = host;
_isEmbeddedRoot = peer is EmbeddableControlRootAutomationPeer;
_uniqueId = "Avalonia.WinUI.Peer:" + System.Threading.Interlocked.Increment(ref s_nextId).ToString(
System.Globalization.CultureInfo.InvariantCulture);
}
internal AvPeer Peer => _peer;
public static AvaloniaToXamlPeerProxy GetOrCreate(AvPeer peer, AvaloniaSwapChainPanelAutomationPeer host)
{
return s_cache.GetValue(peer, p => new AvaloniaToXamlPeerProxy(p, host));
}
internal static AvaloniaToXamlPeerProxy? TryGet(AvPeer? peer)
{
if (peer is null)
return null;
return s_cache.TryGetValue(peer, out var proxy) ? proxy : null;
}
protected override IList<XamlAutomationPeer> GetChildrenCore()
{
var list = new List<XamlAutomationPeer>();
var children = _peer.GetChildren();
if (children is null)
return list;
foreach (var child in children)
{
var childProxy = GetOrCreate(child, _host);
childProxy.SetParent(this);
list.Add(childProxy);
}
return list;
}
protected override string GetClassNameCore() => _peer.GetClassName() ?? string.Empty;
protected override string GetNameCore() => _peer.GetName() ?? string.Empty;
protected override string GetAutomationIdCore() => _peer.GetAutomationId() ?? string.Empty;
protected override string GetHelpTextCore() => _peer.GetHelpText() ?? string.Empty;
protected override string GetAcceleratorKeyCore() => _peer.GetAcceleratorKey() ?? string.Empty;
protected override string GetAccessKeyCore() => _peer.GetAccessKey() ?? string.Empty;
protected override string GetItemStatusCore() => _peer.GetItemStatus() ?? string.Empty;
protected override string GetItemTypeCore() => _peer.GetItemType() ?? string.Empty;
protected override string GetLocalizedControlTypeCore()
=> _peer.GetLocalizedControlType() ?? base.GetLocalizedControlTypeCore();
protected override AutomationControlType GetAutomationControlTypeCore()
=> ControlTypeMap.ToXaml(_peer.GetAutomationControlType());
protected override XamlRect GetBoundingRectangleCore()
{
var rect = _peer.GetBoundingRectangle();
if (rect.Width <= 0 && rect.Height <= 0)
return default;
var (screenX, screenY, w, h) = _host.TransformAvaloniaRectToScreen(rect);
return new XamlRect(screenX, screenY, w, h);
}
protected override bool HasKeyboardFocusCore() => _peer.HasKeyboardFocus();
protected override bool IsKeyboardFocusableCore() => _peer.IsKeyboardFocusable();
protected override bool IsEnabledCore() => _peer.IsEnabled();
// Collapse the redundant Pane → Pane: the embedded root sits beneath the
// panel's own peer and adds nothing to the screen reader's view.
protected override bool IsContentElementCore() => !_isEmbeddedRoot && _peer.IsContentElement();
protected override bool IsControlElementCore() => !_isEmbeddedRoot && _peer.IsControlElement();
protected override bool IsOffscreenCore() => _peer.IsOffscreen();
protected override void SetFocusCore() => _peer.SetFocus();
protected override object GetPatternCore(PatternInterface patternInterface)
{
return patternInterface switch
{
PatternInterface.Invoke
when _peer.GetProvider<global::Avalonia.Automation.Provider.IInvokeProvider>() is not null => this,
PatternInterface.Toggle
when _peer.GetProvider<global::Avalonia.Automation.Provider.IToggleProvider>() is not null => this,
PatternInterface.Value
when _peer.GetProvider<global::Avalonia.Automation.Provider.IValueProvider>() is not null => this,
PatternInterface.RangeValue
when _peer.GetProvider<global::Avalonia.Automation.Provider.IRangeValueProvider>() is not null => this,
PatternInterface.Scroll
when _peer.GetProvider<global::Avalonia.Automation.Provider.IScrollProvider>() is not null => this,
// Every peer supports BringIntoView, so ScrollItem is always available.
PatternInterface.ScrollItem => this,
PatternInterface.Selection
when _peer.GetProvider<global::Avalonia.Automation.Provider.ISelectionProvider>() is not null => this,
PatternInterface.SelectionItem
when _peer.GetProvider<global::Avalonia.Automation.Provider.ISelectionItemProvider>() is not null => this,
PatternInterface.ExpandCollapse
when _peer.GetProvider<global::Avalonia.Automation.Provider.IExpandCollapseProvider>() is not null => this,
_ => null!,
};
}
}

52
src/Windows/Avalonia.WinUI/Automation/ControlTypeMap.cs

@ -0,0 +1,52 @@
using AvControlType = global::Avalonia.Automation.Peers.AutomationControlType;
using XamlControlType = Microsoft.UI.Xaml.Automation.Peers.AutomationControlType;
namespace Avalonia.WinUI.Automation;
internal static class ControlTypeMap
{
public static XamlControlType ToXaml(AvControlType type) => type switch
{
AvControlType.Button => XamlControlType.Button,
AvControlType.Calendar => XamlControlType.Calendar,
AvControlType.CheckBox => XamlControlType.CheckBox,
AvControlType.ComboBox => XamlControlType.ComboBox,
AvControlType.ComboBoxItem => XamlControlType.ListItem,
AvControlType.Edit => XamlControlType.Edit,
AvControlType.Hyperlink => XamlControlType.Hyperlink,
AvControlType.Image => XamlControlType.Image,
AvControlType.ListItem => XamlControlType.ListItem,
AvControlType.List => XamlControlType.List,
AvControlType.Menu => XamlControlType.Menu,
AvControlType.MenuBar => XamlControlType.MenuBar,
AvControlType.MenuItem => XamlControlType.MenuItem,
AvControlType.ProgressBar => XamlControlType.ProgressBar,
AvControlType.RadioButton => XamlControlType.RadioButton,
AvControlType.ScrollBar => XamlControlType.ScrollBar,
AvControlType.Slider => XamlControlType.Slider,
AvControlType.Spinner => XamlControlType.Spinner,
AvControlType.StatusBar => XamlControlType.StatusBar,
AvControlType.Tab => XamlControlType.Tab,
AvControlType.TabItem => XamlControlType.TabItem,
AvControlType.Text => XamlControlType.Text,
AvControlType.ToolBar => XamlControlType.ToolBar,
AvControlType.ToolTip => XamlControlType.ToolTip,
AvControlType.Tree => XamlControlType.Tree,
AvControlType.TreeItem => XamlControlType.TreeItem,
AvControlType.Group => XamlControlType.Group,
AvControlType.Thumb => XamlControlType.Thumb,
AvControlType.DataGrid => XamlControlType.DataGrid,
AvControlType.DataItem => XamlControlType.DataItem,
AvControlType.Document => XamlControlType.Document,
AvControlType.SplitButton => XamlControlType.SplitButton,
AvControlType.Window => XamlControlType.Window,
AvControlType.Pane => XamlControlType.Pane,
AvControlType.Header => XamlControlType.Header,
AvControlType.HeaderItem => XamlControlType.HeaderItem,
AvControlType.Table => XamlControlType.Table,
AvControlType.TitleBar => XamlControlType.TitleBar,
AvControlType.Separator => XamlControlType.Separator,
AvControlType.Expander => XamlControlType.Group,
_ => XamlControlType.Custom,
};
}

30
src/Windows/Avalonia.WinUI/Avalonia.WinUI.csproj

@ -0,0 +1,30 @@
<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>
<PackageDescription>Windows App SDK (WinUI 3) integration for Avalonia, enabling Avalonia content to be hosted inside WinUI applications on Windows.</PackageDescription>
</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>

915
src/Windows/Avalonia.WinUI/AvaloniaSwapChainPanel.cs

@ -0,0 +1,915 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using global::Avalonia;
using global::Avalonia.Controls.Embedding;
using global::Avalonia.Input;
using global::Avalonia.Input.Raw;
using global::Avalonia.Logging;
using global::Avalonia.Platform;
using global::Avalonia.Win32;
using global::Avalonia.Win32.OpenGl.Angle;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Windows.Foundation;
using Microsoft.UI.Input;
using WinUIPointerPoint = Microsoft.UI.Input.PointerPoint;
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 SwapChainTopLevelImpl? _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;
private static readonly List<RawPointerPoint> s_intermediatePoints = new();
private WinUITextInputMethod? _textInputMethod;
private bool _ignoreCharacterReceived;
private readonly CursorOverlay _cursorOverlay;
// ProtectedCursor is protected on UIElement; derive to expose it.
private sealed partial class CursorOverlay : Microsoft.UI.Xaml.Controls.Grid
{
public void SetCursor(Microsoft.UI.Input.InputCursor? cursor) => ProtectedCursor = cursor;
}
static AvaloniaSwapChainPanel()
{
AvaloniaLocator.CurrentMutable
.Bind<global::Avalonia.Input.Platform.IPlatformDragSource>()
.ToSingleton<WinUIDragSource>();
}
// Looking up panels from the drag source: keyed by the Avalonia toplevel
// PlatformImpl so multi-panel hosts work.
private static readonly System.Collections.Concurrent.ConcurrentDictionary<
global::Avalonia.Platform.ITopLevelImpl, AvaloniaSwapChainPanel> s_panelsByImpl = new();
internal static AvaloniaSwapChainPanel? GetPanelFor(global::Avalonia.Platform.ITopLevelImpl impl)
=> s_panelsByImpl.TryGetValue(impl, out var p) ? p : null;
// Most recent WinUI PointerPoint observed by the panel — needed by
// StartDragAsync. Avalonia's IPointer.Id is assigned sequentially and
// does not correspond to WinUI's PointerId, so we can't look up per
// pointer; the latest is good enough for the dominant mouse-drag case.
private WinUIPointerPoint? _lastPointerPoint;
// Cleared in DragStarting and OnUnloaded.
private IDataTransfer? _outgoingDragData;
private DragDropEffects _outgoingDragAllowed;
public AvaloniaSwapChainPanel()
{
IsTabStop = true;
// SwapChainPanel disallows Background, but without a hit-testable
// surface WinUI won't resolve ProtectedCursor for the panel. Add a
// transparent overlay that fills the panel — it costs nothing
// visually but participates in cursor hit-testing.
_cursorOverlay = new CursorOverlay
{
Background = new Microsoft.UI.Xaml.Media.SolidColorBrush(Microsoft.UI.Colors.Transparent),
IsHitTestVisible = true,
AllowDrop = true,
CanDrag = true,
};
_cursorOverlay.DragEnter += OnDragEnter;
_cursorOverlay.DragOver += OnDragOver;
_cursorOverlay.DragLeave += OnDragLeave;
_cursorOverlay.Drop += OnDrop;
_cursorOverlay.DragStarting += OnDragStarting;
Children.Add(_cursorOverlay);
Loaded += OnLoaded;
Unloaded += OnUnloaded;
SizeChanged += OnSizeChanged;
CompositionScaleChanged += OnCompositionScaleChanged;
PointerPressed += OnPointerPressed;
PointerMoved += OnPointerMoved;
PointerReleased += OnPointerReleased;
PointerWheelChanged += OnPointerWheelChanged;
PointerCanceled += OnPointerCanceled;
PointerEntered += OnPointerEntered;
PointerExited += OnPointerExited;
GotFocus += OnGotFocus;
LostFocus += OnLostFocus;
AddHandler(KeyDownEvent, new KeyEventHandler(OnKeyDown), handledEventsToo: true);
AddHandler(KeyUpEvent, new KeyEventHandler(OnKeyUp), handledEventsToo: true);
AddHandler(CharacterReceivedEvent,
new TypedEventHandler<UIElement, CharacterReceivedRoutedEventArgs>(OnCharacterReceived),
handledEventsToo: true);
}
public AvControl? Content
{
get => _content;
set
{
_content = value;
if (_root is not null)
_root.Content = value;
}
}
protected override Microsoft.UI.Xaml.Automation.Peers.AutomationPeer OnCreateAutomationPeer()
=> new global::Avalonia.WinUI.Automation.AvaloniaSwapChainPanelAutomationPeer(this);
// ---- Diagnostics / internal automation hooks ----
/// <summary>
/// Returns the embedded control root, or null if the panel has not loaded yet.
/// Used by the automation peer to enumerate Avalonia children; also exposed as
/// a diagnostic surface for WinUIEmbedSample to verify peer lifecycle.
/// </summary>
internal global::Avalonia.Controls.Embedding.EmbeddableControlRoot? GetEmbeddedRootForAutomation() => _root;
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 SwapChainTopLevelImpl(_glSurface)
{
ClientSize = new AvSize(ActualWidth, ActualHeight),
RenderScaling = CompositionScaleX
};
_textInputMethod = new WinUITextInputMethod(
this,
() => _topLevelImpl?.Input,
() => _topLevelImpl?.InputRoot,
() => AvaloniaLocator.Current.GetService<IKeyboardDevice>());
_topLevelImpl.TextInputMethod = _textInputMethod;
_topLevelImpl.CursorChanged = OnAvaloniaCursorChanged;
// Create and start the EmbeddableControlRoot
_root = new EmbeddableControlRoot(_topLevelImpl)
{
Content = _content
};
_root.Prepare();
_root.StartRendering();
s_panelsByImpl[_topLevelImpl] = this;
}
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)
{
if (_topLevelImpl is not null)
s_panelsByImpl.TryRemove(_topLevelImpl, out _);
_root?.StopRendering();
_root?.Dispose();
_root = null;
_topLevelImpl = null;
_glSurface?.DisposeSwapChain();
_glSurface = null;
_mouseDevice.Dispose();
_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 static RawPointerPoint CreateRawPointerPoint(WinUIPointerPoint point)
{
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)
{
_lastPointerPoint = e.GetCurrentPoint(this);
if (_topLevelImpl?.Input is not { } input || _topLevelImpl.InputRoot is not { } inputRoot)
return;
var device = GetPointerDevice(e);
var timestamp = GetTimestamp(e);
var rawPoint = CreateRawPointerPoint(e.GetCurrentPoint(this));
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)
{
_lastPointerPoint = e.GetCurrentPoint(this);
if (_topLevelImpl?.Input is not { } input || _topLevelImpl.InputRoot is not { } inputRoot)
return;
var device = GetPointerDevice(e);
var timestamp = GetTimestamp(e);
var rawPoint = CreateRawPointerPoint(e.GetCurrentPoint(this));
var modifiers = GetPointerModifiers(e);
var pointerId = e.Pointer.PointerId;
var type = device is TouchDevice ? RawPointerEventType.TouchUpdate : RawPointerEventType.Move;
var args = CreatePointerArgs(device, timestamp, inputRoot, type, rawPoint, modifiers, pointerId);
args.IntermediatePoints = new Lazy<IReadOnlyList<RawPointerPoint>?>(() => GetIntermediatePoints(e));
input(args);
}
private IReadOnlyList<RawPointerPoint>? GetIntermediatePoints(PointerRoutedEventArgs e)
{
// WinUI returns the points oldest-first and includes the current point as the
// last entry; drop that last entry (it is the one we already dispatched).
var coalesced = e.GetIntermediatePoints(this);
if (coalesced is null || coalesced.Count <= 1)
return null;
s_intermediatePoints.Clear();
if (s_intermediatePoints.Capacity < coalesced.Count - 1)
s_intermediatePoints.Capacity = coalesced.Count - 1;
for (var i = 0; i < coalesced.Count - 1; i++)
s_intermediatePoints.Add(CreateRawPointerPoint(coalesced[i]));
return s_intermediatePoints;
}
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.GetCurrentPoint(this));
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 OnAvaloniaCursorChanged(ICursorImpl? cursor)
{
if (cursor is WinUICursorImpl winUiCursor)
{
_cursorOverlay.SetCursor(winUiCursor.Cursor);
}
else if (cursor is Win32.CursorImpl win32Cursor)
{
_cursorOverlay.SetCursor(MapWin32CursorToWinUI(win32Cursor));
}
else
{
_cursorOverlay.SetCursor(null);
}
}
private static InputCursor? MapWin32CursorToWinUI(Win32.CursorImpl cursor)
{
var handle = cursor.Handle;
if (handle == IntPtr.Zero)
return null;
foreach (StandardCursorType type in Enum.GetValues<StandardCursorType>())
{
var cached = Win32.CursorFactory.Instance.GetCursor(type);
if (cached is Win32.CursorImpl c && c.Handle == handle)
{
var shape = WinUICursorFactory.Instance.GetCursor(type);
return (shape as WinUICursorImpl)?.Cursor;
}
}
return InputSystemCursor.Create(InputSystemCursorShape.Arrow);
}
private void OnGotFocus(object sender, RoutedEventArgs e)
=> _textInputMethod?.OnPanelFocusChanged(true);
private void OnLostFocus(object sender, RoutedEventArgs e)
{
_textInputMethod?.OnPanelFocusChanged(false);
// Clear Avalonia's internal focus so the previously focused control
// doesn't keep its :focus visual once keyboard focus moves to a
// native WinUI element.
if (_root is not null)
global::Avalonia.Input.FocusManager.GetFocusManager(_root)?.Focus(null);
}
private void OnPointerEntered(object sender, PointerRoutedEventArgs e)
{
// Touch never hovers — enter is implied by the press that produced it.
if (e.Pointer.PointerDeviceType == Microsoft.UI.Input.PointerDeviceType.Touch)
return;
if (_topLevelImpl?.Input is not { } input || _topLevelImpl.InputRoot is not { } inputRoot)
return;
var device = GetPointerDevice(e);
var timestamp = GetTimestamp(e);
var rawPoint = CreateRawPointerPoint(e.GetCurrentPoint(this));
var modifiers = GetPointerModifiers(e);
var pointerId = e.Pointer.PointerId;
input(CreatePointerArgs(device, timestamp, inputRoot,
RawPointerEventType.Move, rawPoint, modifiers, pointerId));
}
private void OnPointerExited(object sender, PointerRoutedEventArgs e)
{
if (e.Pointer.PointerDeviceType == Microsoft.UI.Input.PointerDeviceType.Touch)
return;
if (_topLevelImpl?.Input is not { } input || _topLevelImpl.InputRoot is not { } inputRoot)
return;
var device = GetPointerDevice(e);
var timestamp = GetTimestamp(e);
var rawPoint = CreateRawPointerPoint(e.GetCurrentPoint(this));
var modifiers = GetPointerModifiers(e);
var pointerId = e.Pointer.PointerId;
input(CreatePointerArgs(device, timestamp, inputRoot,
RawPointerEventType.LeaveWindow, rawPoint, modifiers, pointerId));
}
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.GetCurrentPoint(this));
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)
=> DispatchKey(e, RawKeyEventType.KeyDown);
private void OnKeyUp(object sender, KeyRoutedEventArgs e)
=> DispatchKey(e, RawKeyEventType.KeyUp);
private void DispatchKey(KeyRoutedEventArgs e, RawKeyEventType type)
{
if (_topLevelImpl?.Input is not { } input || _topLevelImpl.InputRoot is not { } inputRoot)
return;
var (key, physicalKey, keySymbol) = WinUIKeyInterop.Resolve(e.Key, e.KeyStatus);
if (key == Key.None && physicalKey == PhysicalKey.None)
return;
// Tab boundary: when Avalonia would wrap focus inside its toplevel
// (TopLevelHost.TabNavigation = Cycle), forward to WinUI so focus
// escapes to the next/previous native control instead.
if (type == RawKeyEventType.KeyDown && key == Key.Tab && TryEscapeTabBoundary(e))
return;
var keyboard = GetKeyboardDevice();
if (keyboard is null)
return;
var args = new RawKeyEventArgs(keyboard, (ulong)Environment.TickCount64, inputRoot,
type, key, GetCurrentModifiers(), physicalKey, keySymbol);
input(args);
if (type == RawKeyEventType.KeyDown)
_ignoreCharacterReceived = key == Key.ImeProcessed || args.Handled;
else if (type == RawKeyEventType.KeyUp)
_ignoreCharacterReceived = false;
// Only mark handled if Avalonia consumed the event — marking a KeyDown
// handled suppresses the matching CharacterReceived and breaks text input.
if (args.Handled)
e.Handled = true;
}
/// <summary>
/// If the current Avalonia focus is at the first or last tab stop in the
/// embedded content, handle Tab / Shift+Tab here by moving WinUI focus to
/// the next or previous native element. Returns true if the event was
/// consumed (the caller must not forward it to Avalonia).
/// </summary>
private bool TryEscapeTabBoundary(KeyRoutedEventArgs e)
{
if (_root is null)
return false;
var focusManager = global::Avalonia.Input.FocusManager.GetFocusManager(_root);
var current = focusManager?.GetFocusedElement();
if (current is null)
return false;
var shift = global::Microsoft.UI.Input.InputKeyboardSource
.GetKeyStateForCurrentThread(Windows.System.VirtualKey.Shift)
.HasFlag(Windows.UI.Core.CoreVirtualKeyStates.Down);
var first = global::Avalonia.Input.KeyboardNavigationHandler.GetNext(
_root, global::Avalonia.Input.NavigationDirection.Next);
if (first is null)
return false;
bool atBoundary;
if (shift)
{
// At the first tab stop — backward tab would wrap.
atBoundary = ReferenceEquals(current, first);
}
else
{
// At the last tab stop — forward tab would wrap. Cycle mode means
// GetNext from the last returns the first; detect that.
var next = global::Avalonia.Input.KeyboardNavigationHandler.GetNext(
current, global::Avalonia.Input.NavigationDirection.Next);
atBoundary = next is null || ReferenceEquals(next, first);
}
if (!atBoundary)
return false;
var direction = shift
? Microsoft.UI.Xaml.Input.FocusNavigationDirection.Previous
: Microsoft.UI.Xaml.Input.FocusNavigationDirection.Next;
// WinUI Desktop requires SearchRoot — the parameterless TryMoveFocus
// overload only works in UWP. Use the panel's XamlRoot.Content as the
// search scope; that covers the whole window's visual tree.
var searchRoot = XamlRoot?.Content as DependencyObject;
if (searchRoot is null)
{
Logger.TryGet(LogEventLevel.Verbose, LogArea.WinUIPlatform)?.Log(this, "Tab boundary: no XamlRoot.Content available; letting Avalonia handle.");
return false;
}
var options = new Microsoft.UI.Xaml.Input.FindNextElementOptions
{
SearchRoot = searchRoot,
};
var moved = Microsoft.UI.Xaml.Input.FocusManager.TryMoveFocus(direction, options);
if (moved)
{
e.Handled = true;
Logger.TryGet(LogEventLevel.Verbose, LogArea.WinUIPlatform)?.Log(this, $"Tab boundary: moved WinUI focus {direction}.");
return true;
}
Logger.TryGet(LogEventLevel.Verbose, LogArea.WinUIPlatform)?.Log(this, "Tab boundary detected but TryMoveFocus returned false; letting Avalonia handle.");
return false;
}
private void OnCharacterReceived(UIElement sender, CharacterReceivedRoutedEventArgs e)
{
if (_ignoreCharacterReceived)
return;
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;
}
}
// Incoming drag-and-drop
private DataTransfer? _activeDrag;
private async void OnDragEnter(object sender, Microsoft.UI.Xaml.DragEventArgs e)
{
var deferral = e.GetDeferral();
try
{
((IDisposable?)_activeDrag)?.Dispose();
_activeDrag = await BuildDataTransferAsync(e.DataView);
UpdateDragUi(e, RawDragEventType.DragEnter);
}
finally
{
deferral.Complete();
}
}
private void OnDragOver(object sender, Microsoft.UI.Xaml.DragEventArgs e)
=> UpdateDragUi(e, RawDragEventType.DragOver);
private void OnDragLeave(object sender, Microsoft.UI.Xaml.DragEventArgs e)
{
UpdateDragUi(e, RawDragEventType.DragLeave);
((IDisposable?)_activeDrag)?.Dispose();
_activeDrag = null;
}
private void OnDrop(object sender, Microsoft.UI.Xaml.DragEventArgs e)
{
UpdateDragUi(e, RawDragEventType.Drop);
((IDisposable?)_activeDrag)?.Dispose();
_activeDrag = null;
}
private void UpdateDragUi(Microsoft.UI.Xaml.DragEventArgs e, RawDragEventType type)
{
if (_topLevelImpl?.Input is not { } input
|| _topLevelImpl.InputRoot is not { } inputRoot
|| _activeDrag is null)
return;
var device = AvaloniaLocator.Current.GetService<IDragDropDevice>();
if (device is null)
return;
var pt = e.GetPosition(this);
var allowed = (DragDropEffects)(int)e.AllowedOperations;
if (allowed == DragDropEffects.None)
allowed = DragDropEffects.Copy | DragDropEffects.Move | DragDropEffects.Link;
var modifiers = GetCurrentModifiers();
var args = new RawDragEvent(device, type, inputRoot,
new AvPoint(pt.X, pt.Y), _activeDrag, allowed, modifiers);
input(args);
e.AcceptedOperation = (Windows.ApplicationModel.DataTransfer.DataPackageOperation)(int)args.Effects;
e.Handled = true;
}
private static async System.Threading.Tasks.Task<DataTransfer> BuildDataTransferAsync(
Windows.ApplicationModel.DataTransfer.DataPackageView view)
{
var dt = new DataTransfer();
if (view.Contains(Windows.ApplicationModel.DataTransfer.StandardDataFormats.StorageItems))
{
try
{
var items = await view.GetStorageItemsAsync();
foreach (var item in items)
{
var path = item.Path;
if (string.IsNullOrEmpty(path))
continue;
// Reuse Avalonia's BclStorage* via the same helper Win32 uses
// (IVT-granted to Avalonia.WinUI on Avalonia.Base).
if (global::Avalonia.Platform.Storage.FileIO.StorageProviderHelpers
.TryCreateBclStorageItem(path) is { } storage)
{
dt.Add(DataTransferItem.CreateFile(storage));
}
}
}
catch (Exception ex)
{
Logger.TryGet(LogEventLevel.Warning, LogArea.WinUIPlatform)?.Log(null, "Failed to resolve dragged storage items: {Exception}", ex);
}
}
if (view.Contains(Windows.ApplicationModel.DataTransfer.StandardDataFormats.Text))
{
try
{
var text = await view.GetTextAsync();
if (!string.IsNullOrEmpty(text))
dt.Add(DataTransferItem.CreateText(text));
}
catch (Exception ex)
{
Logger.TryGet(LogEventLevel.Warning, LogArea.WinUIPlatform)?.Log(null, "Failed to resolve dragged text: {Exception}", ex);
}
}
return dt;
}
// Outgoing drag — invoked by WinUIDragSource on the UI thread.
internal async Task<DragDropEffects> StartOutgoingDragAsync(
IDataTransfer data, DragDropEffects allowed)
{
if (_lastPointerPoint is not { } pp)
{
Logger.TryGet(LogEventLevel.Warning, LogArea.WinUIPlatform)?.Log(this, "StartOutgoingDragAsync called with no cached PointerPoint; drag suppressed.");
return DragDropEffects.None;
}
_outgoingDragData = data;
_outgoingDragAllowed = allowed;
try
{
var op = await _cursorOverlay.StartDragAsync(pp);
return (DragDropEffects)(int)op;
}
catch (Exception ex)
{
Logger.TryGet(LogEventLevel.Warning, LogArea.WinUIPlatform)?.Log(this, "StartDragAsync threw: {Exception}", ex);
return DragDropEffects.None;
}
finally
{
_outgoingDragData = null;
_outgoingDragAllowed = DragDropEffects.None;
}
}
private async void OnDragStarting(UIElement sender, DragStartingEventArgs e)
{
if (_outgoingDragData is null)
{
// The overlay was the drag origin (CanDrag=true) but no Avalonia
// drag was in flight — most likely a user grab on empty panel
// space. Suppress the native drag rather than emit an empty one.
Logger.TryGet(LogEventLevel.Verbose, LogArea.WinUIPlatform)?.Log(this, "DragStarting cancelled: no active Avalonia drag.");
e.Cancel = true;
return;
}
e.AllowedOperations = (Windows.ApplicationModel.DataTransfer.DataPackageOperation)(int)_outgoingDragAllowed;
var deferral = e.GetDeferral();
try
{
await PopulateDataPackageAsync(e.Data, _outgoingDragData);
}
catch (Exception ex)
{
Logger.TryGet(LogEventLevel.Warning, LogArea.WinUIPlatform)?.Log(this, "Populating outgoing DataPackage threw: {Exception}", ex);
}
finally
{
deferral.Complete();
}
}
private static async Task PopulateDataPackageAsync(
Windows.ApplicationModel.DataTransfer.DataPackage package, IDataTransfer source)
{
// Text — direct copy.
var text = source.TryGetValue(DataFormat.Text);
if (!string.IsNullOrEmpty(text))
package.SetText(text);
// Files — convert Avalonia IStorageItem paths to native
// Windows.Storage.IStorageItem instances via the OS APIs.
var avFiles = source.TryGetValues(DataFormat.File);
if (avFiles is not null)
{
var winuiItems = new System.Collections.Generic.List<Windows.Storage.IStorageItem>();
foreach (var av in avFiles)
{
string? path = null;
if (av.Path is { IsAbsoluteUri: true, Scheme: "file" } uri)
path = uri.LocalPath;
if (string.IsNullOrEmpty(path))
continue;
try
{
Windows.Storage.IStorageItem item =
System.IO.Directory.Exists(path)
? await Windows.Storage.StorageFolder.GetFolderFromPathAsync(path)
: await Windows.Storage.StorageFile.GetFileFromPathAsync(path);
winuiItems.Add(item);
}
catch (Exception ex)
{
Logger.TryGet(LogEventLevel.Warning, LogArea.WinUIPlatform)?.Log(null, "Failed to expose dragged path '{Path}' as Windows.Storage item: {Exception}", path, ex);
}
}
if (winuiItems.Count > 0)
package.SetStorageItems(winuiItems);
}
}
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;
}
}

91
src/Windows/Avalonia.WinUI/WinUICursorFactory.cs

@ -0,0 +1,91 @@
using System;
using System.Collections.Concurrent;
using global::Avalonia;
using global::Avalonia.Input;
using global::Avalonia.Media.Imaging;
using global::Avalonia.Platform;
using Microsoft.UI.Input;
namespace Avalonia.WinUI;
/// <summary>
/// Wraps a WinUI <see cref="InputCursor"/> so it can flow through Avalonia's
/// platform-agnostic cursor APIs.
/// </summary>
internal sealed class WinUICursorImpl : ICursorImpl
{
public WinUICursorImpl(InputCursor? cursor)
{
Cursor = cursor;
}
public InputCursor? Cursor { get; }
public void Dispose()
{
// InputSystemCursor instances are cached — never disposed.
}
}
internal sealed class WinUICursorFactory : ICursorFactory
{
public static WinUICursorFactory Instance { get; } = new();
private readonly ConcurrentDictionary<StandardCursorType, WinUICursorImpl> _cache = new();
private WinUICursorFactory() { }
public ICursorImpl GetCursor(StandardCursorType cursorType)
{
return _cache.GetOrAdd(cursorType, static t =>
{
var shape = MapShape(t);
InputCursor? cursor = shape is { } s ? InputSystemCursor.Create(s) : null;
return new WinUICursorImpl(cursor);
});
}
public ICursorImpl CreateCursor(Bitmap cursor, PixelPoint hotSpot)
{
// WinUI 3 exposes no public API to build an InputCursor from an
// in-memory image (only InputSystemCursor.Create(shape) and
// InputDesktopResourceCursor.CreateFromResource(fileName) are
// available). Fall back to the default arrow so the caller still
// gets a usable cursor.
// TODO: Custom bitmap cursors...
return _cache.GetOrAdd(StandardCursorType.Arrow, static t =>
new WinUICursorImpl(InputSystemCursor.Create(InputSystemCursorShape.Arrow)));
}
private static InputSystemCursorShape? MapShape(StandardCursorType t) => t switch
{
StandardCursorType.None => null,
StandardCursorType.Arrow => InputSystemCursorShape.Arrow,
StandardCursorType.Ibeam => InputSystemCursorShape.IBeam,
StandardCursorType.Wait => InputSystemCursorShape.Wait,
StandardCursorType.AppStarting => InputSystemCursorShape.Wait,
StandardCursorType.Cross => InputSystemCursorShape.Cross,
StandardCursorType.Help => InputSystemCursorShape.Help,
StandardCursorType.Hand => InputSystemCursorShape.Hand,
StandardCursorType.No => InputSystemCursorShape.UniversalNo,
StandardCursorType.SizeAll => InputSystemCursorShape.SizeAll,
StandardCursorType.SizeNorthSouth => InputSystemCursorShape.SizeNorthSouth,
StandardCursorType.SizeWestEast => InputSystemCursorShape.SizeWestEast,
StandardCursorType.UpArrow => InputSystemCursorShape.UpArrow,
StandardCursorType.TopSide => InputSystemCursorShape.SizeNorthSouth,
StandardCursorType.BottomSide => InputSystemCursorShape.SizeNorthSouth,
StandardCursorType.LeftSide => InputSystemCursorShape.SizeWestEast,
StandardCursorType.RightSide => InputSystemCursorShape.SizeWestEast,
StandardCursorType.TopLeftCorner => InputSystemCursorShape.SizeNorthwestSoutheast,
StandardCursorType.BottomRightCorner => InputSystemCursorShape.SizeNorthwestSoutheast,
StandardCursorType.TopRightCorner => InputSystemCursorShape.SizeNortheastSouthwest,
StandardCursorType.BottomLeftCorner => InputSystemCursorShape.SizeNortheastSouthwest,
// No built-in drag cursors — fall back to Arrow; OLE drag operations on
// Win32 traditionally use ole32.dll resources we can't reach from here.
StandardCursorType.DragMove => InputSystemCursorShape.Arrow,
StandardCursorType.DragCopy => InputSystemCursorShape.Arrow,
StandardCursorType.DragLink => InputSystemCursorShape.Arrow,
_ => InputSystemCursorShape.Arrow,
};
}

42
src/Windows/Avalonia.WinUI/WinUIDragSource.cs

@ -0,0 +1,42 @@
using System.Threading.Tasks;
using global::Avalonia.Controls;
using global::Avalonia.Input;
using global::Avalonia.Input.Platform;
using global::Avalonia.Logging;
namespace Avalonia.WinUI;
/// <summary>
/// <see cref="IPlatformDragSource"/> implementation that initiates native WinUI
/// drags (via <see cref="Microsoft.UI.Xaml.UIElement.StartDragAsync"/>) for
/// content hosted inside an <see cref="AvaloniaSwapChainPanel"/>. WinUI only
/// permits starting a drag with a <see cref="Microsoft.UI.Input.PointerPoint"/>
/// captured during an active pointer interaction; the panel caches the most
/// recent pointer point (regardless of pointer id) and uses it to start the drag.
/// </summary>
internal sealed class WinUIDragSource : IPlatformDragSource
{
public async Task<DragDropEffects> DoDragDropAsync(
PointerPressedEventArgs triggerEvent,
IDataTransfer dataTransfer,
DragDropEffects allowedEffects)
{
// Resolve the panel that hosts the visual that started the drag.
var topLevel = TopLevel.GetTopLevel(triggerEvent.Source as Visual);
var panel = topLevel?.PlatformImpl is { } impl
? AvaloniaSwapChainPanel.GetPanelFor(impl)
: null;
if (panel is null)
{
Logger.TryGet(LogEventLevel.Warning, LogArea.WinUIPlatform)?.Log(this,
"Could not resolve AvaloniaSwapChainPanel for drag source. " +
"TopLevel={TopLevel}, PlatformImpl={PlatformImpl}.",
topLevel?.GetType().Name ?? "null",
topLevel?.PlatformImpl?.GetType().Name ?? "null");
return DragDropEffects.None;
}
return await panel.StartOutgoingDragAsync(dataTransfer, allowedEffects);
}
}

62
src/Windows/Avalonia.WinUI/WinUIKeyInterop.cs

@ -0,0 +1,62 @@
using global::Avalonia.Input;
using global::Avalonia.Input.Raw;
using global::Avalonia.Win32.Input;
using Windows.System;
using Windows.UI.Core;
namespace Avalonia.WinUI;
internal static class WinUIKeyInterop
{
/// <summary>
/// Resolves WinUI key-event metadata to Avalonia's <see cref="Key"/>,
/// <see cref="PhysicalKey"/> and key symbol, using the shared Win32 mapping
/// tables in <see cref="KeyInterop"/>. WinUI's <c>KeyRoutedEventArgs</c>
/// exposes virtual-key + scan-code + extended flag — packing those into the
/// same lParam layout the Win32 backend uses lets us reuse all of its
/// mapping logic (Numpad disambiguation, left/right modifier split, etc.).
/// </summary>
public static (Key Key, PhysicalKey PhysicalKey, string? KeySymbol) Resolve(
VirtualKey virtualKey, CorePhysicalKeyStatus status)
{
var keyData = EncodeKeyData(status);
var vk = (int)virtualKey;
var key = KeyInterop.KeyFromVirtualKey(vk, keyData);
var physical = KeyInterop.PhysicalKeyFromVirtualKey(vk, keyData);
var symbol = KeyInterop.GetKeySymbolFromVirtualKey(vk);
return (key, physical, symbol);
}
public static Key KeyFromVirtualKey(VirtualKey virtualKey)
{
// Fallback used by callers that don't have KeyStatus (rare).
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;
}
private static int EncodeKeyData(CorePhysicalKeyStatus status)
{
// Mirror the WM_KEYDOWN lParam layout that Win32's KeyInterop expects:
// bits 16-23 : scan code (low byte)
// bit 24 : extended-key flag
// Repeat count, context, previous/transition bits are unused by the
// key/physical-key/symbol resolvers, so leave them zero.
var data = (int)((status.ScanCode & 0xFF) << 16);
if (status.IsExtendedKey)
data |= 1 << 24;
return data;
}
}

384
src/Windows/Avalonia.WinUI/WinUITextInputMethod.cs

@ -0,0 +1,384 @@
using System;
using System.Runtime.InteropServices;
using global::Avalonia;
using global::Avalonia.Input;
using global::Avalonia.Input.Raw;
using global::Avalonia.Input.TextInput;
using global::Avalonia.Logging;
using Microsoft.UI.Xaml;
using Windows.UI.Text.Core;
using AvRect = global::Avalonia.Rect;
namespace Avalonia.WinUI;
/// <summary>
/// Bridges Avalonia's <see cref="ITextInputMethodImpl"/> contract to the WinRT <see cref="CoreTextEditContext"/>.
/// </summary>
internal sealed class WinUITextInputMethod : ITextInputMethodImpl
{
private readonly AvaloniaSwapChainPanel _panel;
private readonly Func<Action<RawInputEventArgs>?> _getInput;
private readonly Func<IInputRoot?> _getInputRoot;
private readonly Func<IKeyboardDevice?> _getKeyboardDevice;
private CoreTextEditContext? _editContext;
private TextInputMethodClient? _client;
private bool _hasFocus;
private bool _isComposing;
private string _imeText = string.Empty;
private CoreTextRange _imeSelection;
private int _compositionStart;
private int _compositionLength;
private AvRect _cursorRect;
private bool _suppressClientEcho;
public WinUITextInputMethod(
AvaloniaSwapChainPanel panel,
Func<Action<RawInputEventArgs>?> getInput,
Func<IInputRoot?> getInputRoot,
Func<IKeyboardDevice?> getKeyboardDevice)
{
_panel = panel;
_getInput = getInput;
_getInputRoot = getInputRoot;
_getKeyboardDevice = getKeyboardDevice;
}
private CoreTextEditContext GetOrCreateContext()
{
if (_editContext is not null)
return _editContext;
var manager = CoreTextServicesManager.GetForCurrentView();
var ctx = manager.CreateEditContext();
ctx.InputPaneDisplayPolicy = CoreTextInputPaneDisplayPolicy.Automatic;
ctx.InputScope = CoreTextInputScope.Text;
ctx.TextRequested += OnTextRequested;
ctx.SelectionRequested += OnSelectionRequested;
ctx.LayoutRequested += OnLayoutRequested;
ctx.TextUpdating += OnTextUpdating;
ctx.SelectionUpdating += OnSelectionUpdating;
ctx.FormatUpdating += OnFormatUpdating;
ctx.CompositionStarted += OnCompositionStarted;
ctx.CompositionCompleted += OnCompositionCompleted;
ctx.FocusRemoved += OnFocusRemoved;
_editContext = ctx;
return ctx;
}
public void SetClient(TextInputMethodClient? client)
{
if (_client is not null)
{
_client.SurroundingTextChanged -= OnClientSurroundingTextChanged;
_client.SelectionChanged -= OnClientSelectionChanged;
}
_client = client;
ResetCompositionState();
if (client is null)
{
if (_editContext is not null && _hasFocus)
_editContext.NotifyFocusLeave();
return;
}
client.SurroundingTextChanged += OnClientSurroundingTextChanged;
client.SelectionChanged += OnClientSelectionChanged;
var ctx = GetOrCreateContext();
SyncFromClient(notifyServer: true);
if (_hasFocus)
ctx.NotifyFocusEnter();
}
public void SetCursorRect(Rect rect)
{
_cursorRect = rect;
_editContext?.NotifyLayoutChanged();
}
public void SetOptions(TextInputOptions options)
{
if (_editContext is null)
return;
_editContext.InputScope = options.ContentType switch
{
TextInputContentType.Email => CoreTextInputScope.EmailAddress,
TextInputContentType.Number => CoreTextInputScope.Number,
TextInputContentType.Password => CoreTextInputScope.Password,
TextInputContentType.Digits => CoreTextInputScope.Digits,
TextInputContentType.Url => CoreTextInputScope.Url,
TextInputContentType.Search => CoreTextInputScope.Search,
_ => CoreTextInputScope.Text,
};
}
public void Reset()
{
if (_isComposing)
_client?.SetPreeditText(null);
ResetCompositionState();
SyncFromClient(notifyServer: true);
}
/// <summary>Called by the panel when it gains/loses focus.</summary>
public void OnPanelFocusChanged(bool hasFocus)
{
_hasFocus = hasFocus;
if (_editContext is null)
return;
if (hasFocus && _client is not null)
_editContext.NotifyFocusEnter();
else
_editContext.NotifyFocusLeave();
}
private void ResetCompositionState()
{
_isComposing = false;
_compositionStart = 0;
_compositionLength = 0;
}
private void SyncFromClient(bool notifyServer)
{
if (_client is null)
{
_imeText = string.Empty;
_imeSelection = default;
return;
}
_imeText = _client.SurroundingText ?? string.Empty;
var sel = _client.Selection;
_imeSelection = new CoreTextRange
{
StartCaretPosition = Math.Clamp(sel.Start, 0, _imeText.Length),
EndCaretPosition = Math.Clamp(sel.End, 0, _imeText.Length),
};
if (notifyServer && _editContext is not null)
{
_editContext.NotifyTextChanged(
new CoreTextRange { StartCaretPosition = 0, EndCaretPosition = int.MaxValue },
_imeText.Length,
_imeSelection);
_editContext.NotifyLayoutChanged();
}
}
private void OnClientSurroundingTextChanged(object? sender, EventArgs e)
{
if (_suppressClientEcho || _isComposing)
return;
SyncFromClient(notifyServer: true);
}
private void OnClientSelectionChanged(object? sender, EventArgs e)
{
if (_suppressClientEcho || _isComposing || _client is null || _editContext is null)
return;
var sel = _client.Selection;
_imeSelection = new CoreTextRange
{
StartCaretPosition = Math.Clamp(sel.Start, 0, _imeText.Length),
EndCaretPosition = Math.Clamp(sel.End, 0, _imeText.Length),
};
_editContext.NotifySelectionChanged(_imeSelection);
}
private void OnTextRequested(CoreTextEditContext sender, CoreTextTextRequestedEventArgs args)
{
var range = args.Request.Range;
var start = Math.Clamp(range.StartCaretPosition, 0, _imeText.Length);
var end = Math.Clamp(range.EndCaretPosition, start, _imeText.Length);
args.Request.Text = _imeText.Substring(start, end - start);
}
private void OnSelectionRequested(CoreTextEditContext sender, CoreTextSelectionRequestedEventArgs args)
{
args.Request.Selection = _imeSelection;
}
private void OnLayoutRequested(CoreTextEditContext sender, CoreTextLayoutRequestedEventArgs args)
{
try
{
var screenRect = ComputeScreenCursorRect();
args.Request.LayoutBounds.TextBounds = screenRect;
args.Request.LayoutBounds.ControlBounds = screenRect;
}
catch (Exception ex)
{
// Panel not in tree yet, or transform unavailable — leave bounds default.
Logger.TryGet(LogEventLevel.Verbose, LogArea.WinUIPlatform)?.Log(this, "LayoutRequested couldn't compute screen rect: {Message}", ex.Message);
}
}
private Windows.Foundation.Rect ComputeScreenCursorRect()
{
var transform = _panel.TransformToVisual(null);
var topLeft = transform.TransformPoint(
new Windows.Foundation.Point(_cursorRect.X, _cursorRect.Y));
var width = Math.Max(_cursorRect.Width, 1);
var height = Math.Max(_cursorRect.Height, 16);
var xamlRoot = _panel.XamlRoot;
var scale = xamlRoot?.RasterizationScale ?? 1.0;
var clientX = topLeft.X * scale;
var clientY = topLeft.Y * scale;
var w = width * scale;
var h = height * scale;
if (xamlRoot?.ContentIslandEnvironment is { } island)
{
var hwnd = Microsoft.UI.Win32Interop.GetWindowFromWindowId(island.AppWindowId);
if (hwnd != IntPtr.Zero)
{
var pt = new POINT { x = 0, y = 0 };
if (ClientToScreen(hwnd, ref pt))
{
clientX += pt.x;
clientY += pt.y;
}
}
}
return new Windows.Foundation.Rect(clientX, clientY, w, h);
}
private void OnTextUpdating(CoreTextEditContext sender, CoreTextTextUpdatingEventArgs args)
{
var newText = args.Text ?? string.Empty;
var range = args.Range;
var start = Math.Clamp(range.StartCaretPosition, 0, _imeText.Length);
var end = Math.Clamp(range.EndCaretPosition, start, _imeText.Length);
_imeText = _imeText.Substring(0, start) + newText + _imeText.Substring(end);
_imeSelection = args.NewSelection;
if (_isComposing)
{
if (_compositionLength == 0)
_compositionStart = start;
_compositionLength = Math.Max(0, _imeSelection.EndCaretPosition - _compositionStart);
_compositionLength = Math.Min(_compositionLength, _imeText.Length - _compositionStart);
var preedit = _imeText.Substring(_compositionStart, _compositionLength);
_client?.SetPreeditText(preedit, _imeSelection.EndCaretPosition - _compositionStart);
}
else if (!string.IsNullOrEmpty(newText))
{
DispatchRawText(newText, notifyServer: true);
}
args.Result = CoreTextTextUpdatingResult.Succeeded;
}
private void OnSelectionUpdating(CoreTextEditContext sender, CoreTextSelectionUpdatingEventArgs args)
{
_imeSelection = args.Selection;
if (!_isComposing && _client is not null)
{
_suppressClientEcho = true;
try
{
_client.Selection = new TextSelection(
_imeSelection.StartCaretPosition, _imeSelection.EndCaretPosition);
}
finally { _suppressClientEcho = false; }
}
args.Result = CoreTextSelectionUpdatingResult.Succeeded;
}
private void OnFormatUpdating(CoreTextEditContext sender, CoreTextFormatUpdatingEventArgs args)
{
args.Result = CoreTextFormatUpdatingResult.Succeeded;
}
private void OnCompositionStarted(CoreTextEditContext sender, CoreTextCompositionStartedEventArgs args)
{
if (_imeSelection.StartCaretPosition != _imeSelection.EndCaretPosition)
{
var input = _getInput();
var inputRoot = _getInputRoot();
var keyboard = _getKeyboardDevice();
if (input is not null && inputRoot is not null && keyboard is not null)
{
_suppressClientEcho = true;
try
{
var timestamp = (ulong)Environment.TickCount64;
input(new RawKeyEventArgs(keyboard, timestamp, inputRoot,
RawKeyEventType.KeyDown, Key.Delete, RawInputModifiers.None, PhysicalKey.Delete, null));
input(new RawKeyEventArgs(keyboard, timestamp, inputRoot,
RawKeyEventType.KeyUp, Key.Delete, RawInputModifiers.None, PhysicalKey.Delete, null));
}
finally
{
_suppressClientEcho = false;
}
}
}
_isComposing = true;
_compositionStart = _imeSelection.StartCaretPosition;
_compositionLength = 0;
}
private void OnCompositionCompleted(CoreTextEditContext sender, CoreTextCompositionCompletedEventArgs args)
{
_isComposing = false;
var len = Math.Max(0, Math.Min(_compositionLength, _imeText.Length - _compositionStart));
var committed = len > 0 ? _imeText.Substring(_compositionStart, len) : string.Empty;
_client?.SetPreeditText(null);
_compositionLength = 0;
if (!string.IsNullOrEmpty(committed))
DispatchRawText(committed, notifyServer: false);
}
private void OnFocusRemoved(CoreTextEditContext sender, object args)
{
_hasFocus = false;
}
private void DispatchRawText(string text, bool notifyServer)
{
var input = _getInput();
var inputRoot = _getInputRoot();
var keyboard = _getKeyboardDevice();
if (input is null || inputRoot is null || keyboard is null)
return;
_suppressClientEcho = true;
try
{
input(new RawTextInputEventArgs(keyboard, (ulong)Environment.TickCount64, inputRoot, text));
}
finally
{
_suppressClientEcho = false;
}
SyncFromClient(notifyServer);
}
[StructLayout(LayoutKind.Sequential)]
private struct POINT
{
public int x;
public int y;
}
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool ClientToScreen(IntPtr hWnd, ref POINT lpPoint);
}
Loading…
Cancel
Save