Browse Source

Merge branch 'master' into tcc-fixes

pull/12173/head
Daniel Mayost 3 years ago
committed by GitHub
parent
commit
d823b24109
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      api/Avalonia.nupkg.xml
  2. 1
      build/ExternalConsumers.props
  3. 8
      build/SourceLink.props
  4. 8
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  5. 18
      native/Avalonia.Native/src/OSX/app.mm
  6. 1
      native/Avalonia.Native/src/OSX/common.h
  7. 8
      native/Avalonia.Native/src/OSX/main.mm
  8. 5
      nukebuild/ApiDiffValidation.cs
  9. 2
      nukebuild/Build.cs
  10. 2
      nukebuild/Numerge
  11. 12
      nukebuild/_build.csproj
  12. 26
      packages/Avalonia/AvaloniaBuildTasks.targets
  13. 31
      samples/ControlCatalog/Models/GDPdLengthConverter.cs
  14. 21
      samples/ControlCatalog/Pages/DataGridPage.xaml
  15. 7
      samples/Sandbox/MainWindow.axaml
  16. 69
      src/Android/Avalonia.Android/AndroidInputMethod.cs
  17. 108
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  18. 1
      src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs
  19. 6
      src/Avalonia.Base/Avalonia.Base.csproj
  20. 20
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  21. 6
      src/Avalonia.Base/Media/GlyphRun.cs
  22. 325
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  23. 122
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  24. 13
      src/Avalonia.Base/Rendering/Composition/CompositionExternalMemory.cs
  25. 8
      src/Avalonia.Base/Rendering/Composition/CompositionInterop.cs
  26. 6
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawingSurface.cs
  27. 7
      src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs
  28. 8
      src/Avalonia.Base/Threading/Dispatcher.Invoke.cs
  29. 2
      src/Avalonia.Base/Threading/Dispatcher.MainLoop.cs
  30. 20
      src/Avalonia.Base/Threading/Dispatcher.Queue.cs
  31. 2
      src/Avalonia.Base/Threading/Dispatcher.Timers.cs
  32. 3
      src/Avalonia.Base/Threading/Dispatcher.cs
  33. 8
      src/Avalonia.Base/Threading/DispatcherFrame.cs
  34. 2
      src/Avalonia.Base/Threading/DispatcherOperation.cs
  35. 12
      src/Avalonia.Base/Threading/DispatcherTimer.cs
  36. 165
      src/Avalonia.Base/Visual.Composition.cs
  37. 28
      src/Avalonia.Base/Visual.cs
  38. 16
      src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
  39. 4
      src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs
  40. 159
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  41. 4
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  42. 17
      src/Avalonia.Controls.DataGrid/DataGridColumns.cs
  43. 1
      src/Avalonia.Controls.DataGrid/DataGridLength.cs
  44. 5
      src/Avalonia.Controls.DataGrid/DataGridRows.cs
  45. 4
      src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs
  46. 10
      src/Avalonia.Controls/ComboBox.cs
  47. 4
      src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs
  48. 4
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  49. 19
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  50. 4
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  51. 10
      src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs
  52. 22
      src/Avalonia.Controls/Utils/StringUtils.cs
  53. 2
      src/Avalonia.Controls/Utils/VirtualizingSnapPointsList.cs
  54. 7
      src/Avalonia.Controls/Window.cs
  55. 10
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  56. 2
      src/Avalonia.Native/NativePlatformSettings.cs
  57. 8
      src/Avalonia.Remote.Protocol/MetsysBson.cs
  58. 50
      src/Avalonia.X11/X11Clipboard.cs
  59. 20
      src/Avalonia.X11/X11Info.cs
  60. 10
      src/Avalonia.X11/X11Platform.cs
  61. 5
      src/Avalonia.X11/X11Structs.cs
  62. 15
      src/Avalonia.X11/X11Window.Ime.cs
  63. 2
      src/Avalonia.X11/XLib.cs
  64. 12
      src/Browser/Avalonia.Browser/webapp/package-lock.json
  65. 3
      src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs
  66. 3
      src/Windows/Avalonia.Win32/WinRT/Composition/D2DEffects.cs
  67. 104
      src/Windows/Avalonia.Win32/WinRT/Composition/WinUIEffectBase.cs
  68. 30
      src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs
  69. 9
      src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionShared.cs
  70. 72
      src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionUtils.cs
  71. 3
      src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs
  72. 23
      src/Windows/Avalonia.Win32/WinRT/WinRTPropertyValue.cs
  73. 2
      src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
  74. 23
      src/Windows/Avalonia.Win32/WindowImpl.cs
  75. 2
      src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj
  76. 2
      src/tools/Avalonia.Generators/Avalonia.Generators.csproj
  77. 6
      src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs
  78. 2
      tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs
  79. 31
      tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
  80. 16
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_SelectedValue.cs
  81. 35
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  82. 3
      tests/Avalonia.Generators.Tests/Views/NamedControl.xml
  83. 3
      tests/Avalonia.Generators.Tests/Views/xNamedControl.xml
  84. 136
      tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

10
api/Avalonia.nupkg.xml

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Rendering.Composition.ICompositionGpuImportedObject.ImportCompleted</Target>
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
</Suppression>
</Suppressions>

1
build/ExternalConsumers.props

@ -1,6 +1,7 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<InternalsVisibleTo Include="AvaloniaUI.Xpf.WinApiShim, PublicKey=0024000004800000940000000602000000240000525341310004000001000100a7f4b8b7db0bfb8d74992dc94ecafae031019197ff4263d87ac0a5835fab101c973ccab6fa6e7d90e8f987374f7c6de18dd0b5cd7d6c41e574a8bc66b64836b7c7e707e1aa393d27e33a08f372c1c9965be81658937c85698f4a1c0f73be68a61ffce06d49d1366bf18464c20a29859ccf105fc2d5e35c7ae68919eab668bf8e" />
<InternalsVisibleTo Include="AvaloniaUI.Xpf.WpfAbstractions, PublicKey=0024000004800000940000000602000000240000525341310004000001000100a7f4b8b7db0bfb8d74992dc94ecafae031019197ff4263d87ac0a5835fab101c973ccab6fa6e7d90e8f987374f7c6de18dd0b5cd7d6c41e574a8bc66b64836b7c7e707e1aa393d27e33a08f372c1c9965be81658937c85698f4a1c0f73be68a61ffce06d49d1366bf18464c20a29859ccf105fc2d5e35c7ae68919eab668bf8e" />
<InternalsVisibleTo Include="System.Windows.Forms, PublicKey=00000000000000000400000000000000" />
<InternalsVisibleTo Include="System.Windows.Forms.Primitives, PublicKey=00000000000000000400000000000000" />
<InternalsVisibleTo Include="PresentationFramework-SystemData, PublicKey=00000000000000000400000000000000" />

8
build/SourceLink.props

@ -1,9 +1,9 @@
<Project>
<PropertyGroup>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>false</IncludeSymbols>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
</PropertyGroup>
<PropertyGroup Condition="'$(TF_BUILD)' == 'true'">
@ -14,10 +14,6 @@
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<PropertyGroup>
<DebugType Condition="$(ContinuousIntegrationBuild) == 'true'">embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All"/>
</ItemGroup>

8
native/Avalonia.Native/src/OSX/WindowBaseImpl.mm

@ -151,6 +151,14 @@ HRESULT WindowBaseImpl::Hide() {
@autoreleasepool {
if (Window != nullptr) {
auto frame = [Window frame];
AvnPoint point;
point.X = frame.origin.x;
point.Y = frame.origin.y + frame.size.height;
lastPositionSet = ConvertPointY(point);
hasPosition = true;
[Window orderOut:Window];
}

18
native/Avalonia.Native/src/OSX/app.mm

@ -2,6 +2,7 @@
#include "AvnString.h"
@interface AvnAppDelegate : NSObject<NSApplicationDelegate>
-(AvnAppDelegate* _Nonnull) initWithEvents: (IAvnApplicationEvents* _Nonnull) events;
-(void) releaseEvents;
@end
NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular;
@ -15,6 +16,11 @@ ComPtr<IAvnApplicationEvents> _events;
return self;
}
- (void)releaseEvents
{
_events = nil;
}
- (void)applicationWillFinishLaunching:(NSNotification *)notification
{
if([[NSApplication sharedApplication] activationPolicy] != AvnDesiredActivationPolicy)
@ -105,6 +111,18 @@ extern void InitializeAvnApp(IAvnApplicationEvents* events, bool disableAppDeleg
}
}
extern void ReleaseAvnAppEvents()
{
NSApplication* app = [AvnApplication sharedApplication];
id delegate = [app delegate];
if ([delegate isMemberOfClass:[AvnAppDelegate class]])
{
AvnAppDelegate* avnDelegate = delegate;
[avnDelegate releaseEvents];
[app setDelegate:nil];
}
}
HRESULT AvnApplicationCommands::HideApp()
{
START_COM_CALL;

1
native/Avalonia.Native/src/OSX/common.h

@ -38,6 +38,7 @@ extern IAvnMenu* GetAppMenu ();
extern NSMenuItem* GetAppMenuItem ();
extern void InitializeAvnApp(IAvnApplicationEvents* events, bool disableAppDelegate);
extern void ReleaseAvnAppEvents();
extern NSApplicationActivationPolicy AvnDesiredActivationPolicy;
extern NSPoint ToNSPoint (AvnPoint p);
extern NSRect ToNSRect (AvnRect r);

8
native/Avalonia.Native/src/OSX/main.mm

@ -197,6 +197,14 @@ class AvaloniaNative : public ComSingleObject<IAvaloniaNativeFactory, &IID_IAval
public:
FORWARD_IUNKNOWN()
virtual ~AvaloniaNative() override
{
ReleaseAvnAppEvents();
_deallocator = nullptr;
_dispatcher = nullptr;
}
virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator,
IAvnApplicationEvents* events,
IAvnDispatcher* dispatcher) override

5
nukebuild/ApiDiffValidation.cs

@ -38,7 +38,8 @@ public static class ApiDiffValidation
var left = new List<string>();
var right = new List<string>();
var suppressionFile = Path.Combine(suppressionFilesFolder, GetPackageId(packagePath) + ".nupkg.xml");
var packageId = GetPackageId(packagePath);
var suppressionFile = Path.Combine(suppressionFilesFolder, packageId + ".nupkg.xml");
// Don't use Path.Combine with these left and right tool parameters.
// Microsoft.DotNet.ApiCompat.Tool is stupid and treats '/' and '\' as different assemblies in suppression files.
@ -57,7 +58,7 @@ public static class ApiDiffValidation
e.target == baselineDll.target && e.entry.Name == baselineDll.entry.Name);
if (targetDll.entry is null)
{
throw new InvalidOperationException($"Some assemblies are missing in the new package: {baselineDll.entry.Name} for {baselineDll.target}");
throw new InvalidOperationException($"Some assemblies are missing in the new package {packageId}: {baselineDll.entry.Name} for {baselineDll.target}");
}
var targetDllPath = $"target/{targetDll.target}/{targetDll.entry.Name}";

2
nukebuild/Build.cs

@ -288,7 +288,7 @@ partial class Build : NukeBuild
.Executes(async () =>
{
await Task.WhenAll(
Directory.GetFiles(Parameters.NugetRoot).Select(nugetPackage => ApiDiffValidation.ValidatePackage(
Directory.GetFiles(Parameters.NugetRoot, "*.nupkg").Select(nugetPackage => ApiDiffValidation.ValidatePackage(
ApiCompatTool, nugetPackage, Parameters.ApiValidationBaseline,
Parameters.ApiValidationSuppressionFiles, Parameters.UpdateApiValidationSuppression)));
});

2
nukebuild/Numerge

@ -1 +1 @@
Subproject commit aef10ae67dc55c95f49b52a505a0be33bfa297a5
Subproject commit 9738c6121fdd143c78d3e25686a7e4e13c00f586

12
nukebuild/_build.csproj

@ -8,8 +8,8 @@
<NukeTelemetryVersion>1</NukeTelemetryVersion>
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>
<Import Project="..\build\JetBrains.dotMemoryUnit.props" />
<Import Project="..\build\JetBrains.dotMemoryUnit.props" />
<ItemGroup>
<PackageReference Include="Nuke.Common" Version="6.2.1" />
<PackageReference Include="vswhere" Version="2.6.7" Condition=" '$(OS)' == 'Windows_NT' " />
@ -32,9 +32,9 @@
<!-- Common build related files -->
<Compile Remove="Numerge/**/*.*" />
<Compile Include="Numerge/Numerge/**/*.cs" />
<EmbeddedResource Include="$(NuGetPackageRoot)sourcelink/1.1.0/tools/pdbstr.exe"></EmbeddedResource>
<EmbeddedResource Include="../build/avalonia.snk"></EmbeddedResource>
<Compile Include="Numerge/Numerge/**/*.cs" Exclude="Numerge/Numerge/obj/**/*.cs" />
<EmbeddedResource Include="$(NuGetPackageRoot)sourcelink/1.1.0/tools/pdbstr.exe" />
<EmbeddedResource Include="../build/avalonia.snk" />
<Compile Remove="il-repack\ILRepack\Application.cs" />
</ItemGroup>
@ -43,7 +43,5 @@
<Link>dirs.proj</Link>
</Content>
</ItemGroup>
</Project>

26
packages/Avalonia/AvaloniaBuildTasks.targets

@ -144,4 +144,30 @@
<UpToDateCheckInput Include="@(AvaloniaResource)" />
<UpToDateCheckInput Include="@(AvaloniaXaml)" />
</ItemGroup>
<PropertyGroup>
<AvaloniaFilePreviewDependsOn Condition="'$(SkipBuild)'!='True'">Build</AvaloniaFilePreviewDependsOn>
</PropertyGroup>
<Target Name="AvaloniaFilePreview" DependsOnTargets="$(AvaloniaFilePreviewDependsOn)">
<PropertyGroup>
<APreviewerUrl>http://127.0.0.1:6001</APreviewerUrl>
<APreviewExecutable>$(OutputPath)/$(AssemblyName).dll</APreviewExecutable>
<APreviewFile Condition="$(APreviewFile) == ''">MainWindow.axaml</APreviewFile>
<APreviewAssembly Condition="$(APreviewAssembly) == ''">$(APreviewExecutable)</APreviewAssembly>
<APreviewerDepsJsonPath>$([System.IO.Path]::ChangeExtension('$(APreviewExecutable)', '.deps.json'))</APreviewerDepsJsonPath>
<APreviewerRuntimeConfigPath>$([System.IO.Path]::ChangeExtension('$(APreviewExecutable)', '.runtimeconfig.json'))</APreviewerRuntimeConfigPath>
<APreviewTransportUrl>$([System.IO.Path]::GetFullPath('$(APreviewFile)'))</APreviewTransportUrl>
</PropertyGroup>
<Message Importance="high" Text="Launching previewer for"/>
<Message Importance="high" Text="File (APreviewFile): $(APreviewTransportUrl)"/>
<Message Importance="high" Text="Containing assembly (APreviewAssembly): $(APreviewDefiningAssembly)"/>
<Message Importance="high" Text="Executable: $(APreviewExecutable)"/>
<Message Importance="high" Text="Url (APreviewerUrl): $(APreviewerUrl)"/>
<Exec Command="dotnet exec --runtimeconfig &quot;$(APreviewerRuntimeConfigPath)&quot; --depsfile &quot;$(APreviewerDepsJsonPath)&quot; &quot;$(AvaloniaPreviewerNetCoreToolPath)&quot; --method html --html-url $(APreviewerUrl) --transport $(APreviewTransportUrl) &quot;$(APreviewExecutable)&quot;"/>
</Target>
</Project>

31
samples/ControlCatalog/Models/GDPdLengthConverter.cs

@ -0,0 +1,31 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace ControlCatalog.Models;
internal class GDPdLengthConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is double d)
{
return new Avalonia.Controls.DataGridLength(d,Avalonia.Controls.DataGridLengthUnitType.Pixel,d,d);
}
else if (value is decimal d2)
{
var dv =System.Convert.ToDouble(d2);
return new Avalonia.Controls.DataGridLength(dv, Avalonia.Controls.DataGridLengthUnitType.Pixel, dv, dv);
}
return value;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is Avalonia.Controls.DataGridLength width)
{
return System.Convert.ToDecimal(width.DisplayValue);
}
return value;
}
}

21
samples/ControlCatalog/Pages/DataGridPage.xaml

@ -1,11 +1,14 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:local="using:ControlCatalog.Models"
xmlns:lc="using:ControlCatalog.Converter"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:pages="clr-namespace:ControlCatalog.Pages"
x:Class="ControlCatalog.Pages.DataGridPage"
x:DataType="pages:DataGridPage">
<UserControl.Resources>
<local:GDPValueConverter x:Key="GDPConverter" />
<local:GDPdLengthConverter x:Key="GDPWidthConverter"/>
<DataTemplate x:Key="Demo.DataTemplates.CountryHeader" x:DataType="local:Country">
<StackPanel Orientation="Horizontal" Spacing="5">
<PathIcon Height="12" Data="M 255 116 A 1 1 0 0 0 254 117 L 254 130 A 1 1 0 0 0 255 131 A 1 1 0 0 0 256 130 L 256 123.87109 C 256.1125 123.90694 256.2187 123.94195 256.33984 123.97852 C 257.18636 124.23404 258.19155 124.5 259 124.5 C 259.80845 124.5 260.52133 124.2168 261.17773 123.9668 C 261.83414 123.7168 262.43408 123.5 263 123.5 C 263.56592 123.5 264.5612 123.73404 265.37109 123.97852 C 266.18098 124.22299 266.82227 124.4668 266.82227 124.4668 A 0.50005 0.50005 0 0 0 267.5 124 L 267.5 118 A 0.50005 0.50005 0 0 0 267.17773 117.5332 C 267.17773 117.5332 266.50667 117.27701 265.66016 117.02148 C 264.81364 116.76596 263.80845 116.5 263 116.5 C 262.19155 116.5 261.47867 116.7832 260.82227 117.0332 C 260.16586 117.2832 259.56592 117.5 259 117.5 C 258.43408 117.5 257.4388 117.26596 256.62891 117.02148 C 256.39123 116.94974 256.17716 116.87994 255.98047 116.81445 A 1 1 0 0 0 255 116 z M 263 117.5 C 263.56592 117.5 264.5612 117.73404 265.37109 117.97852 C 266.00097 118.16865 266.29646 118.28239 266.5 118.35742 L 266.5 120.29297 C 266.25708 120.21012 265.97978 120.11797 265.66016 120.02148 C 264.81364 119.76596 263.80845 119.5 263 119.5 C 262.19155 119.5 261.47867 119.7832 260.82227 120.0332 C 260.16586 120.2832 259.56592 120.5 259 120.5 C 258.43408 120.5 257.4388 120.26596 256.62891 120.02148 C 256.39971 119.9523 256.19148 119.88388 256 119.82031 L 256 117.87109 C 256.1125 117.90694 256.2187 117.94195 256.33984 117.97852 C 257.18636 118.23404 258.19155 118.5 259 118.5 C 259.80845 118.5 260.52133 118.2168 261.17773 117.9668 C 261.83414 117.7168 262.43408 117.5 263 117.5 z M 263 120.5 C 263.56592 120.5 264.5612 120.73404 265.37109 120.97852 C 265.8714 121.12954 266.2398 121.25641 266.5 121.34961 L 266.5 123.30469 C 266.22286 123.20649 266.12863 123.1629 265.66016 123.02148 C 264.81364 122.76596 263.80845 122.5 263 122.5 C 262.19155 122.5 261.47867 122.7832 260.82227 123.0332 C 260.16586 123.2832 259.56592 123.5 259 123.5 C 258.43408 123.5 257.4388 123.26596 256.62891 123.02148 C 256.39971 122.9523 256.19148 122.88388 256 122.82031 L 256 120.87109 C 256.1125 120.90694 256.2187 120.94195 256.33984 120.97852 C 257.18636 121.23404 258.19155 121.5 259 121.5 C 259.80845 121.5 260.52133 121.2168 261.17773 120.9668 C 261.83414 120.7168 262.43408 120.5 263 120.5 z" />
@ -28,8 +31,18 @@
<TabControl Grid.Row="2">
<TabItem Header="DataGrid">
<DockPanel>
<CheckBox x:Name="ShowGDP" IsChecked="True" Content="Toggle GDP Column Visibility"
DockPanel.Dock="Top"/>
<StackPanel Orientation="Horizontal"
DockPanel.Dock="Top"
Spacing="5">
<CheckBox x:Name="ShowGDP" IsChecked="True" Content="Toggle GDP Column Visibility"/>
<TextBlock Text="GDP Width:" VerticalAlignment="Center"/>
<NumericUpDown x:Name="GDPWidth"
Minimum="200"
Maximum="350"
Width="200"
Increment="10"
Value="200"/>
</StackPanel>
<DataGrid Name="dataGrid1" Margin="12" CanUserResizeColumns="True" CanUserReorderColumns="True" CanUserSortColumns="True" HeadersVisibility="All"
RowBackground="#1000">
<DataGrid.Columns>
@ -38,9 +51,11 @@
<DataGridTextColumn Header="Region" Binding="{Binding Region}" Width="4*" x:DataType="local:Country" />
<DataGridTextColumn Header="Population" Binding="{Binding Population}" Width="3*" x:DataType="local:Country" />
<DataGridTextColumn Header="Area" Binding="{Binding Area}" Width="3*" x:DataType="local:Country" />
<DataGridTextColumn Header="GDP" Binding="{Binding GDP}" Width="3*"
<DataGridTextColumn Header="GDP" Binding="{Binding GDP}"
Width="{Binding #GDPWidth.Value, Mode=TwoWay, Converter={StaticResource GDPWidthConverter}}"
CellTheme="{StaticResource GdpCell}"
MinWidth="200"
MaxWidth="350"
IsVisible="{Binding #ShowGDP.IsChecked}"
x:DataType="local:Country" />
</DataGrid.Columns>

7
samples/Sandbox/MainWindow.axaml

@ -1,11 +1,4 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
x:Class="Sandbox.MainWindow">
<ScrollViewer>
<StackPanel>
<Button Margin="0 100000000000000000 0 0">0</Button>
<Button>1</Button>
</StackPanel>
</ScrollViewer>
</Window>

69
src/Android/Avalonia.Android/AndroidInputMethod.cs

@ -18,6 +18,8 @@ namespace Avalonia.Android
public bool IsActive { get; }
public InputMethodManager IMM { get; }
void OnBatchEditedEnded();
}
enum CustomImeFlags
@ -103,6 +105,13 @@ namespace Avalonia.Android
}
private void _client_SelectionChanged(object sender, EventArgs e)
{
if (_inputConnection.IsInBatchEdit)
return;
OnSelectionChanged();
}
private void OnSelectionChanged()
{
var selection = Client.Selection;
@ -113,17 +122,67 @@ namespace Avalonia.Android
private void _client_SurroundingTextChanged(object sender, EventArgs e)
{
if (_inputConnection.IsInBatchEdit)
return;
OnSurroundingTextChanged();
}
public void OnBatchEditedEnded()
{
if (_inputConnection.IsInBatchEdit)
return;
OnSurroundingTextChanged();
OnSelectionChanged();
}
private void OnSurroundingTextChanged()
{
if(_client is null)
{
return;
}
var surroundingText = _client.SurroundingText ?? "";
var editableText = _inputConnection.EditableWrapper.ToString();
_inputConnection.EditableWrapper.IgnoreChange = true;
if (editableText != surroundingText)
{
_inputConnection.EditableWrapper.IgnoreChange = true;
_inputConnection.Editable.Replace(0, _inputConnection.Editable.Length(), surroundingText);
var diff = GetDiff();
_inputConnection.EditableWrapper.IgnoreChange = false;
_inputConnection.Editable.Replace(diff.index, editableText.Length, diff.diff);
var selection = Client.Selection;
_inputConnection.EditableWrapper.IgnoreChange = false;
_imm.UpdateSelection(_host, selection.Start, selection.End, selection.Start, selection.End);
if(diff.index == 0)
{
var selection = _client.Selection;
_client.Selection = new TextSelection(selection.Start, 0);
_client.Selection = selection;
}
}
(int index, string diff) GetDiff()
{
int index = 0;
var longerLength = Math.Max(surroundingText.Length, editableText.Length);
for (int i = 0; i < longerLength; i++)
{
if (surroundingText.Length == i || editableText.Length == i || surroundingText[i] != editableText[i])
{
index = i;
break;
}
}
var diffString = surroundingText.Substring(index, surroundingText.Length - index);
return (index, diffString);
}
}
public void SetCursorRect(Rect rect)

108
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Runtime.Versioning;
using System.Threading;
using Android.App;
using Android.Content;
using Android.Graphics;
@ -447,19 +448,22 @@ namespace Avalonia.Android.Platform.SkiaPlatform
{
private readonly AvaloniaInputConnection _inputConnection;
public event EventHandler<int> SelectionChanged;
public EditableWrapper(AvaloniaInputConnection inputConnection)
{
_inputConnection = inputConnection;
}
public TextSelection CurrentSelection => new TextSelection(Selection.GetSelectionStart(this), Selection.GetSelectionEnd(this));
public TextSelection CurrentComposition => new TextSelection(BaseInputConnection.GetComposingSpanStart(this), BaseInputConnection.GetComposingSpanEnd(this));
public bool IgnoreChange { get; set; }
public override IEditable Replace(int start, int end, ICharSequence tb)
{
if (!IgnoreChange && start != end)
{
var text = tb.SubSequence(0, tb.Length());
SelectSurroundingTextForDeletion(start, end);
}
@ -470,8 +474,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
{
if (!IgnoreChange && start != end)
{
var text = tb.SubSequence(tbstart, tbend);
SelectSurroundingTextForDeletion(start, end);
}
@ -490,8 +492,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
private readonly IAndroidInputMethod _inputMethod;
private readonly EditableWrapper _editable;
private bool _commitInProgress;
private (int Start, int End)? _composingRegion;
private TextSelection _selection;
private int _batchLevel = 0;
public AvaloniaInputConnection(TopLevelImpl toplevel, IAndroidInputMethod inputMethod) : base(inputMethod.View, true)
{
@ -510,63 +511,87 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public TopLevelImpl Toplevel => _toplevel;
public bool IsInBatchEdit => _batchLevel > 0;
public override bool SetComposingRegion(int start, int end)
{
_composingRegion = new(start, end);
return base.SetComposingRegion(start, end);
}
public override bool SetComposingText(ICharSequence text, int newCursorPosition)
{
if(_composingRegion != null)
BeginBatchEdit();
_editable.IgnoreChange = true;
try
{
// Select the composing region.
InputMethod.Client.Selection = new TextSelection(_composingRegion.Value.Start, _composingRegion.Value.End);
}
var compositionText = text.SubSequence(0, text.Length());
if (_editable.CurrentComposition.Start > -1)
{
// Select the composing region.
InputMethod.Client.Selection = new TextSelection(_editable.CurrentComposition.Start, _editable.CurrentComposition.End);
}
var compositionText = text.SubSequence(0, text.Length());
if (_inputMethod.IsActive && !_commitInProgress)
if (_inputMethod.IsActive && !_commitInProgress)
{
if (string.IsNullOrEmpty(compositionText))
_inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel));
else
_toplevel.TextInput(compositionText);
}
base.SetComposingText(text, newCursorPosition);
}
finally
{
if (string.IsNullOrEmpty(compositionText))
_inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel));
_editable.IgnoreChange = false;
else
_toplevel.TextInput(compositionText);
EndBatchEdit();
}
return true;
}
public override bool BeginBatchEdit()
{
_batchLevel = Interlocked.Increment(ref _batchLevel);
return base.BeginBatchEdit();
}
public override bool EndBatchEdit()
{
_batchLevel = Interlocked.Decrement(ref _batchLevel);
_inputMethod.OnBatchEditedEnded();
return base.EndBatchEdit();
}
public override bool CommitText(ICharSequence text, int newCursorPosition)
{
BeginBatchEdit();
_commitInProgress = true;
var ret = base.CommitText(text, newCursorPosition);
var composingRegion = _editable.CurrentComposition;
var committedText = text.SubSequence(0, text.Length());
var ret = base.CommitText(text, newCursorPosition);
if (_inputMethod.IsActive && !string.IsNullOrEmpty(committedText))
if(composingRegion.Start != -1)
{
if(_composingRegion != null)
{
_inputMethod.Client.Selection = new TextSelection(_composingRegion.Value.Start, _composingRegion.Value.End);
}
InputMethod.Client.Selection = composingRegion;
}
_toplevel.TextInput(committedText);
var committedText = text.SubSequence(0, text.Length());
_composingRegion = null;
}
if (_inputMethod.IsActive)
if (string.IsNullOrEmpty(committedText))
_inputMethod.View.DispatchKeyEvent(new KeyEvent(KeyEventActions.Down, Keycode.ForwardDel));
else
_toplevel.TextInput(committedText);
_commitInProgress = false;
EndBatchEdit();
return ret;
}
public override bool FinishComposingText()
{
_composingRegion = null;
return base.FinishComposingText();
return true;
}
public override bool DeleteSurroundingText(int beforeLength, int afterLength)
@ -575,11 +600,10 @@ namespace Avalonia.Android.Platform.SkiaPlatform
{
EditableWrapper.IgnoreChange = true;
}
var result = base.DeleteSurroundingText(beforeLength, afterLength);
if (InputMethod.IsActive)
{
var selection = _selection;
var selection = _editable.CurrentSelection;
InputMethod.Client.Selection = new TextSelection(selection.Start - beforeLength, selection.End + afterLength);
@ -588,13 +612,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
EditableWrapper.IgnoreChange = true;
}
return result;
}
public override bool SetSelection(int start, int end)
{
_selection = new TextSelection(start, end);
return base.SetSelection(start, end);
return true;
}
public override bool PerformEditorAction([GeneratedEnum] ImeAction actionCode)
@ -630,7 +648,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
return null;
}
var selection = _selection;
var selection = _editable.CurrentSelection;
ExtractedText extract = new ExtractedText
{

1
src/Avalonia.Base/Animation/Transitions/Rotate3DTransition.cs

@ -89,6 +89,7 @@ public class Rotate3DTransition: PageSlide
{
Easing = SlideInEasing,
Duration = Duration,
FillMode = FillMode.Forward,
Children =
{
CreateKeyFrame(0d, 90d * (forward ? 1 : -1), 1),

6
src/Avalonia.Base/Avalonia.Base.csproj

@ -43,21 +43,15 @@
<InternalsVisibleTo Include="Avalonia.FreeDesktop, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.X11, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Browser, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.OpenGL, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Skia, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.DesignerSupport, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Direct2D1.RenderTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.LeakTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Markup, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Markup.Xaml, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Markup.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Markup.Xaml.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Skia.RenderTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Skia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Benchmarks, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.X11, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Win32, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Android, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.iOS, PublicKey=$(AvaloniaPublicKey)" />

20
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -60,6 +60,8 @@ namespace Avalonia
}
/// <inheritdoc cref="GetObservable{T}(AvaloniaObject, AvaloniaProperty{T})"/>
/// <typeparam name="TSource">The type of the values held by the <paramref name="property"/>.</typeparam>
/// <typeparam name="TResult">The type of the value returned by the <paramref name="converter"/>.</typeparam>
/// <param name="o"/>
/// <param name="property"/>
/// <param name="converter">A method which is executed to convert each property value to <typeparamref name="TResult"/>.</param>
@ -71,6 +73,15 @@ namespace Avalonia
converter ?? throw new ArgumentNullException(nameof(converter)));
}
/// <inheritdoc cref="GetObservable{TSource,TResult}"/>
public static IObservable<TResult> GetObservable<TResult>(this AvaloniaObject o, AvaloniaProperty property, Func<object?, TResult> converter)
{
return new AvaloniaPropertyObservable<object?, TResult>(
o ?? throw new ArgumentNullException(nameof(o)),
property ?? throw new ArgumentNullException(nameof(property)),
converter ?? throw new ArgumentNullException(nameof(converter)));
}
/// <summary>
/// Gets an observable for an <see cref="AvaloniaProperty"/>.
/// </summary>
@ -92,6 +103,15 @@ namespace Avalonia
property ?? throw new ArgumentNullException(nameof(property)));
}
/// <inheritdoc cref="GetObservable{TSource,TResult}"/>
public static IObservable<BindingValue<TResult>> GetBindingObservable<TResult>(this AvaloniaObject o, AvaloniaProperty property, Func<object?, TResult> converter)
{
return new AvaloniaPropertyBindingObservable<object?, TResult>(
o ?? throw new ArgumentNullException(nameof(o)),
property ?? throw new ArgumentNullException(nameof(property)),
converter?? throw new ArgumentNullException(nameof(converter)));
}
/// <summary>
/// Gets an observable for an <see cref="AvaloniaProperty"/>.
/// </summary>

6
src/Avalonia.Base/Media/GlyphRun.cs

@ -424,13 +424,13 @@ namespace Avalonia.Media
/// </returns>
public CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit)
{
var previousCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
if (characterHit.TrailingLength != 0)
{
return new CharacterHit(characterHit.FirstCharacterIndex);
return previousCharacterHit;
}
var previousCharacterHit = FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
return new CharacterHit(previousCharacterHit.FirstCharacterIndex);
}

325
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -9,7 +9,7 @@ namespace Avalonia.Media.TextFormatting
internal static Comparer<TextBounds> TextBoundsComparer { get; } =
Comparer<TextBounds>.Create((x, y) => x.Rectangle.Left.CompareTo(y.Rectangle.Left));
private IReadOnlyList<IndexedTextRun>? _indexedTextRuns;
internal IReadOnlyList<IndexedTextRun>? _indexedTextRuns;
private readonly TextRun[] _textRuns;
private readonly double _paragraphWidth;
private readonly TextParagraphProperties _paragraphProperties;
@ -204,7 +204,7 @@ namespace Avalonia.Media.TextFormatting
if (_paragraphProperties.FlowDirection == FlowDirection.RightToLeft)
{
currentPosition = Length - firstRun.Length;
currentPosition += lineLength - firstRun.Length;
}
return GetRunCharacterHit(firstRun, currentPosition, 0);
@ -216,7 +216,7 @@ namespace Avalonia.Media.TextFormatting
if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight)
{
currentPosition = lineLength - lastRun.Length;
currentPosition += lineLength - lastRun.Length;
}
return GetRunCharacterHit(lastRun, currentPosition, distance);
@ -512,38 +512,45 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/>
public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
{
if (_textRuns.Length == 0)
if (_textRuns.Length == 0 || _indexedTextRuns is null)
{
return new CharacterHit();
}
if (TryFindNextCharacterHit(characterHit, out var nextCharacterHit))
{
return nextCharacterHit;
}
var lastTextPosition = FirstTextSourceIndex + Length;
var currentCharacterrHit = characterHit;
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
// Can't move, we're after the last character
var runIndex = GetRunIndexAtCharacterIndex(lastTextPosition, LogicalDirection.Forward, out var currentPosition);
var currentRun = GetRunAtCharacterIndex(characterIndex, LogicalDirection.Forward, out var currentPosition);
var currentRun = _textRuns[runIndex];
var nextCharacterHit = characterHit;
switch (currentRun)
{
case ShapedTextRun shapedRun:
{
nextCharacterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(characterHit);
var offset = Math.Max(0, currentPosition - shapedRun.GlyphRun.Metrics.FirstCluster - characterHit.TrailingLength);
if (offset > 0)
{
currentCharacterrHit = new CharacterHit(Math.Max(0, characterHit.FirstCharacterIndex - offset), characterHit.TrailingLength);
}
nextCharacterHit = shapedRun.GlyphRun.GetNextCaretCharacterHit(currentCharacterrHit);
if (offset > 0)
{
nextCharacterHit = new CharacterHit(nextCharacterHit.FirstCharacterIndex + offset, nextCharacterHit.TrailingLength);
}
break;
}
default:
case TextRun:
{
nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length);
break;
}
}
if (characterHit.FirstCharacterIndex + characterHit.TrailingLength == nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength)
if (characterIndex == nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength)
{
return characterHit;
}
@ -554,17 +561,75 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/>
public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit)
{
if (TryFindPreviousCharacterHit(characterHit, out var previousCharacterHit))
if (_textRuns.Length == 0 || _indexedTextRuns is null)
{
return new CharacterHit();
}
if (characterHit.TrailingLength > 0 && characterHit.FirstCharacterIndex <= FirstTextSourceIndex)
{
return new CharacterHit(FirstTextSourceIndex);
}
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
if (characterIndex <= FirstTextSourceIndex)
{
return previousCharacterHit;
return new CharacterHit(FirstTextSourceIndex);
}
var currentCharacterrHit = characterHit;
var currentRun = GetRunAtCharacterIndex(characterIndex, LogicalDirection.Backward, out var currentPosition);
if (currentPosition == characterHit.FirstCharacterIndex)
{
currentRun = GetRunAtCharacterIndex(characterHit.FirstCharacterIndex, LogicalDirection.Backward, out currentPosition);
}
var previousCharacterHit = characterHit;
switch (currentRun)
{
case ShapedTextRun shapedRun:
{
var offset = Math.Max(0, currentPosition - shapedRun.GlyphRun.Metrics.FirstCluster);
if (offset > 0)
{
currentCharacterrHit = new CharacterHit(Math.Max(0, characterHit.FirstCharacterIndex - offset), characterHit.TrailingLength);
}
previousCharacterHit = shapedRun.GlyphRun.GetPreviousCaretCharacterHit(currentCharacterrHit);
if (offset > 0)
{
previousCharacterHit = new CharacterHit(previousCharacterHit.FirstCharacterIndex + offset, previousCharacterHit.TrailingLength);
}
break;
}
case TextRun:
{
if (characterHit.TrailingLength > 0)
{
previousCharacterHit = new CharacterHit(currentPosition, currentRun.Length);
}
else
{
previousCharacterHit = new CharacterHit(currentPosition + currentRun.Length);
}
break;
}
}
if (characterHit.FirstCharacterIndex <= FirstTextSourceIndex)
if (characterIndex == previousCharacterHit.FirstCharacterIndex + previousCharacterHit.TrailingLength)
{
characterHit = new CharacterHit(FirstTextSourceIndex);
return characterHit;
}
return characterHit; // Can't move, we're before the first character
return previousCharacterHit;
}
/// <inheritdoc/>
@ -1009,161 +1074,7 @@ namespace Avalonia.Media.TextFormatting
if (_textLineBreak is null && _textRuns.Length > 1 && _textRuns[_textRuns.Length - 1] is TextEndOfLine textEndOfLine)
{
_textLineBreak = new TextLineBreak(textEndOfLine);
}
}
/// <summary>
/// Tries to find the next character hit.
/// </summary>
/// <param name="characterHit">The current character hit.</param>
/// <param name="nextCharacterHit">The next character hit.</param>
/// <returns></returns>
private bool TryFindNextCharacterHit(CharacterHit characterHit, out CharacterHit nextCharacterHit)
{
nextCharacterHit = characterHit;
var codepointIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
var lastCodepointIndex = FirstTextSourceIndex + Length;
if (codepointIndex >= lastCodepointIndex)
{
return false; // Cannot go forward anymore
}
if (codepointIndex < FirstTextSourceIndex)
{
codepointIndex = FirstTextSourceIndex;
}
var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward, out var currentPosition);
while (runIndex < _textRuns.Length)
{
var currentRun = _textRuns[runIndex];
switch (currentRun)
{
case ShapedTextRun shapedRun:
{
var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _);
var isAtEnd = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength == FirstTextSourceIndex + Length;
if (isAtEnd && !shapedRun.GlyphRun.IsLeftToRight)
{
nextCharacterHit = foundCharacterHit;
return true;
}
nextCharacterHit = isAtEnd || characterHit.TrailingLength != 0 ?
foundCharacterHit :
new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength);
if (isAtEnd || nextCharacterHit.FirstCharacterIndex > characterHit.FirstCharacterIndex)
{
return true;
}
break;
}
default:
{
var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
if (textPosition == currentPosition)
{
nextCharacterHit = new CharacterHit(currentPosition + currentRun.Length);
return true;
}
break;
}
}
currentPosition += currentRun.Length;
runIndex++;
}
return false;
}
/// <summary>
/// Tries to find the previous character hit.
/// </summary>
/// <param name="characterHit">The current character hit.</param>
/// <param name="previousCharacterHit">The previous character hit.</param>
/// <returns></returns>
private bool TryFindPreviousCharacterHit(CharacterHit characterHit, out CharacterHit previousCharacterHit)
{
var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength;
if (characterIndex == FirstTextSourceIndex)
{
previousCharacterHit = new CharacterHit(FirstTextSourceIndex);
return true;
}
previousCharacterHit = characterHit;
if (characterIndex < FirstTextSourceIndex)
{
return false; // Cannot go backward anymore.
}
var runIndex = GetRunIndexAtCharacterIndex(characterIndex, LogicalDirection.Backward, out var currentPosition);
while (runIndex >= 0)
{
var currentRun = _textRuns[runIndex];
switch (currentRun)
{
case ShapedTextRun shapedRun:
{
var foundCharacterHit = shapedRun.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _);
if (foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength < characterIndex)
{
previousCharacterHit = foundCharacterHit;
return true;
}
var previousPosition = foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength;
if (foundCharacterHit.TrailingLength > 0 && previousPosition == characterIndex)
{
previousCharacterHit = new CharacterHit(foundCharacterHit.FirstCharacterIndex);
}
if (previousCharacterHit != characterHit)
{
return true;
}
break;
}
default:
{
if (characterIndex == currentPosition + currentRun.Length)
{
previousCharacterHit = new CharacterHit(currentPosition);
return true;
}
break;
}
}
currentPosition -= currentRun.Length;
runIndex--;
}
return false;
}
/// <summary>
@ -1173,15 +1084,23 @@ namespace Avalonia.Media.TextFormatting
/// <param name="direction">The logical direction.</param>
/// <param name="textPosition">The text position of the found run index.</param>
/// <returns>The text run index.</returns>
private int GetRunIndexAtCharacterIndex(int codepointIndex, LogicalDirection direction, out int textPosition)
private TextRun? GetRunAtCharacterIndex(int codepointIndex, LogicalDirection direction, out int textPosition)
{
var runIndex = 0;
textPosition = FirstTextSourceIndex;
if (_indexedTextRuns is null)
{
return null;
}
TextRun? currentRun = null;
TextRun? previousRun = null;
while (runIndex < _textRuns.Length)
while (runIndex < _indexedTextRuns.Count)
{
var currentRun = _textRuns[runIndex];
var indexedRun = _indexedTextRuns[runIndex];
currentRun = indexedRun.TextRun;
switch (currentRun)
{
@ -1189,64 +1108,49 @@ namespace Avalonia.Media.TextFormatting
{
var firstCluster = shapedRun.GlyphRun.Metrics.FirstCluster;
if (firstCluster > codepointIndex)
{
break;
}
if (previousRun is ShapedTextRun previousShaped && !previousShaped.ShapedBuffer.IsLeftToRight)
{
if (shapedRun.ShapedBuffer.IsLeftToRight)
{
if (firstCluster >= codepointIndex)
{
return --runIndex;
}
}
else
{
if (codepointIndex > firstCluster + currentRun.Length)
{
return --runIndex;
}
}
}
firstCluster += Math.Max(0, indexedRun.TextSourceCharacterIndex - firstCluster);
if (direction == LogicalDirection.Forward)
{
if (codepointIndex >= firstCluster && codepointIndex <= firstCluster + currentRun.Length)
if (codepointIndex >= firstCluster && codepointIndex < firstCluster + currentRun.Length)
{
return runIndex;
return currentRun;
}
}
else
{
if (codepointIndex > firstCluster &&
codepointIndex <= firstCluster + currentRun.Length)
if (previousRun is not null && previousRun is not ShapedTextRun && codepointIndex == textPosition + firstCluster)
{
textPosition -= previousRun.Length;
return previousRun;
}
if (codepointIndex > firstCluster && codepointIndex <= firstCluster + currentRun.Length)
{
return runIndex;
return currentRun;
}
}
if (runIndex + 1 >= _textRuns.Length)
{
return runIndex;
return currentRun;
}
textPosition += currentRun.Length;
break;
}
default:
case TextRun:
{
if (codepointIndex == textPosition)
{
return runIndex;
return currentRun;
}
if (runIndex + 1 >= _textRuns.Length)
{
return runIndex;
return currentRun;
}
textPosition += currentRun.Length;
@ -1257,10 +1161,11 @@ namespace Avalonia.Media.TextFormatting
}
runIndex++;
previousRun = currentRun;
}
return runIndex;
return currentRun;
}
private TextLineMetrics CreateLineMetrics()

122
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@ -142,93 +142,6 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester
QueueUpdate();
}
private static void SyncChildren(Visual v)
{
//TODO: Optimize by moving that logic to Visual itself
if(v.CompositionVisual == null)
return;
var compositionChildren = v.CompositionVisual.Children;
var visualChildren = (AvaloniaList<Visual>)v.GetVisualChildren();
PooledList<(Visual visual, int index)>? sortedChildren = null;
if (v.HasNonUniformZIndexChildren && visualChildren.Count > 1)
{
sortedChildren = new (visualChildren.Count);
for (var c = 0; c < visualChildren.Count; c++)
sortedChildren.Add((visualChildren[c], c));
// Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements.
sortedChildren.Sort(static (lhs, rhs) =>
{
var result = lhs.visual.ZIndex.CompareTo(rhs.visual.ZIndex);
return result == 0 ? lhs.index.CompareTo(rhs.index) : result;
});
}
var childVisual = v.ChildCompositionVisual;
// Check if the current visual somehow got migrated to another compositor
if (childVisual != null && childVisual.Compositor != v.CompositionVisual.Compositor)
childVisual = null;
var expectedCount = visualChildren.Count;
if (childVisual != null)
expectedCount++;
if (compositionChildren.Count == expectedCount)
{
bool mismatch = false;
if (sortedChildren != null)
for (var c = 0; c < visualChildren.Count; c++)
{
if (!ReferenceEquals(compositionChildren[c], sortedChildren[c].visual.CompositionVisual))
{
mismatch = true;
break;
}
}
else
for (var c = 0; c < visualChildren.Count; c++)
if (!ReferenceEquals(compositionChildren[c], visualChildren[c].CompositionVisual))
{
mismatch = true;
break;
}
if (childVisual != null &&
!ReferenceEquals(compositionChildren[compositionChildren.Count - 1], childVisual))
mismatch = true;
if (!mismatch)
{
sortedChildren?.Dispose();
return;
}
}
compositionChildren.Clear();
if (sortedChildren != null)
{
foreach (var ch in sortedChildren)
{
var compositionChild = ch.visual.CompositionVisual;
if (compositionChild != null)
compositionChildren.Add(compositionChild);
}
sortedChildren.Dispose();
}
else
foreach (var ch in visualChildren)
{
var compositionChild = ch.CompositionVisual;
if (compositionChild != null)
compositionChildren.Add(compositionChild);
}
if (childVisual != null)
compositionChildren.Add(childVisual);
}
private void UpdateCore()
{
_queuedUpdate = false;
@ -238,36 +151,7 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester
if(comp == null)
continue;
// TODO: Optimize all of that by moving to the Visual itself, so we won't have to recalculate every time
comp.Offset = new (visual.Bounds.Left, visual.Bounds.Top, 0);
comp.Size = new (visual.Bounds.Width, visual.Bounds.Height);
comp.Visible = visual.IsVisible;
comp.Opacity = (float)visual.Opacity;
comp.ClipToBounds = visual.ClipToBounds;
comp.Clip = visual.Clip?.PlatformImpl;
if (!Equals(comp.OpacityMask, visual.OpacityMask))
comp.OpacityMask = visual.OpacityMask?.ToImmutable();
if (!comp.Effect.EffectEquals(visual.Effect))
comp.Effect = visual.Effect?.ToImmutable();
comp.RenderOptions = visual.RenderOptions;
var renderTransform = Matrix.Identity;
if (visual.HasMirrorTransform)
renderTransform = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0);
if (visual.RenderTransform != null)
{
var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height));
var offset = Matrix.CreateTranslation(origin);
renderTransform *= (-offset) * visual.RenderTransform.Value * (offset);
}
comp.TransformMatrix = renderTransform;
visual.SynchronizeCompositionProperties();
try
{
@ -279,11 +163,11 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester
_recorder.Reset();
}
SyncChildren(visual);
visual.SynchronizeCompositionChildVisuals();
}
foreach(var v in _recalculateChildren)
if (!_dirty.Contains(v))
SyncChildren(v);
v.SynchronizeCompositionChildVisuals();
_dirty.Clear();
_recalculateChildren.Clear();
CompositionTarget.Size = _root.ClientSize;

13
src/Avalonia.Base/Rendering/Composition/CompositionExternalMemory.cs

@ -1,10 +1,13 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading.Tasks;
using Avalonia.Metadata;
using Avalonia.Platform;
namespace Avalonia.Rendering.Composition;
[NotClientImplementable]
public interface ICompositionGpuInterop
{
/// <summary>
@ -84,14 +87,22 @@ public enum CompositionGpuImportedImageSynchronizationCapabilities
/// <summary>
/// An imported GPU object that's usable by composition APIs
/// </summary>
[NotClientImplementable]
public interface ICompositionGpuImportedObject : IAsyncDisposable
{
/// <summary>
/// Tracks the import status of the object. Once the task is completed,
/// the user code is allowed to free the resource owner in case when a non-owning
/// sharing handle was used
/// sharing handle was used.
/// </summary>
Task ImportCompleted { get; }
/// <inheritdoc cref="ImportCompleted"/>
/// <seealso cref="ImportCompleted">ImportCompleted (recommended replacement)</seealso>
[Obsolete("Please use ICompositionGpuImportedObject.ImportCompleted instead")]
[EditorBrowsable(EditorBrowsableState.Never)]
Task ImportCompeted { get; }
/// <summary>
/// Indicates if the device context this instance is associated with is no longer available
/// </summary>

8
src/Avalonia.Base/Rendering/Composition/CompositionInterop.cs

@ -67,18 +67,20 @@ abstract class CompositionGpuImportedObjectBase : ICompositionGpuImportedObject
Context = context;
Feature = feature;
ImportCompeted = Compositor.InvokeServerJobAsync(Import);
ImportCompleted = Compositor.InvokeServerJobAsync(Import);
}
protected abstract void Import();
public abstract void Dispose();
public Task ImportCompeted { get; }
public Task ImportCompleted { get; }
public Task ImportCompeted => ImportCompleted;
public bool IsLost => Context.IsLost;
public ValueTask DisposeAsync() => new(Compositor.InvokeServerJobAsync(() =>
{
if (ImportCompeted.Status == TaskStatus.RanToCompletion)
if (ImportCompleted.Status == TaskStatus.RanToCompletion)
Dispose();
}));
}

6
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawingSurface.cs

@ -31,12 +31,12 @@ internal class ServerCompositionDrawingSurface : ServerCompositionSurface, IDisp
throw new PlatformGraphicsContextLostException();
// This should never happen, but check for it anyway to avoid a deadlock
if (!image.ImportCompeted.IsCompleted)
if (!image.ImportCompleted.IsCompleted)
throw new InvalidOperationException("The import operation is not completed yet");
// Rethrow the import here exception
if (image.ImportCompeted.IsFaulted)
image.ImportCompeted.GetAwaiter().GetResult();
if (image.ImportCompleted.IsFaulted)
image.ImportCompleted.GetAwaiter().GetResult();
}
void Update(IBitmapImpl newImage, IPlatformRenderInterfaceContext context)

7
src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs

@ -99,14 +99,15 @@ namespace Avalonia.Threading
}
}
public static RestoreContext Ensure(DispatcherPriority priority)
public static RestoreContext Ensure(DispatcherPriority priority) => Ensure(Dispatcher.UIThread, priority);
public static RestoreContext Ensure(Dispatcher dispatcher, DispatcherPriority priority)
{
if (Current is AvaloniaSynchronizationContext avaloniaContext
&& avaloniaContext.Priority == priority)
return default;
var oldContext = Current;
Dispatcher.UIThread.VerifyAccess();
SetSynchronizationContext(Dispatcher.UIThread.GetContextWithPriority(priority));
dispatcher.VerifyAccess();
SetSynchronizationContext(dispatcher.GetContextWithPriority(priority));
return new RestoreContext(oldContext);
}
}

8
src/Avalonia.Base/Threading/Dispatcher.Invoke.cs

@ -98,7 +98,7 @@ public partial class Dispatcher
if (timeout.TotalMilliseconds < 0 &&
timeout != TimeSpan.FromMilliseconds(-1))
{
throw new ArgumentOutOfRangeException("timeout");
throw new ArgumentOutOfRangeException(nameof(timeout));
}
// Fast-Path: if on the same thread, and invoking at Send priority,
@ -106,7 +106,7 @@ public partial class Dispatcher
// call the callback directly.
if (!cancellationToken.IsCancellationRequested && priority == DispatcherPriority.Send && CheckAccess())
{
using (AvaloniaSynchronizationContext.Ensure(priority))
using (AvaloniaSynchronizationContext.Ensure(this, priority))
callback();
return;
}
@ -220,7 +220,7 @@ public partial class Dispatcher
if (timeout.TotalMilliseconds < 0 &&
timeout != TimeSpan.FromMilliseconds(-1))
{
throw new ArgumentOutOfRangeException("timeout");
throw new ArgumentOutOfRangeException(nameof(timeout));
}
// Fast-Path: if on the same thread, and invoking at Send priority,
@ -228,7 +228,7 @@ public partial class Dispatcher
// call the callback directly.
if (!cancellationToken.IsCancellationRequested && priority == DispatcherPriority.Send && CheckAccess())
{
using (AvaloniaSynchronizationContext.Ensure(priority))
using (AvaloniaSynchronizationContext.Ensure(this, priority))
return callback();
}

2
src/Avalonia.Base/Threading/Dispatcher.MainLoop.cs

@ -49,7 +49,7 @@ public partial class Dispatcher
try
{
_frames.Push(frame);
using (AvaloniaSynchronizationContext.Ensure(DispatcherPriority.Normal))
using (AvaloniaSynchronizationContext.Ensure(this, DispatcherPriority.Normal))
frame.Run(_controlledImpl);
}
finally

20
src/Avalonia.Base/Threading/Dispatcher.Queue.cs

@ -9,7 +9,9 @@ public partial class Dispatcher
private readonly DispatcherPriorityQueue _queue = new();
private bool _signaled;
private bool _explicitBackgroundProcessingRequested;
private const int MaximumTimeProcessingBackgroundJobs = 50;
private const int MaximumInputStarvationTimeInFallbackMode = 50;
private const int MaximumInputStarvationTimeInExplicitProcessingExplicitMode = 50;
private int _maximumInputStarvationTime;
void RequestBackgroundProcessing()
{
@ -35,8 +37,8 @@ public partial class Dispatcher
lock (InstanceLock)
{
_explicitBackgroundProcessingRequested = false;
ExecuteJobsCore();
}
ExecuteJobsCore(true);
}
/// <summary>
@ -130,10 +132,10 @@ public partial class Dispatcher
lock (InstanceLock)
_signaled = false;
ExecuteJobsCore();
ExecuteJobsCore(false);
}
void ExecuteJobsCore()
void ExecuteJobsCore(bool fromExplicitBackgroundProcessingCallback)
{
long? backgroundJobExecutionStartedAt = null;
while (true)
@ -151,7 +153,6 @@ public partial class Dispatcher
if (job.Priority > DispatcherPriority.Input)
{
ExecuteJob(job);
backgroundJobExecutionStartedAt = null;
}
// If platform supports pending input query, ask the platform if we can continue running low priority jobs
else if (_pendingInputImpl?.CanQueryPendingInput == true)
@ -164,6 +165,13 @@ public partial class Dispatcher
return;
}
}
// We can't ask if the implementation has pending input, so we should let it to call us back
// Once it thinks that input is handled
else if (_backgroundProcessingImpl != null && !fromExplicitBackgroundProcessingCallback)
{
RequestBackgroundProcessing();
return;
}
// We can't check if there is pending input, but still need to enforce interactivity
// so we stop processing background jobs after some timeout and start a timer to continue later
else
@ -171,7 +179,7 @@ public partial class Dispatcher
if (backgroundJobExecutionStartedAt == null)
backgroundJobExecutionStartedAt = Now;
if (Now - backgroundJobExecutionStartedAt.Value > MaximumTimeProcessingBackgroundJobs)
if (Now - backgroundJobExecutionStartedAt.Value > _maximumInputStarvationTime)
{
_signaled = true;
RequestBackgroundProcessing();

2
src/Avalonia.Base/Threading/Dispatcher.Timers.cs

@ -127,7 +127,7 @@ public partial class Dispatcher
if (needToPromoteTimers)
PromoteTimers();
if (needToProcessQueue)
ExecuteJobsCore();
ExecuteJobsCore(false);
UpdateOSTimer();
}

3
src/Avalonia.Base/Threading/Dispatcher.cs

@ -34,6 +34,9 @@ public partial class Dispatcher : IDispatcher
_controlledImpl = _impl as IControlledDispatcherImpl;
_pendingInputImpl = _impl as IDispatcherImplWithPendingInput;
_backgroundProcessingImpl = _impl as IDispatcherImplWithExplicitBackgroundProcessing;
_maximumInputStarvationTime = _backgroundProcessingImpl == null ?
MaximumInputStarvationTimeInFallbackMode :
MaximumInputStarvationTimeInExplicitProcessingExplicitMode;
if (_backgroundProcessingImpl != null)
_backgroundProcessingImpl.ReadyForBackgroundProcessing += OnReadyForExplicitBackgroundProcessing;
}

8
src/Avalonia.Base/Threading/DispatcherFrame.cs

@ -38,10 +38,14 @@ public class DispatcherFrame
/// for their important criteria to be met. These frames
/// should have a timeout associated with them.
/// </param>
public DispatcherFrame(bool exitWhenRequested)
public DispatcherFrame(bool exitWhenRequested) : this(Dispatcher.UIThread, exitWhenRequested)
{
Dispatcher = Dispatcher.UIThread;
Dispatcher.VerifyAccess();
}
internal DispatcherFrame(Dispatcher dispatcher, bool exitWhenRequested)
{
Dispatcher = dispatcher;
_exitWhenRequested = exitWhenRequested;
_continue = true;
}

2
src/Avalonia.Base/Threading/DispatcherOperation.cs

@ -258,7 +258,7 @@ public class DispatcherOperation
try
{
using (AvaloniaSynchronizationContext.Ensure(Priority))
using (AvaloniaSynchronizationContext.Ensure(Dispatcher, Priority))
InvokeCore();
}
finally

12
src/Avalonia.Base/Threading/DispatcherTimer.cs

@ -112,11 +112,11 @@ public partial class DispatcherTimer
bool updateOSTimer = false;
if (value.TotalMilliseconds < 0)
throw new ArgumentOutOfRangeException("value",
throw new ArgumentOutOfRangeException(nameof(value),
"TimeSpan period must be greater than or equal to zero.");
if (value.TotalMilliseconds > Int32.MaxValue)
throw new ArgumentOutOfRangeException("value",
throw new ArgumentOutOfRangeException(nameof(value),
"TimeSpan period must be less than or equal to Int32.MaxValue.");
lock (_instanceLock)
@ -259,14 +259,14 @@ public partial class DispatcherTimer
DispatcherPriority.Validate(priority, "priority");
if (priority == DispatcherPriority.Inactive)
{
throw new ArgumentException("Specified priority is not valid.", "priority");
throw new ArgumentException("Specified priority is not valid.", nameof(priority));
}
if (interval.TotalMilliseconds < 0)
throw new ArgumentOutOfRangeException("interval", "TimeSpan period must be greater than or equal to zero.");
throw new ArgumentOutOfRangeException(nameof(interval), "TimeSpan period must be greater than or equal to zero.");
if (interval.TotalMilliseconds > Int32.MaxValue)
throw new ArgumentOutOfRangeException("interval",
throw new ArgumentOutOfRangeException(nameof(interval),
"TimeSpan period must be less than or equal to Int32.MaxValue.");
@ -349,4 +349,4 @@ public partial class DispatcherTimer
// used by Dispatcher
internal long DueTimeInMs { get; private set; }
}
}

165
src/Avalonia.Base/Visual.Composition.cs

@ -0,0 +1,165 @@
using Avalonia.Collections;
using Avalonia.Collections.Pooled;
using Avalonia.Media;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Server;
using Avalonia.VisualTree;
namespace Avalonia;
public partial class Visual
{
internal CompositionDrawListVisual? CompositionVisual { get; private set; }
internal CompositionVisual? ChildCompositionVisual { get; set; }
private protected virtual CompositionDrawListVisual CreateCompositionVisual(Compositor compositor)
=> new CompositionDrawListVisual(compositor,
new ServerCompositionDrawListVisual(compositor.Server, this), this);
internal CompositionVisual AttachToCompositor(Compositor compositor)
{
if (CompositionVisual == null || CompositionVisual.Compositor != compositor)
{
CompositionVisual = CreateCompositionVisual(compositor);
}
return CompositionVisual;
}
internal virtual void DetachFromCompositor()
{
if (CompositionVisual != null)
{
if (ChildCompositionVisual != null)
CompositionVisual.Children.Remove(ChildCompositionVisual);
CompositionVisual.DrawList = null;
CompositionVisual = null;
}
}
internal virtual void SynchronizeCompositionChildVisuals()
{
if(CompositionVisual == null)
return;
var compositionChildren = CompositionVisual.Children;
var visualChildren = (AvaloniaList<Visual>)VisualChildren;
PooledList<(Visual visual, int index)>? sortedChildren = null;
if (HasNonUniformZIndexChildren && visualChildren.Count > 1)
{
sortedChildren = new (visualChildren.Count);
for (var c = 0; c < visualChildren.Count; c++)
sortedChildren.Add((visualChildren[c], c));
// Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements.
sortedChildren.Sort(static (lhs, rhs) =>
{
var result = lhs.visual.ZIndex.CompareTo(rhs.visual.ZIndex);
return result == 0 ? lhs.index.CompareTo(rhs.index) : result;
});
}
var childVisual = ChildCompositionVisual;
// Check if the current visual somehow got migrated to another compositor
if (childVisual != null && childVisual.Compositor != CompositionVisual.Compositor)
childVisual = null;
var expectedCount = visualChildren.Count;
if (childVisual != null)
expectedCount++;
if (compositionChildren.Count == expectedCount)
{
bool mismatch = false;
if (sortedChildren != null)
for (var c = 0; c < visualChildren.Count; c++)
{
if (!ReferenceEquals(compositionChildren[c], sortedChildren[c].visual.CompositionVisual))
{
mismatch = true;
break;
}
}
else
for (var c = 0; c < visualChildren.Count; c++)
if (!ReferenceEquals(compositionChildren[c], visualChildren[c].CompositionVisual))
{
mismatch = true;
break;
}
if (childVisual != null &&
!ReferenceEquals(compositionChildren[compositionChildren.Count - 1], childVisual))
mismatch = true;
if (!mismatch)
{
sortedChildren?.Dispose();
return;
}
}
compositionChildren.Clear();
if (sortedChildren != null)
{
foreach (var ch in sortedChildren)
{
var compositionChild = ch.visual.CompositionVisual;
if (compositionChild != null)
compositionChildren.Add(compositionChild);
}
sortedChildren.Dispose();
}
else
foreach (var ch in visualChildren)
{
var compositionChild = ch.CompositionVisual;
if (compositionChild != null)
compositionChildren.Add(compositionChild);
}
if (childVisual != null)
compositionChildren.Add(childVisual);
}
internal virtual void SynchronizeCompositionProperties()
{
if(CompositionVisual == null)
return;
var comp = CompositionVisual;
// TODO: Introduce a dirty mask like WPF has, so we don't overwrite properties every time
comp.Offset = new (Bounds.Left, Bounds.Top, 0);
comp.Size = new (Bounds.Width, Bounds.Height);
comp.Visible = IsVisible;
comp.Opacity = (float)Opacity;
comp.ClipToBounds = ClipToBounds;
comp.Clip = Clip?.PlatformImpl;
if (!Equals(comp.OpacityMask, OpacityMask))
comp.OpacityMask = OpacityMask?.ToImmutable();
if (!comp.Effect.EffectEquals(Effect))
comp.Effect = Effect?.ToImmutable();
comp.RenderOptions = RenderOptions;
var renderTransform = Matrix.Identity;
if (HasMirrorTransform)
renderTransform = new Matrix(-1.0, 0.0, 0.0, 1.0, Bounds.Width, 0);
if (RenderTransform != null)
{
var origin = RenderTransformOrigin.ToPixels(new Size(Bounds.Width, Bounds.Height));
var offset = Matrix.CreateTranslation(origin);
renderTransform *= (-offset) * RenderTransform.Value * (offset);
}
comp.TransformMatrix = renderTransform;
}
}

28
src/Avalonia.Base/Visual.cs

@ -29,7 +29,7 @@ namespace Avalonia
/// extension methods defined in <see cref="VisualExtensions"/>.
/// </remarks>
[UsableDuringInitialization]
public class Visual : StyledElement
public partial class Visual : StyledElement
{
/// <summary>
/// Defines the <see cref="Bounds"/> property.
@ -316,9 +316,6 @@ namespace Avalonia
/// </summary>
protected internal IRenderRoot? VisualRoot => _visualRoot ?? (this as IRenderRoot);
internal CompositionDrawListVisual? CompositionVisual { get; private set; }
internal CompositionVisual? ChildCompositionVisual { get; set; }
internal RenderOptions RenderOptions { get; set; }
internal bool HasNonUniformZIndexChildren { get; private set; }
@ -515,20 +512,6 @@ namespace Avalonia
}
}
private protected virtual CompositionDrawListVisual CreateCompositionVisual(Compositor compositor)
=> new CompositionDrawListVisual(compositor,
new ServerCompositionDrawListVisual(compositor.Server, this), this);
internal CompositionVisual AttachToCompositor(Compositor compositor)
{
if (CompositionVisual == null || CompositionVisual.Compositor != compositor)
{
CompositionVisual = CreateCompositionVisual(compositor);
}
return CompositionVisual;
}
/// <summary>
/// Calls the <see cref="OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs)"/> method
/// for this control and all of its visual descendants.
@ -547,14 +530,7 @@ namespace Avalonia
DisableTransitions();
OnDetachedFromVisualTree(e);
if (CompositionVisual != null)
{
if (ChildCompositionVisual != null)
CompositionVisual.Children.Remove(ChildCompositionVisual);
CompositionVisual.DrawList = null;
CompositionVisual = null;
}
DetachFromCompositor();
DetachedFromVisualTree?.Invoke(this, e);
e.Root.Renderer.AddDirty(this);

16
src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj

@ -1,12 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<OutputType>exe</OutputType>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<BuildOutputTargetFolder>tools</BuildOutputTargetFolder>
<DefineConstants>$(DefineConstants);BUILDTASK;XAMLX_CECIL_INTERNAL;XAMLX_INTERNAL</DefineConstants>
<CopyLocalLockFileAssemblies Condition="$(TargetFramework) == 'netstandard2.0'">true</CopyLocalLockFileAssemblies>
<NoWarn>$(NoWarn);NU1605;CS8632</NoWarn>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<OutputType>exe</OutputType>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<BuildOutputTargetFolder>tools</BuildOutputTargetFolder>
<DefineConstants>$(DefineConstants);BUILDTASK;XAMLX_CECIL_INTERNAL;XAMLX_INTERNAL</DefineConstants>
<CopyLocalLockFileAssemblies Condition="$(TargetFramework) == 'netstandard2.0'">true</CopyLocalLockFileAssemblies>
<NoWarn>$(NoWarn);NU1605;CS8632</NoWarn>
<DebugType>embedded</DebugType>
<IncludeSymbols>false</IncludeSymbols>
</PropertyGroup>
<!--Disable Net Perf. analyzer for submodule to avoid commit issue -->

4
src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs

@ -807,7 +807,7 @@ namespace Avalonia.Collections
{
if (value < 0)
{
throw new ArgumentOutOfRangeException("PageSize cannot have a negative value.");
throw new ArgumentOutOfRangeException(nameof(value), "PageSize cannot have a negative value.");
}
// if the Refresh is currently deferred, cache the desired PageSize
@ -1954,7 +1954,7 @@ namespace Avalonia.Collections
// for indices larger than the count
if (index >= Count || index < 0)
{
throw new ArgumentOutOfRangeException("index");
throw new ArgumentOutOfRangeException(nameof(index));
}
if (IsGrouping)

159
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@ -7,7 +7,6 @@ using Avalonia.Data;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using Avalonia.Collections;
using Avalonia.Utilities;
using System;
using System.ComponentModel;
using System.Linq;
@ -24,8 +23,6 @@ namespace Avalonia.Controls
{
internal const int DATAGRIDCOLUMN_maximumWidth = 65536;
private const bool DATAGRIDCOLUMN_defaultIsReadOnly = false;
private DataGridLength? _width; // Null by default, null means inherit the Width from the DataGrid
private bool? _isReadOnly;
private double? _maxWidth;
private double? _minWidth;
@ -39,6 +36,7 @@ namespace Avalonia.Controls
private IBinding _clipboardContentBinding;
private ControlTheme _cellTheme;
private Classes _cellStyleClasses;
private bool _setWidthInternalNoCallback;
/// <summary>
/// Initializes a new instance of the <see cref="T:Avalonia.Controls.DataGridColumn" /> class.
@ -214,6 +212,36 @@ namespace Avalonia.Controls
OwningGrid?.OnColumnVisibleStateChanged(this);
NotifyPropertyChanged(change.Property.Name);
}
else if (change.Property == WidthProperty)
{
if (!_settingWidthInternally)
{
InheritsWidth = false;
}
if (_setWidthInternalNoCallback == false)
{
var grid = OwningGrid;
var width = (change as AvaloniaPropertyChangedEventArgs<DataGridLength>).NewValue.Value;
if (grid != null)
{
var oldWidth = (change as AvaloniaPropertyChangedEventArgs<DataGridLength>).OldValue.Value;
if (width.IsStar != oldWidth.IsStar)
{
SetWidthInternalNoCallback(width);
IsInitialDesiredWidthDetermined = false;
grid.OnColumnWidthChanged(this);
}
else
{
Resize(oldWidth, width, false);
}
}
else
{
SetWidthInternalNoCallback(width);
}
}
}
}
@ -549,48 +577,15 @@ namespace Avalonia.Controls
}
}
public static readonly StyledProperty<DataGridLength> WidthProperty = AvaloniaProperty
.Register<DataGridColumn, DataGridLength>(nameof(Width)
, coerce: CoerceWidth
);
public DataGridLength Width
{
get
{
return
_width ??
OwningGrid?.ColumnWidth ??
// We don't have a good choice here because we don't want to make this property nullable, see DevDiv Bugs 196581
DataGridLength.Auto;
}
set
{
if (!_width.HasValue || _width.Value != value)
{
if (!_settingWidthInternally)
{
InheritsWidth = false;
}
if (OwningGrid != null)
{
DataGridLength width = CoerceWidth(value);
if (width.IsStar != Width.IsStar)
{
// If a column has changed either from or to a star value, we want to recalculate all
// star column widths. They are recalculated during Measure based off what the value we set here.
SetWidthInternalNoCallback(width);
IsInitialDesiredWidthDetermined = false;
OwningGrid.OnColumnWidthChanged(this);
}
else
{
// If a column width's value is simply changing, we resize it (to the right only).
Resize(width.Value, width.UnitType, width.DesiredValue, width.DisplayValue, false);
}
}
else
{
SetWidthInternalNoCallback(value);
}
}
}
get => this.GetValue(WidthProperty);
set => SetValue(WidthProperty, value);
}
/// <summary>
@ -812,19 +807,34 @@ namespace Avalonia.Controls
/// on the rest of the star columns. For pixel widths, the desired value is based on the pixel value.
/// For auto widths, the desired value is initialized as the column's minimum width.
/// </summary>
/// <param name="source"></param>
/// <param name="width">The DataGridLength to coerce.</param>
/// <returns>The resultant (coerced) DataGridLength.</returns>
internal DataGridLength CoerceWidth(DataGridLength width)
static DataGridLength CoerceWidth(AvaloniaObject source, DataGridLength width)
{
var target = (DataGridColumn)source;
if (target._setWidthInternalNoCallback)
{
return width;
}
if (!target.IsSet(WidthProperty))
{
return target.OwningGrid?.ColumnWidth ??
DataGridLength.Auto;
}
double desiredValue = width.DesiredValue;
if (double.IsNaN(desiredValue))
{
if (width.IsStar && OwningGrid != null && OwningGrid.ColumnsInternal != null)
if (width.IsStar && target.OwningGrid != null && target.OwningGrid.ColumnsInternal != null)
{
double totalStarValues = 0;
double totalStarDesiredValues = 0;
double totalNonStarDisplayWidths = 0;
foreach (DataGridColumn column in OwningGrid.ColumnsInternal.GetDisplayedColumns(c => c.IsVisible && c != this && !double.IsNaN(c.Width.DesiredValue)))
foreach (DataGridColumn column in target.OwningGrid.ColumnsInternal.GetDisplayedColumns(c => c.IsVisible && c != target && !double.IsNaN(c.Width.DesiredValue)))
{
if (column.Width.IsStar)
{
@ -839,7 +849,7 @@ namespace Avalonia.Controls
if (totalStarValues == 0)
{
// Compute the new star column's desired value based on the available space if there are no other visible star columns
desiredValue = Math.Max(ActualMinWidth, OwningGrid.CellsWidth - totalNonStarDisplayWidths);
desiredValue = Math.Max(target.ActualMinWidth, target.OwningGrid.CellsWidth - totalNonStarDisplayWidths);
}
else
{
@ -853,7 +863,7 @@ namespace Avalonia.Controls
}
else
{
desiredValue = ActualMinWidth;
desiredValue = target.ActualMinWidth;
}
}
@ -862,7 +872,7 @@ namespace Avalonia.Controls
{
displayValue = desiredValue;
}
displayValue = Math.Max(ActualMinWidth, Math.Min(ActualMaxWidth, displayValue));
displayValue = Math.Max(target.ActualMinWidth, Math.Min(target.ActualMaxWidth, displayValue));
return new DataGridLength(width.Value, width.UnitType, desiredValue, displayValue);
}
@ -896,7 +906,7 @@ namespace Avalonia.Controls
};
result[!ContentControl.ContentProperty] = this[!HeaderProperty];
result[!ContentControl.ContentTemplateProperty] = this[!HeaderTemplateProperty];
if (OwningGrid.ColumnHeaderTheme is {} columnTheme)
if (OwningGrid.ColumnHeaderTheme is { } columnTheme)
{
result.SetValue(StyledElement.ThemeProperty, columnTheme, BindingPriority.Template);
}
@ -909,7 +919,7 @@ namespace Avalonia.Controls
/// </summary>
internal void EnsureWidth()
{
SetWidthInternalNoCallback(CoerceWidth(Width));
SetWidthInternalNoCallback(CoerceWidth(this, Width));
}
internal Control GenerateElementInternal(DataGridCell cell, object dataItem)
@ -931,17 +941,17 @@ namespace Avalonia.Controls
/// can only decrease in size by the amount that the columns after it can increase in size.
/// Likewise, the column can only increase in size if other columns can spare the width.
/// </summary>
/// <param name="value">The new Value.</param>
/// <param name="unitType">The new UnitType.</param>
/// <param name="desiredValue">The new DesiredValue.</param>
/// <param name="displayValue">The new DisplayValue.</param>
/// <param name="oldWidth">with before resize.</param>
/// <param name="newWidth">with after resize.</param>
/// <param name="userInitiated">Whether or not this resize was initiated by a user action.</param>
internal void Resize(double value, DataGridLengthUnitType unitType, double desiredValue, double displayValue, bool userInitiated)
// double value, DataGridLengthUnitType unitType, double desiredValue, double displayValue
internal void Resize(DataGridLength oldWidth, DataGridLength newWidth, bool userInitiated)
{
double newValue = value;
double newDesiredValue = desiredValue;
double newDisplayValue = Math.Max(ActualMinWidth, Math.Min(ActualMaxWidth, displayValue));
DataGridLengthUnitType newUnitType = unitType;
double newValue = newWidth.Value;
double newDesiredValue = newWidth.DesiredValue;
double newDisplayValue = Math.Max(ActualMinWidth, Math.Min(ActualMaxWidth, newWidth.DisplayValue));
DataGridLengthUnitType newUnitType = newWidth.UnitType;
int starColumnsCount = 0;
double totalDisplayWidth = 0;
@ -955,11 +965,11 @@ namespace Avalonia.Controls
// If we're using star sizing, we can only resize the column as much as the columns to the
// right will allow (i.e. until they hit their max or min widths).
if (!hasInfiniteAvailableWidth && (starColumnsCount > 0 || (unitType == DataGridLengthUnitType.Star && Width.IsStar && userInitiated)))
if (!hasInfiniteAvailableWidth && (starColumnsCount > 0 || (newUnitType == DataGridLengthUnitType.Star && newWidth.IsStar && userInitiated)))
{
double limitedDisplayValue = Width.DisplayValue;
double limitedDisplayValue = oldWidth.DisplayValue;
double availableIncrease = Math.Max(0, OwningGrid.CellsWidth - totalDisplayWidth);
double desiredChange = newDisplayValue - Width.DisplayValue;
double desiredChange = newDisplayValue - oldWidth.DisplayValue;
if (desiredChange > availableIncrease)
{
// The desired change is greater than the amount of available space,
@ -979,7 +989,7 @@ namespace Avalonia.Controls
// The desired change is negative, so we need to increase the widths of columns to the right.
limitedDisplayValue += desiredChange + OwningGrid.IncreaseColumnWidths(DisplayIndex + 1, -desiredChange, userInitiated);
}
if (ActualCanUserResize || (Width.IsStar && !userInitiated))
if (ActualCanUserResize || (oldWidth.IsStar && !userInitiated))
{
newDisplayValue = limitedDisplayValue;
}
@ -1002,9 +1012,10 @@ namespace Avalonia.Controls
}
}
DataGridLength oldWidth = Width;
SetWidthInternalNoCallback(new DataGridLength(Math.Min(double.MaxValue, newValue), newUnitType, newDesiredValue, newDisplayValue));
if (Width != oldWidth)
newDisplayValue = Math.Min(double.MaxValue, newValue);
newWidth = new DataGridLength(newDisplayValue, newUnitType, newDesiredValue, newDisplayValue);
SetWidthInternalNoCallback(newWidth);
if (newWidth != oldWidth)
{
OwningGrid.OnColumnWidthChanged(this);
}
@ -1052,7 +1063,17 @@ namespace Avalonia.Controls
/// <param name="width">The new Width.</param>
internal void SetWidthInternalNoCallback(DataGridLength width)
{
_width = width;
var originalValue = _setWidthInternalNoCallback;
_setWidthInternalNoCallback = true;
try
{
Width = width;
}
finally
{
_setWidthInternalNoCallback = originalValue;
}
}
/// <summary>
@ -1122,7 +1143,7 @@ namespace Avalonia.Controls
&& OwningGrid.DataConnection != null
&& OwningGrid.DataConnection.SortDescriptions != null)
{
if(CustomSortComparer != null)
if (CustomSortComparer != null)
{
return
OwningGrid.DataConnection.SortDescriptions

4
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@ -785,7 +785,9 @@ namespace Avalonia.Controls
double desiredWidth = _originalWidth + mouseDelta;
desiredWidth = Math.Max(_dragColumn.ActualMinWidth, Math.Min(_dragColumn.ActualMaxWidth, desiredWidth));
_dragColumn.Resize(_dragColumn.Width.Value, _dragColumn.Width.UnitType, _dragColumn.Width.DesiredValue, desiredWidth, true);
_dragColumn.Resize(_dragColumn.Width,
new(_dragColumn.Width.Value, _dragColumn.Width.UnitType, _dragColumn.Width.DesiredValue, desiredWidth),
true);
OwningGrid.UpdateHorizontalOffset(_originalHorizontalOffset);

17
src/Avalonia.Controls.DataGrid/DataGridColumns.cs

@ -91,7 +91,10 @@ namespace Avalonia.Controls
// this column is newly added, we'll just set its display value directly.
if (UsesStarSizing && column.IsInitialDesiredWidthDetermined)
{
column.Resize(column.Width.Value, column.Width.UnitType, desiredWidth, desiredWidth, false);
var oldWidth = column.Width;
column.Resize(oldWidth,
new(column.Width.Value, column.Width.UnitType, desiredWidth, desiredWidth),
false);
}
else
{
@ -142,7 +145,7 @@ namespace Avalonia.Controls
{
Debug.Assert(dataGridColumn != null);
if (dataGridColumn is DataGridBoundColumn dataGridBoundColumn &&
if (dataGridColumn is DataGridBoundColumn dataGridBoundColumn &&
dataGridBoundColumn.Binding is BindingBase binding)
{
var path = (binding as Binding)?.Path ?? (binding as CompiledBindingExtension)?.Path.ToString();
@ -359,6 +362,7 @@ namespace Avalonia.Controls
if (column.IsVisible && oldValue != column.ActualMaxWidth)
{
DataGridLength oldWitdh = new(oldValue, column.Width.UnitType, column.Width.DesiredValue, column.Width.DesiredValue);
if (column.ActualMaxWidth < column.Width.DisplayValue)
{
// If the maximum width has caused the column to decrease in size, try first to resize
@ -371,7 +375,9 @@ namespace Avalonia.Controls
{
// If the column was previously limited by its maximum value but has more room now,
// attempt to resize the column to its desired width.
column.Resize(column.Width.Value, column.Width.UnitType, column.Width.DesiredValue, column.Width.DesiredValue, false);
column.Resize(oldWitdh,
new (column.Width.Value, column.Width.UnitType, column.Width.DesiredValue, column.Width.DesiredValue),
false);
}
OnColumnWidthChanged(column);
}
@ -388,6 +394,7 @@ namespace Avalonia.Controls
if (column.IsVisible && oldValue != column.ActualMinWidth)
{
DataGridLength oldWitdh = new(oldValue, column.Width.UnitType, column.Width.DesiredValue, column.Width.DesiredValue);
if (column.ActualMinWidth > column.Width.DisplayValue)
{
// If the minimum width has caused the column to increase in size, try first to resize
@ -400,7 +407,9 @@ namespace Avalonia.Controls
{
// If the column was previously limited by its minimum value but but can be smaller now,
// attempt to resize the column to its desired width.
column.Resize(column.Width.Value, column.Width.UnitType, column.Width.DesiredValue, column.Width.DesiredValue, false);
column.Resize(oldWitdh,
new(column.Width.Value, column.Width.UnitType, column.Width.DesiredValue, column.Width.DesiredValue),
false);
}
OnColumnWidthChanged(column);
}

1
src/Avalonia.Controls.DataGrid/DataGridLength.cs

@ -7,7 +7,6 @@ using Avalonia.Utilities;
using System;
using System.ComponentModel;
using System.Globalization;
using Avalonia.Controls.Utils;
namespace Avalonia.Controls
{

5
src/Avalonia.Controls.DataGrid/DataGridRows.cs

@ -2969,11 +2969,8 @@ namespace Avalonia.Controls
var detailsContent = RowDetailsTemplate.Build(dataItem);
if (detailsContent != null)
{
detailsContent.DataContext = dataItem;
_rowsPresenter.Children.Add(detailsContent);
if (dataItem != null)
{
detailsContent.DataContext = dataItem;
}
detailsContent.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
RowDetailsHeightEstimate = detailsContent.DesiredSize.Height;
_rowsPresenter.Children.Remove(detailsContent);

4
src/Avalonia.Controls/Automation/Peers/ScrollViewerAutomationPeer.cs

@ -146,12 +146,12 @@ namespace Avalonia.Automation.Peers
if (scrollHorizontally && (horizontalPercent < 0.0) || (horizontalPercent > 100.0))
{
throw new ArgumentOutOfRangeException("horizontalPercent");
throw new ArgumentOutOfRangeException(nameof(horizontalPercent));
}
if (scrollVertically && (verticalPercent < 0.0) || (verticalPercent > 100.0))
{
throw new ArgumentOutOfRangeException("verticalPercent");
throw new ArgumentOutOfRangeException(nameof(verticalPercent));
}
var offset = Owner.Offset;

10
src/Avalonia.Controls/ComboBox.cs

@ -270,6 +270,7 @@ namespace Avalonia.Controls
{
if (_popup?.IsInsidePopup(source) == true)
{
e.Handled = true;
return;
}
}
@ -516,5 +517,14 @@ namespace Avalonia.Controls
}
}
}
/// <summary>
/// Clears the selection
/// </summary>
public void Clear()
{
SelectedItem = null;
SelectedIndex = -1;
}
}
}

4
src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs

@ -165,7 +165,7 @@ namespace Avalonia.Controls.Primitives
set
{
if (value > MaximumValue || value < MinimumValue)
throw new ArgumentOutOfRangeException("SelectedValue");
throw new ArgumentOutOfRangeException(nameof(value));
var sel = CoerceSelected(value);
_selectedValue = sel;
@ -195,7 +195,7 @@ namespace Avalonia.Controls.Primitives
set
{
if (value <= 0 || value > _range)
throw new ArgumentOutOfRangeException("Increment");
throw new ArgumentOutOfRangeException(nameof(value));
_increment = value;
UpdateHelperInfo();
var sel = CoerceSelected(SelectedValue);

4
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@ -1113,11 +1113,11 @@ namespace Avalonia.Controls
}
if (value < Minimum)
{
throw new ArgumentOutOfRangeException(nameof(value), string.Format("Value must be greater than Minimum value of {0}", Minimum));
throw new ArgumentOutOfRangeException(nameof(value), $"Value must be greater than Minimum value of {Minimum}");
}
else if (value > Maximum)
{
throw new ArgumentOutOfRangeException(nameof(value), string.Format("Value must be less than Maximum value of {0}", Maximum));
throw new ArgumentOutOfRangeException(nameof(value), $"Value must be less than Maximum value of {Maximum}");
}
}

19
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -473,18 +473,31 @@ namespace Avalonia.Controls.Presenters
}
Viewport = finalSize;
Extent = ComputeExtent(finalSize);
_isAnchorElementDirty = true;
return finalSize;
}
private Size ComputeExtent(Size viewportSize)
{
var childMargin = Child!.Margin;
if (Child.UseLayoutRounding)
{
var scale = LayoutHelper.GetLayoutScale(Child);
childMargin = LayoutHelper.RoundLayoutThickness(childMargin, scale, scale);
}
Extent = Child!.Bounds.Size.Inflate(childMargin);
_isAnchorElementDirty = true;
var extent = Child!.Bounds.Size.Inflate(childMargin);
return finalSize;
if (MathUtilities.AreClose(extent.Width, viewportSize.Width, LayoutHelper.LayoutEpsilon))
extent = extent.WithWidth(viewportSize.Width);
if (MathUtilities.AreClose(extent.Height, viewportSize.Height, LayoutHelper.LayoutEpsilon))
extent = extent.WithHeight(viewportSize.Height);
return extent;
}
private void OnScrollGesture(object? sender, ScrollGestureEventArgs e)

4
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -1395,10 +1395,8 @@ namespace Avalonia.Controls.Primitives
public object Evaluate(object? dataContext)
{
dataContext = dataContext ?? throw new ArgumentNullException(nameof(dataContext));
// Only update the DataContext if necessary
if (!dataContext.Equals(DataContext))
if (!Equals(dataContext, DataContext))
DataContext = dataContext;
return GetValue(ValueProperty);

10
src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs

@ -98,14 +98,14 @@ namespace Avalonia.Controls.PullToRefresh
if (_scrollViewer.Content == null)
{
throw new ArgumentException(nameof(adaptee), "Adaptee's content property cannot be null.");
throw new ArgumentException("Adaptee's content property cannot be null.", nameof(adaptee));
}
var content = adaptee.Content as Visual;
if (content == null)
{
throw new ArgumentException(nameof(adaptee), "Adaptee's content property must be a Visual");
throw new ArgumentException("Adaptee's content property must be a Visual", nameof(adaptee));
}
if (content.GetVisualParent() == null)
@ -118,7 +118,7 @@ namespace Avalonia.Controls.PullToRefresh
if (content.Parent is not InputElement)
{
throw new ArgumentException(nameof(adaptee), "Adaptee's content's parent must be a InputElement");
throw new ArgumentException("Adaptee's content's parent must be a InputElement", nameof(adaptee));
}
}
@ -194,12 +194,12 @@ namespace Avalonia.Controls.PullToRefresh
var content = _scrollViewer?.Content as Visual;
if (content == null)
{
throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content property must be a Visual");
throw new ArgumentException("Adaptee's content property must be a Visual", nameof(_scrollViewer));
}
if (content.Parent is not InputElement parent)
{
throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content parent must be an InputElement");
throw new ArgumentException("Adaptee's content parent must be an InputElement", nameof(_scrollViewer));
}
MakeInteractionSource(parent);

22
src/Avalonia.Controls/Utils/StringUtils.cs

@ -76,7 +76,7 @@ namespace Avalonia.Controls.Utils
}
var codepoint = new Codepoint(text[index]);
if (!codepoint.IsWhiteSpace)
{
return false;
@ -85,12 +85,20 @@ namespace Avalonia.Controls.Utils
// preceeded by lwsp.
if (index > 0)
{
var nextCodePoint = new Codepoint(text[index + 1]);
if (index + 1 < text.Length)
{
var nextCodePoint = new Codepoint(text[index + 1]);
if (nextCodePoint.IsBreakChar)
if (nextCodePoint.IsBreakChar)
{
return true;
}
}
else
{
return true;
}
}
switch (codepoint.GeneralCategory)
@ -125,7 +133,7 @@ namespace Avalonia.Controls.Utils
}
cursor = Math.Min(cursor, text.Length);
int begin;
int i;
int cr;
@ -181,7 +189,7 @@ namespace Avalonia.Controls.Utils
{
return cursor;
}
if (cr < text.Length && text[cr] == '\r' && cr + 1 < text.Length && text[cr + 1] == '\n')
{
lf = cr + 1;
@ -214,9 +222,9 @@ namespace Avalonia.Controls.Utils
{
return i;
}
var cc = GetCharClass(text[i]);
// skip over the word, punctuation, or run of whitespace
while (i < cr && GetCharClass(text[i]) == cc)
{

2
src/Avalonia.Controls/Utils/VirtualizingSnapPointsList.cs

@ -39,7 +39,7 @@ namespace Avalonia.Controls.Utils
get
{
if(index < 0 || index >= Count)
throw new ArgumentOutOfRangeException("index");
throw new ArgumentOutOfRangeException(nameof(index));
index += _start;

7
src/Avalonia.Controls/Window.cs

@ -169,6 +169,7 @@ namespace Avalonia.Controls
private readonly Size _maxPlatformClientSize;
private bool _shown;
private bool _showingAsDialog;
private bool _wasShownBefore;
/// <summary>
/// Initializes static members of the <see cref="Window"/> class.
@ -718,6 +719,7 @@ namespace Avalonia.Controls
StartRendering();
PlatformImpl?.Show(ShowActivated, false);
OnOpened(EventArgs.Empty);
_wasShownBefore = true;
}
}
@ -871,6 +873,11 @@ namespace Avalonia.Controls
private void SetWindowStartupLocation(Window? owner = null)
{
if (_wasShownBefore == true)
{
return;
}
var startupLocation = WindowStartupLocation;
if (startupLocation == WindowStartupLocation.CenterOwner &&

10
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -3,9 +3,7 @@ using System.Runtime.InteropServices;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.MicroCom;
using Avalonia.Native.Interop;
using Avalonia.OpenGL;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
@ -163,6 +161,14 @@ namespace Avalonia.Native
Compositor = new Compositor(_platformGraphics, true);
AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
}
private void OnProcessExit(object? sender, EventArgs e)
{
AppDomain.CurrentDomain.ProcessExit -= OnProcessExit;
_factory.Dispose();
}
public ITrayIconImpl CreateTrayIcon()

2
src/Avalonia.Native/NativePlatformSettings.cs

@ -23,7 +23,7 @@ internal class NativePlatformSettings : DefaultPlatformSettings
AvnPlatformThemeVariant.Dark => (PlatformThemeVariant.Dark, ColorContrastPreference.NoPreference),
AvnPlatformThemeVariant.Light => (PlatformThemeVariant.Light, ColorContrastPreference.NoPreference),
AvnPlatformThemeVariant.HighContrastDark => (PlatformThemeVariant.Dark, ColorContrastPreference.High),
AvnPlatformThemeVariant.HighContrastLight => (PlatformThemeVariant.Dark, ColorContrastPreference.High),
AvnPlatformThemeVariant.HighContrastLight => (PlatformThemeVariant.Light, ColorContrastPreference.High),
_ => throw new ArgumentOutOfRangeException()
};
var color = _platformSettings.AccentColor;

8
src/Avalonia.Remote.Protocol/MetsysBson.cs

@ -752,7 +752,7 @@ namespace Metsys.Bson
if (memberExpression.Expression.NodeType != ExpressionType.Parameter && memberExpression.Expression.NodeType != ExpressionType.Convert)
{
throw new ArgumentException(string.Format("Expression '{0}' must resolve to top-level member.", lambdaExpression), nameof(lambdaExpression));
throw new ArgumentException($"Expression '{lambdaExpression}' must resolve to top-level member.", nameof(lambdaExpression));
}
return memberExpression.Member.Name;
default:
@ -942,7 +942,7 @@ namespace Metsys.Bson
return new ListWrapper();
}
}
throw new BsonException(string.Format("Collection of type {0} cannot be deserialized", type.FullName));
throw new BsonException($"Collection of type {type.FullName} cannot be deserialized");
}
public abstract void Add(object value);
@ -1514,7 +1514,7 @@ namespace Metsys.Bson.Configuration
result = Visit((MemberExpression)expression.Left);
}
var index = Expression.Lambda(expression.Right).Compile().DynamicInvoke();
return result + string.Format("[{0}]", index);
return result + $"[{index}]";
}
private string Visit(MemberExpression expression)
@ -1540,7 +1540,7 @@ namespace Metsys.Bson.Configuration
if (expression.Method.Name == "get_Item" && expression.Arguments.Count == 1)
{
var index = Expression.Lambda(expression.Arguments[0]).Compile().DynamicInvoke();
name += string.Format("[{0}]", index);
name += $"[{index}]";
}
return name;
}

50
src/Avalonia.X11/X11Clipboard.cs

@ -14,6 +14,7 @@ namespace Avalonia.X11
private readonly X11Info _x11;
private IDataObject _storedDataObject;
private IntPtr _handle;
private TaskCompletionSource<bool> _storeAtomTcs;
private TaskCompletionSource<IntPtr[]> _requestedFormatsTcs;
private TaskCompletionSource<object> _requestedDataTcs;
private readonly IntPtr[] _textAtoms;
@ -52,6 +53,12 @@ namespace Avalonia.X11
private unsafe void OnEvent(ref XEvent ev)
{
if (ev.type == XEventName.SelectionClear)
{
_storeAtomTcs?.TrySetResult(true);
return;
}
if (ev.type == XEventName.SelectionRequest)
{
var sel = ev.SelectionRequestEvent;
@ -82,18 +89,9 @@ namespace Avalonia.X11
Encoding textEnc;
if (target == _x11.Atoms.TARGETS)
{
var atoms = new HashSet<IntPtr> { _x11.Atoms.TARGETS, _x11.Atoms.MULTIPLE };
foreach (var fmt in _storedDataObject.GetDataFormats())
{
if (fmt == DataFormats.Text)
foreach (var ta in _textAtoms)
atoms.Add(ta);
else
atoms.Add(_x11.Atoms.GetAtom(fmt));
}
var atoms = ConvertDataObject(_storedDataObject);
XChangeProperty(_x11.Display, window, property,
_x11.Atoms.XA_ATOM, 32, PropertyMode.Replace, atoms.ToArray(), atoms.Count);
_x11.Atoms.XA_ATOM, 32, PropertyMode.Replace, atoms, atoms.Length);
return property;
}
else if(target == _x11.Atoms.SAVE_TARGETS && _x11.Atoms.SAVE_TARGETS != IntPtr.Zero)
@ -252,20 +250,41 @@ namespace Avalonia.X11
return (string)await SendDataRequest(target);
}
private void StoreAtomsInClipboardManager(IntPtr[] atoms)
private IntPtr[] ConvertDataObject(IDataObject data)
{
var atoms = new HashSet<IntPtr> { _x11.Atoms.TARGETS, _x11.Atoms.MULTIPLE };
foreach (var fmt in data.GetDataFormats())
{
if (fmt == DataFormats.Text)
foreach (var ta in _textAtoms)
atoms.Add(ta);
else
atoms.Add(_x11.Atoms.GetAtom(fmt));
}
return atoms.ToArray();
}
private Task StoreAtomsInClipboardManager(IDataObject data)
{
if (_x11.Atoms.CLIPBOARD_MANAGER != IntPtr.Zero && _x11.Atoms.SAVE_TARGETS != IntPtr.Zero)
{
var clipboardManager = XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD_MANAGER);
if (clipboardManager != IntPtr.Zero)
{
{
if (_storeAtomTcs == null || _storeAtomTcs.Task.IsCompleted)
_storeAtomTcs = new TaskCompletionSource<bool>();
var atoms = ConvertDataObject(data);
XChangeProperty(_x11.Display, _handle, _avaloniaSaveTargetsAtom, _x11.Atoms.XA_ATOM, 32,
PropertyMode.Replace,
atoms, atoms.Length);
XConvertSelection(_x11.Display, _x11.Atoms.CLIPBOARD_MANAGER, _x11.Atoms.SAVE_TARGETS,
_avaloniaSaveTargetsAtom, _handle, IntPtr.Zero);
return _storeAtomTcs.Task;
}
}
return Task.CompletedTask;
}
public Task SetTextAsync(string text)
@ -283,9 +302,8 @@ namespace Avalonia.X11
public Task SetDataObjectAsync(IDataObject data)
{
_storedDataObject = data;
XSetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD, _handle, IntPtr.Zero);
StoreAtomsInClipboardManager(_textAtoms);
return Task.CompletedTask;
XSetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD, _handle, IntPtr.Zero);
return StoreAtomsInClipboardManager(data);
}
public async Task<string[]> GetFormatsAsync()

20
src/Avalonia.X11/X11Info.cs

@ -34,7 +34,10 @@ namespace Avalonia.X11
public bool HasXim { get; set; }
public bool HasXSync { get; set; }
public IntPtr DefaultFontSet { get; set; }
[DllImport("libc")]
private static extern void setlocale(int type, string s);
public unsafe X11Info(IntPtr display, IntPtr deferredDisplay, bool useXim)
{
Display = display;
@ -48,7 +51,10 @@ namespace Avalonia.X11
DefaultFontSet = XCreateFontSet(Display, "-*-*-*-*-*-*-*-*-*-*-*-*-*-*",
out var _, out var _, IntPtr.Zero);
// We have problems with text input otherwise
setlocale(0, "");
if (useXim)
{
XSetLocaleModifiers("");
@ -59,7 +65,15 @@ namespace Avalonia.X11
if (Xim == IntPtr.Zero)
{
XSetLocaleModifiers("@im=none");
if (XSetLocaleModifiers("@im=none") == IntPtr.Zero)
{
setlocale(0, "en_US.UTF-8");
if (XSetLocaleModifiers("@im=none") == IntPtr.Zero)
{
setlocale(0, "C.UTF-8");
XSetLocaleModifiers("@im=none");
}
}
Xim = XOpenIM(display, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
}

10
src/Avalonia.X11/X11Platform.cs

@ -36,12 +36,11 @@ namespace Avalonia.X11
public IntPtr OrphanedWindow { get; private set; }
public X11Globals Globals { get; private set; }
public ManualRawEventGrouperDispatchQueue EventGrouperDispatchQueue { get; } = new();
[DllImport("libc")]
private static extern void setlocale(int type, string s);
public void Initialize(X11PlatformOptions options)
{
Options = options;
bool useXim = false;
if (EnableIme(options))
{
@ -50,9 +49,6 @@ namespace Avalonia.X11
useXim = true;
}
// We have problems with text input otherwise
setlocale(0, "");
XInitThreads();
Display = XOpenDisplay(IntPtr.Zero);
if (Display == IntPtr.Zero)
@ -64,7 +60,7 @@ namespace Avalonia.X11
OrphanedWindow = XCreateSimpleWindow(Display, XDefaultRootWindow(Display), 0, 0, 1, 1, 0, IntPtr.Zero,
IntPtr.Zero);
XError.Init();
Info = new X11Info(Display, DeferredDisplay, useXim);
Globals = new X11Globals(this);
//TODO: log

5
src/Avalonia.X11/X11Structs.cs

@ -1109,7 +1109,7 @@ namespace Avalonia.X11 {
public override string ToString ()
{
return string.Format("MotifWmHints <flags={0}, functions={1}, decorations={2}, input_mode={3}, status={4}", (MotifFlags) flags.ToInt32 (), (MotifFunctions) functions.ToInt32 (), (MotifDecorations) decorations.ToInt32 (), (MotifInputMode) input_mode.ToInt32 (), status.ToInt32 ());
return $"MotifWmHints <flags={(MotifFlags)flags.ToInt32()}, functions={(MotifFunctions)functions.ToInt32()}, decorations={(MotifDecorations)decorations.ToInt32()}, input_mode={(MotifInputMode)input_mode.ToInt32()}, status={status.ToInt32()}";
}
}
@ -1707,8 +1707,7 @@ namespace Avalonia.X11 {
public override string ToString ()
{
return string.Format ("XCursorImage (version: {0}, size: {1}, width: {2}, height: {3}, xhot: {4}, yhot: {5}, delay: {6}, pixels: {7}",
version, size, width, height, xhot, yhot, delay, pixels);
return $"XCursorImage (version: {version}, size: {size}, width: {width}, height: {height}, xhot: {xhot}, yhot: {yhot}, delay: {delay}, pixels: {pixels}";
}
} ;

15
src/Avalonia.X11/X11Window.Ime.cs

@ -133,14 +133,21 @@ namespace Avalonia.X11
{
if (ImeBuffer == IntPtr.Zero)
ImeBuffer = Marshal.AllocHGlobal(ImeBufferSize);
var len = Xutf8LookupString(_xic, ref ev, ImeBuffer.ToPointer(), ImeBufferSize,
out _, out var istatus);
var status = (XLookupStatus)istatus;
IntPtr istatus;
int len;
if(_xic != IntPtr.Zero)
len = Xutf8LookupString(_xic, ref ev, ImeBuffer.ToPointer(),
ImeBufferSize, out _, out istatus);
else
len = XLookupString(ref ev, ImeBuffer.ToPointer(), ImeBufferSize,
out _, out istatus);
if (len == 0)
return null;
var status = (XLookupStatus)istatus;
string text;
if (status == XLookupStatus.XBufferOverflow)
return null;

2
src/Avalonia.X11/XLib.cs

@ -468,7 +468,7 @@ namespace Avalonia.X11
public static extern unsafe int XLookupString(ref XEvent xevent, void* buffer, int num_bytes, out IntPtr keysym, out IntPtr status);
[DllImport (libX11)]
public static extern unsafe int Xutf8LookupString(IntPtr xic, ref XEvent xevent, void* buffer, int num_bytes, out IntPtr keysym, out UIntPtr status);
public static extern unsafe int Xutf8LookupString(IntPtr xic, ref XEvent xevent, void* buffer, int num_bytes, out IntPtr keysym, out IntPtr status);
[DllImport (libX11)]
public static extern unsafe int Xutf8LookupString(IntPtr xic, XEvent* xevent, void* buffer, int num_bytes, out IntPtr keysym, out IntPtr status);

12
src/Browser/Avalonia.Browser/webapp/package-lock.json

@ -3296,9 +3296,9 @@
}
},
"node_modules/word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
@ -5560,9 +5560,9 @@
}
},
"word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"dev": true
},
"wrappy": {

3
src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs

@ -33,8 +33,7 @@ namespace Avalonia.Win32.Automation
return null;
var p = WindowImpl.PointToClient(new PixelPoint((int)x, (int)y));
var peer = (WindowBaseAutomationPeer)Peer;
var found = InvokeSync(() => peer.GetPeerFromPoint(p));
var found = InvokeSync(() => Peer.GetPeerFromPoint(p));
var result = GetOrCreate(found) as IRawElementProviderFragment;
return result;
}

3
src/Windows/Avalonia.Win32/WinRT/Composition/D2DEffects.cs

@ -28,6 +28,9 @@ namespace Avalonia.Win32.WinRT.Composition
public static readonly Guid CLSID_D2D1Border =
new Guid(0x2A2D49C0, 0x4ACF, 0x43C7, 0x8C, 0x6A, 0x7C, 0x4A, 0x27, 0x87, 0x4D, 0x27);
public static readonly Guid CLSID_D2D1Opacity =
new Guid("811d79a4-de28-4454-8094-c64685f8bd4c");
public static readonly Guid CLSID_D2D1Brightness =
new Guid(0x8CEA8D1E, 0x77B0, 0x4986, 0xB3, 0xB9, 0x2F, 0x0C, 0x0E, 0xAE, 0x78, 0x87);

104
src/Windows/Avalonia.Win32/WinRT/Composition/WinUIEffectBase.cs

@ -52,6 +52,110 @@ namespace Avalonia.Win32.WinRT.Composition
_sources = null;
}
}
class BorderEffect : WinUIEffectBase
{
private readonly int _x;
private readonly int _y;
public override Guid EffectId => D2DEffects.CLSID_D2D1Border;
public override uint PropertyCount => 2;
public BorderEffect(int x, int y, params IGraphicsEffectSource[] _sources):base(_sources)
{
_x = x;
_y = y;
}
public override IPropertyValue? GetProperty(uint index)
{
if (index == 0)
return new WinRTPropertyValue((uint)_x);
if (index == 1)
return new WinRTPropertyValue((uint)_y);
return null;
}
}
class BlendEffect : WinUIEffectBase
{
private readonly int _mode;
public BlendEffect(int mode, params IGraphicsEffectSource[] _sources) : base(_sources)
{
_mode = mode;
}
public override Guid EffectId => D2DEffects.CLSID_D2D1Blend;
public override uint PropertyCount => 1;
public override IPropertyValue? GetProperty(uint index)
{
if (index == 0)
return new WinRTPropertyValue((uint)_mode);
return null;
}
}
class CompositeStepEffect : WinUIEffectBase
{
private readonly float _mode;
public CompositeStepEffect(int mode, params IGraphicsEffectSource[] _sources) : base(_sources)
{
_mode = mode;
}
public override Guid EffectId => D2DEffects.CLSID_D2D1Composite;
public override uint PropertyCount => 1;
public override IPropertyValue? GetProperty(uint index)
{
if (index == 0)
return new WinRTPropertyValue((uint)_mode);
return null;
}
}
class OpacityEffect : WinUIEffectBase
{
private readonly float _opacity;
public OpacityEffect(float opacity, params IGraphicsEffectSource[] _sources) : base(_sources)
{
_opacity = opacity;
}
public override Guid EffectId => D2DEffects.CLSID_D2D1Opacity;
public override uint PropertyCount => 1;
public override IPropertyValue? GetProperty(uint index)
{
if (index == 0)
return new WinRTPropertyValue(_opacity);
return null;
}
}
class ColorSourceEffect : WinUIEffectBase
{
private readonly float[] _color;
public ColorSourceEffect(float[] color)
{
_color = color;
}
public override Guid EffectId => D2DEffects.CLSID_D2D1Flood;
public override uint PropertyCount => 1;
public override IPropertyValue? GetProperty(uint index)
{
if (index == 0)
return new WinRTPropertyValue(_color);
return null;
}
}
internal class WinUIGaussianBlurEffect : WinUIEffectBase
{

30
src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindow.cs

@ -12,7 +12,8 @@ internal class WinUiCompositedWindow : IDisposable
public EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo WindowInfo { get; }
private readonly WinUiCompositionShared _shared;
private readonly ICompositionRoundedRectangleGeometry? _compositionRoundedRectangleGeometry;
private readonly IVisual? _mica;
private readonly IVisual? _micaLight;
private readonly IVisual? _micaDark;
private readonly IVisual _blur;
private readonly IVisual _visual;
private PixelSize _size;
@ -25,7 +26,8 @@ internal class WinUiCompositedWindow : IDisposable
{
_compositionRoundedRectangleGeometry?.Dispose();
_blur.Dispose();
_mica?.Dispose();
_micaLight?.Dispose();
_micaDark?.Dispose();
_visual.Dispose();
_surfaceBrush.Dispose();
_target.Dispose();
@ -50,14 +52,20 @@ internal class WinUiCompositedWindow : IDisposable
_target.SetRoot(containerVisual);
_blur = WinUiCompositionUtils.CreateBlurVisual(shared.Compositor, shared.BlurBrush);
if (shared.MicaBrush != null)
if (shared.MicaBrushLight != null)
{
_mica = WinUiCompositionUtils.CreateBlurVisual(shared.Compositor, shared.MicaBrush);
containerChildren.InsertAtTop(_mica);
_micaLight = WinUiCompositionUtils.CreateBlurVisual(shared.Compositor, shared.MicaBrushLight);
containerChildren.InsertAtTop(_micaLight);
}
if (shared.MicaBrushDark != null)
{
_micaDark = WinUiCompositionUtils.CreateBlurVisual(shared.Compositor, shared.MicaBrushDark);
containerChildren.InsertAtTop(_micaDark);
}
_compositionRoundedRectangleGeometry =
WinUiCompositionUtils.ClipVisual(shared.Compositor, backdropCornerRadius, _blur, _mica);
WinUiCompositionUtils.ClipVisual(shared.Compositor, backdropCornerRadius, _blur, _micaLight, _micaDark);
containerChildren.InsertAtTop(_blur);
using var spriteVisual = shared.Compositor.CreateSpriteVisual();
@ -68,9 +76,6 @@ internal class WinUiCompositedWindow : IDisposable
using var compositionBrush = _surfaceBrush.QueryInterface<ICompositionBrush>();
spriteVisual.SetBrush(compositionBrush);
_target.SetRoot(containerVisual);
}
public void SetSurface(ICompositionSurface surface) => _surfaceBrush.SetSurface(surface);
@ -79,12 +84,13 @@ internal class WinUiCompositedWindow : IDisposable
{
lock (_shared.SyncRoot)
{
_blur.SetIsVisible(blurEffect == BlurEffect.Acrylic
|| blurEffect == BlurEffect.Mica && _mica == null ?
|| (blurEffect == BlurEffect.MicaLight && _micaLight == null) ||
(blurEffect == BlurEffect.MicaDark && _micaDark == null) ?
1 :
0);
_mica?.SetIsVisible(blurEffect == BlurEffect.Mica ? 1 : 0);
_micaLight?.SetIsVisible(blurEffect == BlurEffect.MicaLight ? 1 : 0);
_micaDark?.SetIsVisible(blurEffect == BlurEffect.MicaDark ? 1 : 0);
}
}

9
src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionShared.cs

@ -9,7 +9,8 @@ internal class WinUiCompositionShared : IDisposable
public ICompositor5 Compositor5 { get; }
public ICompositorDesktopInterop DesktopInterop { get; }
public ICompositionBrush BlurBrush { get; }
public ICompositionBrush? MicaBrush { get; }
public ICompositionBrush? MicaBrushLight { get; }
public ICompositionBrush? MicaBrushDark { get; }
public object SyncRoot { get; } = new();
public static readonly Version MinWinCompositionVersion = new(10, 0, 17134);
@ -21,14 +22,16 @@ internal class WinUiCompositionShared : IDisposable
Compositor = compositor.CloneReference();
Compositor5 = compositor.QueryInterface<ICompositor5>();
BlurBrush = WinUiCompositionUtils.CreateAcrylicBlurBackdropBrush(compositor);
MicaBrush = WinUiCompositionUtils.CreateMicaBackdropBrush(compositor);
MicaBrushLight = WinUiCompositionUtils.CreateMicaBackdropBrush(compositor, 242, 0.6f);
MicaBrushDark = WinUiCompositionUtils.CreateMicaBackdropBrush(compositor, 32, 0.8f);
DesktopInterop = compositor.QueryInterface<ICompositorDesktopInterop>();
}
public void Dispose()
{
BlurBrush.Dispose();
MicaBrush?.Dispose();
MicaBrushLight?.Dispose();
MicaBrushDark?.Dispose();
DesktopInterop.Dispose();
Compositor.Dispose();
Compositor5.Dispose();

72
src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositionUtils.cs

@ -1,3 +1,4 @@
using System;
using System.Numerics;
using MicroCom.Runtime;
@ -5,16 +6,72 @@ namespace Avalonia.Win32.WinRT.Composition;
internal static class WinUiCompositionUtils
{
public static ICompositionBrush? CreateMicaBackdropBrush(ICompositor compositor)
public static ICompositionBrush? CreateMicaBackdropBrush(ICompositor compositor, float color, float opacity)
{
if (Win32Platform.WindowsVersion.Build < 22000)
return null;
using var backDropParameterFactory =
NativeWinRTMethods.CreateActivationFactory<ICompositionEffectSourceParameterFactory>(
"Windows.UI.Composition.CompositionEffectSourceParameter");
var tint = new[] { color / 255f, color / 255f, color / 255f, 255f / 255f };
using var tintColorEffect = new ColorSourceEffect(tint);
using var tintOpacityEffect = new OpacityEffect(1.0f, tintColorEffect);
using var tintOpacityEffectFactory = compositor.CreateEffectFactory(tintOpacityEffect);
using var tintOpacityEffectBrushEffect = tintOpacityEffectFactory.CreateBrush();
using var tintOpacityEffectBrush = tintOpacityEffectBrushEffect.QueryInterface<ICompositionBrush>();
using var luminosityColorEffect = new ColorSourceEffect(tint);
using var luminosityOpacityEffect = new OpacityEffect(opacity, luminosityColorEffect);
using var luminosityOpacityEffectFactory = compositor.CreateEffectFactory(luminosityOpacityEffect);
using var luminosityOpacityEffectBrushEffect = luminosityOpacityEffectFactory.CreateBrush();
using var luminosityOpacityEffectBrush =
luminosityOpacityEffectBrushEffect.QueryInterface<ICompositionBrush>();
using var compositorWithBlurredWallpaperBackdropBrush =
compositor.QueryInterface<ICompositorWithBlurredWallpaperBackdropBrush>();
using var blurredWallpaperBackdropBrush =
compositorWithBlurredWallpaperBackdropBrush?.TryCreateBlurredWallpaperBackdropBrush();
return blurredWallpaperBackdropBrush?.QueryInterface<ICompositionBrush>();
using var micaBackdropBrush = blurredWallpaperBackdropBrush?.QueryInterface<ICompositionBrush>();
using var backgroundParameterAsSource =
GetParameterSource("Background", backDropParameterFactory, out var backgroundHandle);
using var foregroundParameterAsSource =
GetParameterSource("Foreground", backDropParameterFactory, out var foregroundHandle);
using var luminosityBlendEffect =
new BlendEffect(23, backgroundParameterAsSource, foregroundParameterAsSource);
using var luminosityBlendEffectFactory = compositor.CreateEffectFactory(luminosityBlendEffect);
using var luminosityBlendEffectBrush = luminosityBlendEffectFactory.CreateBrush();
using var luminosityBlendEffectBrush1 = luminosityBlendEffectBrush.QueryInterface<ICompositionBrush>();
luminosityBlendEffectBrush.SetSourceParameter(backgroundHandle, micaBackdropBrush);
luminosityBlendEffectBrush.SetSourceParameter(foregroundHandle, luminosityOpacityEffectBrush);
using var backgroundParameterAsSource1 =
GetParameterSource("Background", backDropParameterFactory, out var backgroundHandle1);
using var foregroundParameterAsSource1 =
GetParameterSource("Foreground", backDropParameterFactory, out var foregroundHandle1);
using var colorBlendEffect =
new BlendEffect(22, backgroundParameterAsSource1, foregroundParameterAsSource1);
using var colorBlendEffectFactory = compositor.CreateEffectFactory(colorBlendEffect);
using var colorBlendEffectBrush = colorBlendEffectFactory.CreateBrush();
colorBlendEffectBrush.SetSourceParameter(backgroundHandle1, luminosityBlendEffectBrush1);
colorBlendEffectBrush.SetSourceParameter(foregroundHandle1, tintOpacityEffectBrush);
// colorBlendEffectBrush.SetSourceParameter(backgroundHandle, micaBackdropBrush);
using var micaBackdropBrush1 = colorBlendEffectBrush.QueryInterface<ICompositionBrush>();
return micaBackdropBrush1.CloneReference();
}
public static ICompositionBrush CreateAcrylicBlurBackdropBrush(ICompositor compositor)
@ -97,4 +154,15 @@ internal static class WinUiCompositionUtils
brush?.Dispose();
}
}
private static IGraphicsEffectSource GetParameterSource(string name,
ICompositionEffectSourceParameterFactory backDropParameterFactory, out IntPtr handle)
{
var backdropString = new HStringInterop(name);
var backDropParameter =
backDropParameterFactory.Create(backdropString.Handle);
var backDropParameterAsSource = backDropParameter.QueryInterface<IGraphicsEffectSource>();
handle = backdropString.Handle;
return backDropParameterAsSource;
}
}

3
src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs

@ -4,7 +4,8 @@
{
None,
Acrylic,
Mica
MicaLight,
MicaDark
}
internal interface IBlurHost

23
src/Windows/Avalonia.Win32/WinRT/WinRTPropertyValue.cs

@ -1,4 +1,5 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Avalonia.Win32.WinRT
@ -16,7 +17,15 @@ namespace Avalonia.Win32.WinRT
UInt32 = u;
Type = PropertyType.UInt32;
}
public WinRTPropertyValue(float[] uiColor)
{
Type = PropertyType.SingleArray;
_singleArray = uiColor;
}
private readonly float[]? _singleArray;
public PropertyType Type { get; }
public int IsNumericScalar { get; }
public byte UInt8 { get; }
@ -62,7 +71,17 @@ namespace Avalonia.Win32.WinRT
public unsafe ulong* GetUInt64Array(uint* __valueSize) => throw NotImplemented;
public unsafe float* GetSingleArray(uint* __valueSize) => throw NotImplemented;
public unsafe float* GetSingleArray(uint* __valueSize)
{
if (_singleArray == null)
throw NotImplemented;
*__valueSize = (uint)_singleArray.Length;
var allocCoTaskMem = Marshal.AllocCoTaskMem(_singleArray.Length * Unsafe.SizeOf<float>());
Marshal.Copy(_singleArray, 0, allocCoTaskMem, _singleArray.Length);
float* s = (float*)allocCoTaskMem;
return s;
}
public unsafe double* GetDoubleArray(uint* __valueSize) => throw NotImplemented;

2
src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs

@ -59,7 +59,7 @@ namespace Avalonia.Win32
case WindowsMessage.WM_NCCALCSIZE:
{
if (ToInt32(wParam) == 1 && !HasFullDecorations || _isClientAreaExtended)
if (ToInt32(wParam) == 1 && _windowProperties.Decorations == SystemDecorations.None || _isClientAreaExtended)
{
return IntPtr.Zero;
}

23
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -106,6 +106,7 @@ namespace Avalonia.Win32
private static POINTER_PEN_INFO[]? s_historyPenInfos;
private static POINTER_INFO[]? s_historyInfos;
private static MOUSEMOVEPOINT[]? s_mouseHistoryInfos;
private PlatformThemeVariant _currentThemeVariant;
public WindowImpl()
{
@ -474,7 +475,12 @@ namespace Avalonia.Win32
return;
SetUseHostBackdropBrush(false);
_blurHost?.SetBlur(BlurEffect.Mica);
_blurHost?.SetBlur(_currentThemeVariant switch
{
PlatformThemeVariant.Light => BlurEffect.MicaLight,
PlatformThemeVariant.Dark => BlurEffect.MicaDark,
_ => throw new ArgumentOutOfRangeException()
});
}
private void SetAccentState(AccentState state)
@ -778,6 +784,7 @@ namespace Avalonia.Win32
public unsafe void SetFrameThemeVariant(PlatformThemeVariant themeVariant)
{
_currentThemeVariant = themeVariant;
if (Win32Platform.WindowsVersion.Build >= 22000)
{
var pvUseBackdropBrush = themeVariant == PlatformThemeVariant.Dark ? 1 : 0;
@ -786,6 +793,10 @@ namespace Avalonia.Win32
(int)DwmWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE,
&pvUseBackdropBrush,
sizeof(int));
if (TransparencyLevel == WindowTransparencyLevel.Mica)
{
SetTransparencyMica(Win32Platform.WindowsVersion);
}
}
}
@ -1329,14 +1340,12 @@ namespace Avalonia.Win32
if (!_isFullScreenActive)
{
var margin = newProperties.Decorations == SystemDecorations.BorderOnly ? 1 : 0;
var margins = new MARGINS
{
cyBottomHeight = margin,
cxRightWidth = margin,
cxLeftWidth = margin,
cyTopHeight = margin
cyBottomHeight = 0,
cxRightWidth = 0,
cxLeftWidth = 0,
cyTopHeight = 0
};
DwmExtendFrameIntoClientArea(_hwnd, ref margins);

2
src/tools/Avalonia.Analyzers/Avalonia.Analyzers.csproj

@ -4,7 +4,9 @@
<IncludeBuildOutput>false</IncludeBuildOutput>
<PackageId>Avalonia.Analyzers</PackageId>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<DebugType>embedded</DebugType>
<IsPackable>true</IsPackable>
<IncludeSymbols>false</IncludeSymbols>
<IsRoslynComponent>true</IsRoslynComponent>
</PropertyGroup>

2
src/tools/Avalonia.Generators/Avalonia.Generators.csproj

@ -4,7 +4,9 @@
<IncludeBuildOutput>false</IncludeBuildOutput>
<PackageId>Avalonia.Generators</PackageId>
<DefineConstants>$(DefineConstants);XAMLX_INTERNAL</DefineConstants>
<DebugType>embedded</DebugType>
<IsPackable>true</IsPackable>
<IncludeSymbols>false</IncludeSymbols>
<IsRoslynComponent>true</IsRoslynComponent>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>

6
src/tools/Avalonia.Generators/Common/XamlXNameResolver.cs

@ -38,6 +38,7 @@ internal class XamlXNameResolver : INameResolver, IXamlAstVisitor
{
if (child is XamlAstXamlPropertyValueNode propertyValueNode &&
propertyValueNode.Property is XamlAstNamePropertyReference namedProperty &&
!IsAttachedProperty(namedProperty) &&
namedProperty.Name == "Name" &&
propertyValueNode.Values.Count > 0 &&
propertyValueNode.Values[0] is XamlAstTextNode text)
@ -89,4 +90,9 @@ internal class XamlXNameResolver : INameResolver, IXamlAstVisitor
_ => _defaultFieldModifier
};
}
private static bool IsAttachedProperty(XamlAstNamePropertyReference namedProperty)
{
return !namedProperty.DeclaringType.Equals(namedProperty.TargetType);
}
}

2
tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs

@ -111,7 +111,7 @@ namespace Avalonia.Base.UnitTests.Media
using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
{
var characterHit = glyphRun.GetPreviousCaretCharacterHit(new CharacterHit(currentIndex, currentLength));
var characterHit = glyphRun.GetPreviousCaretCharacterHit(new CharacterHit(currentIndex + currentLength));
Assert.Equal(previousIndex, characterHit.FirstCharacterIndex);

31
tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs

@ -275,6 +275,37 @@ namespace Avalonia.Controls.UnitTests.Presenters
Assert.Equal(new Size(203.2, 203.2), target.Extent);
}
[Fact]
public void Extent_Should_Be_Rounded_To_Viewport_When_Close()
{
var root = new TestRoot
{
LayoutScaling = 1.75,
UseLayoutRounding = true
};
var target = new ScrollContentPresenter
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Content = new Border
{
Width = 164.57142857142858,
Height = 164.57142857142858,
Margin = new Thickness(6)
}
};
root.Child = target;
target.UpdateChild();
target.Measure(new Size(1000, 1000));
target.Arrange(new Rect(0, 0, 1000, 1000));
Assert.Equal(new Size(176.00000000000003, 176.00000000000003), target.Child!.DesiredSize);
Assert.Equal(new Size(176, 176), target.Viewport);
Assert.Equal(new Size(176, 176), target.Extent);
}
[Fact]
public void Extent_Width_Should_Be_Arrange_Width_When_CanScrollHorizontally_False()
{

16
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_SelectedValue.cs

@ -268,6 +268,22 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.True(called);
}
[Fact]
public void Handles_Null_SelectedItem_When_SelectedValueBinding_Assigned()
{
// Issue #11220
var items = new object[] { null };
var sic = new SelectingItemsControl
{
ItemsSource = items,
SelectedIndex = 0,
SelectedValueBinding = new Binding("Name"),
Template = Template()
};
Assert.Null(sic.SelectedValue);
}
private static FuncControlTemplate Template()
{
return new FuncControlTemplate<SelectingItemsControl>((control, scope) =>

35
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@ -513,6 +513,41 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Window_Should_Not_Be_Centered_When_WindowStartupLocation_Is_CenterScreen_And_Window_Is_Hidden_And_Shown()
{
var screen1 = new Mock<Screen>(1.0, new PixelRect(new PixelSize(1920, 1080)), new PixelRect(new PixelSize(1920, 1040)), true);
var screens = new Mock<IScreenImpl>();
screens.Setup(x => x.AllScreens).Returns(new Screen[] { screen1.Object });
screens.Setup(x => x.ScreenFromPoint(It.IsAny<PixelPoint>())).Returns(screen1.Object);
var windowImpl = MockWindowingPlatform.CreateWindowMock();
windowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480));
windowImpl.Setup(x => x.DesktopScaling).Returns(1);
windowImpl.Setup(x => x.RenderScaling).Returns(1);
windowImpl.Setup(x => x.Screen).Returns(screens.Object);
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = new Window(windowImpl.Object)
{
WindowStartupLocation = WindowStartupLocation.CenterScreen
};
window.Show();
var expected = new PixelPoint(150, 400);
window.Position = expected;
window.IsVisible = false;
window.IsVisible = true;
Assert.Equal(expected, window.Position);
}
}
[Fact]
public void Window_Should_Be_Centered_When_WindowStartupLocation_Is_CenterScreen()
{

3
tests/Avalonia.Generators.Tests/Views/NamedControl.xml

@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sample.App.NamedControl">
<TextBox Name="UserNameTextBox"
AutomationProperties.Name="The user name"
Watermark="Username input"
UseFloatingWatermark="True" />
</Window>
</Window>

3
tests/Avalonia.Generators.Tests/Views/xNamedControl.xml

@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sample.App.xNamedControl">
<TextBox x:Name="UserNameTextBox"
AutomationProperties.Name="The user name"
Watermark="Username input"
UseFloatingWatermark="True" />
</Window>
</Window>

136
tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs

@ -194,7 +194,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
for (var i = 0; i < clusters.Count; i++)
{
var expectedCluster = clusters[i];
var actualCluster = nextCharacterHit.FirstCharacterIndex;
var actualCluster = nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength;
Assert.Equal(expectedCluster, actualCluster);
@ -278,16 +278,6 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
Assert.Equal(clusters[i],
previousCharacterHit.FirstCharacterIndex + previousCharacterHit.TrailingLength);
}
firstCharacterHit = previousCharacterHit;
firstCharacterHit = textLine.GetPreviousCaretCharacterHit(firstCharacterHit);
previousCharacterHit = textLine.GetPreviousCaretCharacterHit(firstCharacterHit);
Assert.Equal(firstCharacterHit.FirstCharacterIndex, previousCharacterHit.FirstCharacterIndex);
Assert.Equal(0, previousCharacterHit.TrailingLength);
}
}
@ -728,6 +718,130 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting
}
}
[Fact]
public void Should_GetNextCaretCharacterHit_From_Mixed_TextBuffer()
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new MixedTextBufferTextSource();
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
var characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(9, 1));
Assert.Equal(10, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
characterHit = textLine.GetNextCaretCharacterHit(characterHit);
Assert.Equal(11, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(19, 1));
Assert.Equal(20, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(10));
Assert.Equal(11, characterHit.FirstCharacterIndex);
Assert.Equal(0, characterHit.TrailingLength);
characterHit = textLine.GetNextCaretCharacterHit(characterHit);
Assert.Equal(12, characterHit.FirstCharacterIndex);
Assert.Equal(0, characterHit.TrailingLength);
characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(20));
Assert.Equal(21, characterHit.FirstCharacterIndex);
Assert.Equal(0, characterHit.TrailingLength);
}
}
[Fact]
public void Should_GetPreviousCaretCharacterHit_From_Mixed_TextBuffer()
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new MixedTextBufferTextSource();
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 0, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
var characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(20, 1));
Assert.Equal(19, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(10, 1));
Assert.Equal(9, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
characterHit = textLine.GetPreviousCaretCharacterHit(characterHit);
Assert.Equal(8, characterHit.FirstCharacterIndex);
Assert.Equal(1, characterHit.TrailingLength);
characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(21));
Assert.Equal(20, characterHit.FirstCharacterIndex);
Assert.Equal(0, characterHit.TrailingLength);
characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(11));
Assert.Equal(10, characterHit.FirstCharacterIndex);
Assert.Equal(0, characterHit.TrailingLength);
characterHit = textLine.GetPreviousCaretCharacterHit(characterHit);
Assert.Equal(9, characterHit.FirstCharacterIndex);
Assert.Equal(0, characterHit.TrailingLength);
}
}
[Fact]
public void Should_GetCharacterHitFromDistance_From_Mixed_TextBuffer()
{
using (Start())
{
var defaultProperties = new GenericTextRunProperties(Typeface.Default);
var textSource = new MixedTextBufferTextSource();
var formatter = new TextFormatterImpl();
var textLine =
formatter.FormatLine(textSource, 20, double.PositiveInfinity,
new GenericTextParagraphProperties(defaultProperties));
var characterHit = textLine.GetCharacterHitFromDistance(double.PositiveInfinity);
Assert.Equal(40, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
}
}
private class MixedTextBufferTextSource : ITextSource
{
public TextRun? GetTextRun(int textSourceIndex)

Loading…
Cancel
Save