* 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); |
|||
} |
|||