* 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 samplepull/21537/head
@ -0,0 +1,16 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<Application |
||||
|
x:Class="ControlGallery.WinUI.App" |
||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
xmlns:local="using:ControlGallery.WinUI"> |
||||
|
<Application.Resources> |
||||
|
<ResourceDictionary> |
||||
|
<ResourceDictionary.MergedDictionaries> |
||||
|
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" /> |
||||
|
<!-- Other merged dictionaries here --> |
||||
|
</ResourceDictionary.MergedDictionaries> |
||||
|
<!-- Other app resources here --> |
||||
|
</ResourceDictionary> |
||||
|
</Application.Resources> |
||||
|
</Application> |
||||
@ -0,0 +1,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; } |
||||
|
} |
||||
|
} |
||||
|
After Width: | Height: | Size: 432 B |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 637 B |
|
After Width: | Height: | Size: 283 B |
|
After Width: | Height: | Size: 456 B |
|
After Width: | Height: | Size: 2.0 KiB |
@ -0,0 +1,67 @@ |
|||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
<PropertyGroup> |
||||
|
<OutputType>WinExe</OutputType> |
||||
|
<TargetFramework>$(AvsCurrentWinUITargetFramework)</TargetFramework> |
||||
|
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion> |
||||
|
<RootNamespace>ControlGallery.WinUI</RootNamespace> |
||||
|
<ApplicationManifest>app.manifest</ApplicationManifest> |
||||
|
<Platforms>x86;x64;ARM64</Platforms> |
||||
|
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers> |
||||
|
<PublishProfile>win-$(Platform).pubxml</PublishProfile> |
||||
|
<UseWinUI>true</UseWinUI> |
||||
|
<WinUISDKReferences>false</WinUISDKReferences> |
||||
|
<EnableMsixTooling>true</EnableMsixTooling> |
||||
|
<Nullable>enable</Nullable> |
||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<Content Include="Assets\SplashScreen.scale-200.png" /> |
||||
|
<Content Include="Assets\LockScreenLogo.scale-200.png" /> |
||||
|
<Content Include="Assets\Square150x150Logo.scale-200.png" /> |
||||
|
<Content Include="Assets\Square44x44Logo.scale-200.png" /> |
||||
|
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" /> |
||||
|
<Content Include="Assets\StoreLogo.png" /> |
||||
|
<Content Include="Assets\Wide310x150Logo.scale-200.png" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<ItemGroup> |
||||
|
<Manifest Include="$(ApplicationManifest)" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<!-- |
||||
|
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging |
||||
|
Tools extension to be activated for this project even if the Windows App SDK Nuget |
||||
|
package has not yet been restored. |
||||
|
--> |
||||
|
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'"> |
||||
|
<ProjectCapability Include="Msix" /> |
||||
|
</ItemGroup> |
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" /> |
||||
|
<PackageReference Include="Microsoft.WindowsAppSDK" /> |
||||
|
</ItemGroup> |
||||
|
<ItemGroup> |
||||
|
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" /> |
||||
|
<ProjectReference Include="..\..\src\Windows\Avalonia.Win32\Avalonia.Win32.csproj" /> |
||||
|
<ProjectReference Include="..\..\src\Windows\Avalonia.WinUI\Avalonia.WinUI.csproj" /> |
||||
|
<ProjectReference Include="..\..\src\HarfBuzz\Avalonia.HarfBuzz\Avalonia.HarfBuzz.csproj" /> |
||||
|
</ItemGroup> |
||||
|
|
||||
|
<!-- |
||||
|
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution |
||||
|
Explorer "Package and Publish" context menu entry to be enabled for this project even if |
||||
|
the Windows App SDK Nuget package has not yet been restored. |
||||
|
--> |
||||
|
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'"> |
||||
|
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu> |
||||
|
</PropertyGroup> |
||||
|
|
||||
|
<!-- Publish Properties --> |
||||
|
<PropertyGroup> |
||||
|
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun> |
||||
|
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun> |
||||
|
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed> |
||||
|
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed> |
||||
|
</PropertyGroup> |
||||
|
</Project> |
||||
@ -0,0 +1,15 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<Window |
||||
|
x:Class="ControlGallery.WinUI.MainWindow" |
||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
xmlns:avalonia="using:Avalonia.WinUI" |
||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
|
mc:Ignorable="d" |
||||
|
Title="ControlGallery.WinUI"> |
||||
|
|
||||
|
<Grid> |
||||
|
<avalonia:AvaloniaSwapChainPanel x:Name="AvaloniaPanel" /> |
||||
|
</Grid> |
||||
|
</Window> |
||||
@ -0,0 +1,11 @@ |
|||||
|
namespace ControlGallery.WinUI |
||||
|
{ |
||||
|
public sealed partial class MainWindow : Microsoft.UI.Xaml.Window |
||||
|
{ |
||||
|
public MainWindow() |
||||
|
{ |
||||
|
InitializeComponent(); |
||||
|
AvaloniaPanel.Content = App.Lifetime.MainView; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -0,0 +1,10 @@ |
|||||
|
{ |
||||
|
"profiles": { |
||||
|
"ControlGallery.WinUI (Package)": { |
||||
|
"commandName": "MsixPackage" |
||||
|
}, |
||||
|
"ControlGallery.WinUI (Unpackaged)": { |
||||
|
"commandName": "Project" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> |
||||
|
<assemblyIdentity version="1.0.0.0" name="ControlGallery.WinUI.app"/> |
||||
|
|
||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> |
||||
|
<application> |
||||
|
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10. |
||||
|
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation. |
||||
|
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest --> |
||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" /> |
||||
|
</application> |
||||
|
</compatibility> |
||||
|
|
||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3"> |
||||
|
<windowsSettings> |
||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness> |
||||
|
</windowsSettings> |
||||
|
</application> |
||||
|
</assembly> |
||||
@ -0,0 +1,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> |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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 & 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: "Hello from Avalonia"" |
||||
|
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> |
||||
@ -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"; |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -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 |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -0,0 +1,10 @@ |
|||||
|
{ |
||||
|
"profiles": { |
||||
|
"WinUIEmbedSample (Package)": { |
||||
|
"commandName": "MsixPackage" |
||||
|
}, |
||||
|
"WinUIEmbedSample (Unpackaged)": { |
||||
|
"commandName": "Project" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -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> |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
@ -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, |
||||
|
}; |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
@ -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!, |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -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, |
||||
|
}; |
||||
|
} |
||||
@ -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> |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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, |
||||
|
}; |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||