Browse Source

Merge upstream/master

pull/20647/head
Wiesław Šoltés 1 month ago
parent
commit
6fcbdb7f3b
  1. 5
      .ncrunch/Avalonia.HarfBuzz.v3.ncrunchproject
  2. 3
      Avalonia.v3.ncrunchsolution
  3. 6
      api/Avalonia.Android.nupkg.xml
  4. 40
      api/Avalonia.Headless.nupkg.xml
  5. 72
      api/Avalonia.Skia.nupkg.xml
  6. 1910
      api/Avalonia.nupkg.xml
  7. 3
      native/Avalonia.Native/src/OSX/AvnView.mm
  8. 44
      samples/BindingDemo/MainWindow.xaml
  9. 3
      samples/ControlCatalog/MainView.xaml
  10. 8
      samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
  11. 45
      samples/ControlCatalog/Pages/BitmapCachePage.axaml
  12. 13
      samples/ControlCatalog/Pages/BitmapCachePage.axaml.cs
  13. 19
      samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml
  14. 4
      samples/ControlCatalog/Pages/ClipboardPage.xaml
  15. 21
      samples/ControlCatalog/Pages/DialogsPage.xaml
  16. 95
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  17. 26
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml
  18. 43
      samples/ControlCatalog/Pages/TextBoxPage.xaml
  19. 6
      samples/ControlCatalog/Pages/ThemePage.axaml
  20. 14
      samples/Generators.Sandbox/Controls/SignUpView.xaml
  21. 2
      samples/IntegrationTestApp/Pages/EmbeddingPage.axaml
  22. 69
      samples/IntegrationTestApp/Pages/EmbeddingPage.axaml.cs
  23. 14
      samples/IntegrationTestApp/Pages/ScreensPage.axaml
  24. 2
      samples/IntegrationTestApp/Pages/WindowDecorationsPage.axaml
  25. 2
      samples/IntegrationTestApp/Pages/WindowPage.axaml
  26. 4
      samples/SafeAreaDemo/Views/MainView.xaml
  27. 8
      samples/SingleProjectSandbox/MainView.axaml
  28. 2
      src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs
  29. 2
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  30. 6
      src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs
  31. 11
      src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs
  32. 13
      src/Avalonia.Base/Animation/Animation.AnimatorRegistry.cs
  33. 17
      src/Avalonia.Base/Animation/Easings/CubicBezierEasing.cs
  34. 30
      src/Avalonia.Base/Animation/ICustomAnimator.cs
  35. 27
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  36. 5
      src/Avalonia.Base/Data/BindingPriority.cs
  37. 91
      src/Avalonia.Base/Data/CompiledBinding.cs
  38. 6
      src/Avalonia.Base/Data/CompiledBindingPath.cs
  39. 53
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  40. 402
      src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs
  41. 20
      src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitorMembers.cs
  42. 37
      src/Avalonia.Base/Diagnostics/StyleDiagnostics.cs
  43. 3
      src/Avalonia.Base/Diagnostics/StyleValueFrameDiagnostic.cs
  44. 24
      src/Avalonia.Base/Diagnostics/StyledElementExtensions.cs
  45. 61
      src/Avalonia.Base/Input/DataFormats.cs
  46. 45
      src/Avalonia.Base/Input/DataObject.cs
  47. 54
      src/Avalonia.Base/Input/DataObjectExtensions.cs
  48. 9
      src/Avalonia.Base/Input/DataTransferExtensions.cs
  49. 10
      src/Avalonia.Base/Input/DragDrop.cs
  50. 17
      src/Avalonia.Base/Input/DragEventArgs.cs
  51. 32
      src/Avalonia.Base/Input/IDataObject.cs
  52. 2
      src/Avalonia.Base/Input/MouseDevice.cs
  53. 55
      src/Avalonia.Base/Input/Platform/Clipboard.cs
  54. 76
      src/Avalonia.Base/Input/Platform/DataObjectToDataTransferItemWrapper.cs
  55. 95
      src/Avalonia.Base/Input/Platform/DataObjectToDataTransferWrapper.cs
  56. 42
      src/Avalonia.Base/Input/Platform/DataTransferToDataObjectWrapper.cs
  57. 55
      src/Avalonia.Base/Input/Platform/IClipboard.cs
  58. 9
      src/Avalonia.Base/Input/Platform/IPlatformDragSource.cs
  59. 4
      src/Avalonia.Base/Input/Pointer.cs
  60. 22
      src/Avalonia.Base/Input/Raw/RawDragEvent.cs
  61. 33
      src/Avalonia.Base/Input/Raw/RawKeyEventArgs.cs
  62. 12
      src/Avalonia.Base/Layout/Layoutable.cs
  63. 114
      src/Avalonia.Base/Media/BitmapCache.cs
  64. 21
      src/Avalonia.Base/Media/CacheMode.cs
  65. 7
      src/Avalonia.Base/Media/Color.cs
  66. 9
      src/Avalonia.Base/Media/DrawingContext.cs
  67. 2
      src/Avalonia.Base/Media/IRadialGradientBrush.cs
  68. 2
      src/Avalonia.Base/Media/Immutable/ImmutableRadialGradientBrush.cs
  69. 40
      src/Avalonia.Base/Media/RadialGradientBrush.cs
  70. 8
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  71. 47
      src/Avalonia.Base/Platform/IRenderTarget.cs
  72. 70
      src/Avalonia.Base/Platform/LtrbRect.cs
  73. 2
      src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleCompositionBrush.cs
  74. 5
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  75. 10
      src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs
  76. 5
      src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
  77. 6
      src/Avalonia.Base/Rendering/Composition/CompositionExternalMemory.cs
  78. 1
      src/Avalonia.Base/Rendering/Composition/CompositionInterop.cs
  79. 18
      src/Avalonia.Base/Rendering/Composition/CompositionOptions.cs
  80. 76
      src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
  81. 5
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositorResourceHelpers.cs
  82. 8
      src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs
  83. 6
      src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs
  84. 15
      src/Avalonia.Base/Rendering/Composition/Server/CompositionTargetOverlays.cs
  85. 41
      src/Avalonia.Base/Rendering/Composition/Server/CompositorPools.cs
  86. 105
      src/Avalonia.Base/Rendering/Composition/Server/DirtyRectTracker.cs
  87. 13
      src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/DebugEventsDirtyRectCollectorProxy.cs
  88. 24
      src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/IDirtyRectTracker.cs
  89. 348
      src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/MultiDirtyRectTracker.CDirtyRegion.cs
  90. 86
      src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/MultiDirtyRectTracker.cs
  91. 60
      src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/RegionDirtyRectTracker.cs
  92. 50
      src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/SingleDirtyRectTracker.cs
  93. 45
      src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs
  94. 12
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBitmapCache.cs
  95. 24
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionCacheMode.cs
  96. 95
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs
  97. 24
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs
  98. 24
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs
  99. 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs
  100. 48
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs

5
.ncrunch/Avalonia.HarfBuzz.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>False</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

3
Avalonia.v3.ncrunchsolution

@ -13,8 +13,9 @@
<Value>TargetFrameworks = net10.0</Value>
</CustomBuildProperties>
<EnableRDI>False</EnableRDI>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
<ProjectConfigStoragePathRelativeToSolutionDir>.ncrunch</ProjectConfigStoragePathRelativeToSolutionDir>
<RdiConfigured>True</RdiConfigured>
<SolutionConfigured>True</SolutionConfigured>
</Settings>
</SolutionConfiguration>
</SolutionConfiguration>

6
api/Avalonia.Android.nupkg.xml

@ -1,6 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/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>CP0001</DiagnosticId>
<Target>T:Avalonia.Android.Platform.Specific.IAndroidView</Target>
<Left>baseline/Avalonia.Android/lib/net10.0-android36.0/Avalonia.Android.dll</Left>
<Right>current/Avalonia.Android/lib/net10.0-android36.0/Avalonia.Android.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0008</DiagnosticId>
<Target>T:Avalonia.Android.AvaloniaActivity</Target>

40
api/Avalonia.Headless.nupkg.xml

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/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>CP0002</DiagnosticId>
<Target>M:Avalonia.Headless.HeadlessWindowExtensions.DragDrop(Avalonia.Controls.TopLevel,Avalonia.Point,Avalonia.Input.Raw.RawDragEventType,Avalonia.Input.IDataObject,Avalonia.Input.DragDropEffects,Avalonia.Input.RawInputModifiers)</Target>
<Left>baseline/Avalonia.Headless/lib/net10.0/Avalonia.Headless.dll</Left>
<Right>current/Avalonia.Headless/lib/net10.0/Avalonia.Headless.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Headless.HeadlessWindowExtensions.KeyPress(Avalonia.Controls.TopLevel,Avalonia.Input.Key,Avalonia.Input.RawInputModifiers)</Target>
<Left>baseline/Avalonia.Headless/lib/net10.0/Avalonia.Headless.dll</Left>
<Right>current/Avalonia.Headless/lib/net10.0/Avalonia.Headless.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Headless.HeadlessWindowExtensions.KeyRelease(Avalonia.Controls.TopLevel,Avalonia.Input.Key,Avalonia.Input.RawInputModifiers)</Target>
<Left>baseline/Avalonia.Headless/lib/net10.0/Avalonia.Headless.dll</Left>
<Right>current/Avalonia.Headless/lib/net10.0/Avalonia.Headless.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Headless.HeadlessWindowExtensions.DragDrop(Avalonia.Controls.TopLevel,Avalonia.Point,Avalonia.Input.Raw.RawDragEventType,Avalonia.Input.IDataObject,Avalonia.Input.DragDropEffects,Avalonia.Input.RawInputModifiers)</Target>
<Left>baseline/Avalonia.Headless/lib/net8.0/Avalonia.Headless.dll</Left>
<Right>current/Avalonia.Headless/lib/net8.0/Avalonia.Headless.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Headless.HeadlessWindowExtensions.KeyPress(Avalonia.Controls.TopLevel,Avalonia.Input.Key,Avalonia.Input.RawInputModifiers)</Target>
<Left>baseline/Avalonia.Headless/lib/net8.0/Avalonia.Headless.dll</Left>
<Right>current/Avalonia.Headless/lib/net8.0/Avalonia.Headless.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Headless.HeadlessWindowExtensions.KeyRelease(Avalonia.Controls.TopLevel,Avalonia.Input.Key,Avalonia.Input.RawInputModifiers)</Target>
<Left>baseline/Avalonia.Headless/lib/net8.0/Avalonia.Headless.dll</Left>
<Right>current/Avalonia.Headless/lib/net8.0/Avalonia.Headless.dll</Right>
</Suppression>
</Suppressions>

72
api/Avalonia.Skia.nupkg.xml

@ -1,6 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://learn.microsoft.com/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>CP0001</DiagnosticId>
<Target>T:Avalonia.Skia.ISkiaGpuRenderTarget2</Target>
<Left>baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll</Left>
<Right>current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Skia.ISkiaGpuWithPlatformGraphicsContext</Target>
<Left>baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll</Left>
<Right>current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Skia.ISkiaGpuRenderTarget2</Target>
<Left>baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll</Left>
<Right>current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0001</DiagnosticId>
<Target>T:Avalonia.Skia.ISkiaGpuWithPlatformGraphicsContext</Target>
<Left>baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll</Left>
<Right>current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Skia.ISkiaGpuRenderTarget.BeginRenderingSession</Target>
<Left>baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll</Left>
<Right>current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Avalonia.Skia.ISkiaGpuRenderTarget.BeginRenderingSession</Target>
<Left>baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll</Left>
<Right>current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Skia.ISkiaGpu.TryGetGrContext</Target>
<Left>baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll</Left>
<Right>current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Skia.ISkiaGpuRenderTarget.BeginRenderingSession(System.Nullable{Avalonia.PixelSize})</Target>
<Left>baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll</Left>
<Right>current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Skia.ISkiaGpu.PlatformGraphicsContext</Target>
<Left>baseline/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll</Left>
<Right>current/Avalonia.Skia/lib/net10.0/Avalonia.Skia.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Skia.ISkiaGpu.TryGetGrContext</Target>
<Left>baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll</Left>
<Right>current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>M:Avalonia.Skia.ISkiaGpuRenderTarget.BeginRenderingSession(System.Nullable{Avalonia.PixelSize})</Target>
<Left>baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll</Left>
<Right>current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0006</DiagnosticId>
<Target>P:Avalonia.Skia.ISkiaGpu.PlatformGraphicsContext</Target>
<Left>baseline/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll</Left>
<Right>current/Avalonia.Skia/lib/net8.0/Avalonia.Skia.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0008</DiagnosticId>
<Target>T:Avalonia.Skia.ISkiaGpu</Target>

1910
api/Avalonia.nupkg.xml

File diff suppressed because it is too large

3
native/Avalonia.Native/src/OSX/AvnView.mm

@ -42,7 +42,8 @@
- (void) updateRenderTarget
{
if(_currentRenderTarget) {
[_currentRenderTarget resize:_lastPixelSize withScale:static_cast<float>([[self window] backingScaleFactor])];
AvnPixelSize size { MAX(_lastPixelSize.Width, 1), MAX(_lastPixelSize.Height, 1) };
[_currentRenderTarget resize:size withScale:static_cast<float>([[self window] backingScaleFactor])];
[self setNeedsDisplayInRect:[self frame]];
}
}

44
samples/BindingDemo/MainWindow.xaml

@ -1,7 +1,7 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
x:Class="BindingDemo.MainWindow"
xmlns:vm="using:BindingDemo.ViewModels"
xmlns:vm="using:BindingDemo.ViewModels"
xmlns:local="using:BindingDemo"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
Title="AvaloniaUI Bindings Test"
@ -13,29 +13,29 @@
<Setter Property="FontSize" Value="18"/>
</Style>
</Window.Styles>
<TabControl>
<TabItem Header="Basic">
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Simple Bindings"/>
<TextBox Watermark="Two Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue}" Name="first"/>
<TextBox Watermark="Two Way (LostFocus)" UseFloatingWatermark="True" Text="{Binding Path=StringValue, UpdateSourceTrigger=LostFocus}"/>
<TextBox Watermark="One Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneWay}"/>
<TextBox Watermark="One Time" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneTime}"/>
<TextBox PlaceholderText="Two Way" UseFloatingPlaceholder="True" Text="{Binding Path=StringValue}" Name="first"/>
<TextBox PlaceholderText="Two Way (LostFocus)" UseFloatingPlaceholder="True" Text="{Binding Path=StringValue, UpdateSourceTrigger=LostFocus}"/>
<TextBox PlaceholderText="One Way" UseFloatingPlaceholder="True" Text="{Binding Path=StringValue, Mode=OneWay}"/>
<TextBox PlaceholderText="One Time" UseFloatingPlaceholder="True" Text="{Binding Path=StringValue, Mode=OneTime}"/>
<!-- Removed due to #2983: reinstate when that's fixed.
<TextBox Watermark="One Way to Source" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneWayToSource}"/>
<TextBox Placeholder="One Way to Source" UseFloatingPlaceholder="True" Text="{Binding Path=StringValue, Mode=OneWayToSource}"/>
-->
</StackPanel>
<StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Collection Bindings"/>
<TextBox Watermark="Items[1].Value" UseFloatingWatermark="True" Text="{Binding Path=Items[1].Value}"/>
<TextBox PlaceholderText="Items[1].Value" UseFloatingPlaceholder="True" Text="{Binding Path=Items[1].Value}"/>
<Button Command="{Binding ShuffleItems}">Shuffle</Button>
</StackPanel>
<StackPanel Margin="18" Spacing="4" Width="200">
<TextBlock FontSize="16" Text="Negated Bindings"/>
<TextBox Watermark="Boolean String" UseFloatingWatermark="True" Text="{Binding Path=BooleanString}"/>
<TextBox PlaceholderText="Boolean String" UseFloatingPlaceholder="True" Text="{Binding Path=BooleanString}"/>
<CheckBox IsChecked="{Binding !BooleanString}">!BooleanString</CheckBox>
<CheckBox IsChecked="{Binding !!BooleanString}">!!BooleanString</CheckBox>
</StackPanel>
@ -43,24 +43,24 @@
<StackPanel Orientation="Horizontal">
<StackPanel Margin="18" Spacing="4" Width="200" HorizontalAlignment="Left">
<TextBlock FontSize="16" Text="Numeric Bindings"/>
<TextBox Watermark="Double" UseFloatingWatermark="True" Text="{Binding Path=DoubleValue, Mode=TwoWay}"/>
<TextBox PlaceholderText="Double" UseFloatingPlaceholder="True" Text="{Binding Path=DoubleValue, Mode=TwoWay}"/>
<TextBlock Text="{Binding Path=DoubleValue}"/>
<ProgressBar Maximum="10" Value="{Binding DoubleValue}"/>
</StackPanel>
<StackPanel Margin="18" Spacing="4" Width="200" HorizontalAlignment="Left">
<TextBlock FontSize="16" Text="Binding Sources"/>
<TextBox Watermark="Value of first TextBox" UseFloatingWatermark="True"
<TextBox PlaceholderText="Value of first TextBox" UseFloatingPlaceholder="True"
Text="{Binding #first.Text, Mode=TwoWay}"/>
<TextBox Watermark="Value of SharedItem.StringValue" UseFloatingWatermark="True"
<TextBox PlaceholderText="Value of SharedItem.StringValue" UseFloatingPlaceholder="True"
Text="{Binding Value, Source={StaticResource SharedItem}, Mode=TwoWay, DataType={x:Type vm:MainWindowViewModel+TestItem, x:TypeArguments=x:String}}"/>
<TextBox Watermark="Value of SharedItem.StringValue (duplicate)" UseFloatingWatermark="True"
<TextBox PlaceholderText="Value of SharedItem.StringValue (duplicate)" UseFloatingPlaceholder="True"
Text="{Binding Value, Source={StaticResource SharedItem}, Mode=TwoWay, DataType={x:Type vm:MainWindowViewModel+TestItem, x:TypeArguments=x:String}}"/>
</StackPanel>
<StackPanel Margin="18" Spacing="4" Width="200" HorizontalAlignment="Left">
<TextBlock FontSize="16" Text="Scheduler"/>
<TextBox Watermark="Background Thread" Text="{Binding CurrentTime, Mode=OneWay}"/>
<TextBox PlaceholderText="Background Thread" Text="{Binding CurrentTime, Mode=OneWay}"/>
<TextBlock FontSize="16" Text="Stream Operator"/>
<TextBox Watermark="StreamOperator" Text="{CompiledBinding CurrentTimeObservable^, Mode=OneWay}"/>
<TextBox PlaceholderText="StreamOperator" Text="{CompiledBinding CurrentTimeObservable^, Mode=OneWay}"/>
</StackPanel>
</StackPanel>
</StackPanel>
@ -91,19 +91,19 @@
</TabItem>
<TabItem Header="Property Validation">
<StackPanel Orientation="Horizontal">
<StackPanel Margin="18" Spacing="4" MinWidth="200" DataContext="{Binding ExceptionDataValidation}">
<StackPanel Margin="18" Spacing="4" MinWidth="200" DataContext="{Binding ExceptionDataValidation}">
<TextBlock FontSize="16" Text="Exception Validation"/>
<TextBox Watermark="Less Than 10" UseFloatingWatermark="True" Text="{Binding Path=LessThan10}"/>
<TextBox PlaceholderText="Less Than 10" UseFloatingPlaceholder="True" Text="{Binding Path=LessThan10}"/>
</StackPanel>
<StackPanel Margin="18" Spacing="4" MinWidth="200" DataContext="{Binding IndeiDataValidation}">
<TextBlock FontSize="16" Text="INotifyDataErrorInfo Validation"/>
<TextBox Watermark="Maximum" UseFloatingWatermark="True" Text="{Binding Path=Maximum}"/>
<TextBox Watermark="Value" UseFloatingWatermark="True" Text="{Binding Path=Value}"/>
<TextBox PlaceholderText="Maximum" UseFloatingPlaceholder="True" Text="{Binding Path=Maximum}"/>
<TextBox PlaceholderText="Value" UseFloatingPlaceholder="True" Text="{Binding Path=Value}"/>
</StackPanel>
<StackPanel Margin="18" Spacing="4" MinWidth="200" DataContext="{Binding DataAnnotationsValidation}">
<StackPanel Margin="18" Spacing="4" MinWidth="200" DataContext="{Binding DataAnnotationsValidation}">
<TextBlock FontSize="16" Text="Data Annotations Validation"/>
<TextBox Watermark="Phone #" UseFloatingWatermark="True" Text="{Binding PhoneNumber}"/>
<TextBox Watermark="Less Than 10" UseFloatingWatermark="True" Text="{Binding Path=LessThan10}"/>
<TextBox PlaceholderText="Phone #" UseFloatingPlaceholder="True" Text="{Binding PhoneNumber}"/>
<TextBox PlaceholderText="Less Than 10" UseFloatingPlaceholder="True" Text="{Binding Path=LessThan10}"/>
</StackPanel>
</StackPanel>
</TabItem>

3
samples/ControlCatalog/MainView.xaml

@ -34,6 +34,9 @@
<TabItem Header="Border">
<pages:BorderPage />
</TabItem>
<TabItem Header="BitmapCache">
<pages:BitmapCachePage />
</TabItem>
<TabItem Header="Buttons">
<pages:ButtonsPage />
</TabItem>

8
samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml

@ -39,8 +39,12 @@
<AutoCompleteBox MaxDropDownHeight="60" />
</StackPanel>
<StackPanel>
<TextBlock Text="Watermark" />
<AutoCompleteBox Watermark="Hello World" />
<TextBlock Text="PlaceholderText" />
<AutoCompleteBox PlaceholderText="Hello World" />
</StackPanel>
<StackPanel>
<TextBlock Text="PlaceholderText with custom color" />
<AutoCompleteBox PlaceholderText="Search here..." PlaceholderForeground="Green" />
</StackPanel>
<StackPanel>
<TextBlock Text="Disabled" />

45
samples/ControlCatalog/Pages/BitmapCachePage.axaml

@ -0,0 +1,45 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="ControlCatalog.Pages.BitmapCachePage">
<DockPanel>
<DockPanel.Resources>
<BitmapCache x:Key="Cache"/>
<ScaleTransform x:Key="Transform" ScaleX="1" ScaleY="{Binding $self.ScaleX, Mode=OneWay}"/>
<TranslateTransform x:Key="SubPixelTransform"/>
</DockPanel.Resources>
<StackPanel DockPanel.Dock="Right" ZIndex="1">
<TextBlock>Render at scale</TextBlock>
<Slider Minimum="0.1" Maximum="4" Value="{Binding Source={StaticResource Cache}, Path=RenderAtScale, Mode=TwoWay}" Width="200"/>
<TextBlock>Scale</TextBlock>
<Slider Minimum="0.1" Maximum="4" Value="{Binding Source={StaticResource Transform}, Path=ScaleX, Mode=TwoWay}" Width="200"/>
<CheckBox IsChecked="{Binding Source={StaticResource Cache}, Path=EnableClearType, Mode=TwoWay}">Enable clear type</CheckBox>
<CheckBox IsChecked="{Binding Source={StaticResource Cache}, Path=SnapsToDevicePixels, Mode=TwoWay}">Snap to device pixels</CheckBox>
<TextBlock>Subpixel offset X</TextBlock>
<Slider Minimum="0" Maximum="1" Value="{Binding Source={StaticResource SubPixelTransform}, Path=X, Mode=TwoWay}" Width="200"/>
</StackPanel>
<Decorator RenderTransform="{StaticResource SubPixelTransform}">
<Border Background="Beige" Margin="10"
RenderTransform="{StaticResource Transform}"
RenderTransformOrigin="0.5,0.5">
<Border Margin="10"
CacheMode="{StaticResource Cache}"
TextElement.Foreground="Black">
<Border
Margin="10"
BorderThickness="2"
BorderBrush="Black"
Background="White">
<StackPanel>
<Slider MinWidth="200"/>
<TextBlock TextWrapping="Wrap">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</TextBlock>
</StackPanel>
</Border>
</Border>
</Border>
</Decorator>
</DockPanel>
</UserControl>

13
samples/ControlCatalog/Pages/BitmapCachePage.axaml.cs

@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace ControlCatalog.Pages;
public partial class BitmapCachePage : UserControl
{
public BitmapCachePage()
{
InitializeComponent();
}
}

19
samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml

@ -5,7 +5,7 @@
x:Class="ControlCatalog.Pages.CalendarDatePickerPage">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h2">A control for selecting dates with a calendar drop-down</TextBlock>
<StackPanel Orientation="Horizontal"
Margin="0,16,0,0"
HorizontalAlignment="Center"
@ -32,19 +32,24 @@
Margin="0,0,0,8"/>
<CalendarDatePicker Margin="0,0,0,8"
Watermark="Watermark"/>
PlaceholderText="Placeholder"/>
<CalendarDatePicker Margin="0,0,0,8"
Name="DatePicker5"
Watermark="Floating Watermark"
UseFloatingWatermark="True"/>
PlaceholderText="Floating Placeholder"
UseFloatingPlaceholder="True"/>
<TextBlock Text="PlaceholderText with custom color"/>
<CalendarDatePicker Margin="0,0,0,8"
PlaceholderText="Select a date"
PlaceholderForeground="Purple"/>
<TextBlock Text="Disabled"/>
<CalendarDatePicker IsEnabled="False"/>
<TextBlock Text="Validation Example"/>
<CalendarDatePicker SelectedDate="{CompiledBinding ValidatedDateExample, Mode=TwoWay}"/>
</StackPanel>
</StackPanel>
</StackPanel>
</StackPanel>
</UserControl>

4
samples/ControlCatalog/Pages/ClipboardPage.xaml

@ -1,4 +1,4 @@
<UserControl x:Class="ControlCatalog.Pages.ClipboardPage"
<UserControl x:Class="ControlCatalog.Pages.ClipboardPage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel Orientation="Vertical" Spacing="4">
@ -21,7 +21,7 @@
<TextBox x:Name="ClipboardContent"
MinHeight="100"
AcceptsReturn="True"
Watermark="Text to copy of file names per line" />
PlaceholderText="Text to copy of file names per line" />
<Viewbox Width="420" Height="360">
<Image x:Name="ClipboardImage"
/>

21
samples/ControlCatalog/Pages/DialogsPage.xaml

@ -49,26 +49,17 @@
<Button Name="OpenFolderFromBookmark">Open Folder Bookmark</Button>
</StackPanel>
</Expander>
<Expander Header="Legacy OpenFileDialog">
<StackPanel Spacing="4">
<Button Name="OpenFile">_Open File</Button>
<Button Name="OpenMultipleFiles">Open _Multiple File</Button>
<Button Name="SaveFile">_Save File</Button>
<Button Name="SelectFolder">Select Fo_lder</Button>
<Button Name="OpenBoth">Select _Both</Button>
</StackPanel>
</Expander>
<Expander Header="Launcher dialogs">
<StackPanel Spacing="4">
<TextBox Name="UriToLaunch" Watermark="Uri to launch" Text="https://avaloniaui.net/" />
<TextBox Name="UriToLaunch" PlaceholderText="Uri to launch" Text="https://avaloniaui.net/" />
<Button Name="LaunchUri">Launch Uri</Button>
<Button Name="LaunchFile">Launch File</Button>
<TextBlock Name="LaunchStatus" />
</StackPanel>
</Expander>
<AutoCompleteBox x:Name="CurrentFolderBox" Watermark="Write full path/uri or well known folder name">
<AutoCompleteBox x:Name="CurrentFolderBox" PlaceholderText="Write full path/uri or well known folder name">
<AutoCompleteBox.ItemsSource>
<generic:List x:TypeArguments="storage:WellKnownFolder">
<storage:WellKnownFolder>Desktop</storage:WellKnownFolder>
@ -80,17 +71,17 @@
</generic:List>
</AutoCompleteBox.ItemsSource>
</AutoCompleteBox>
<TextBlock x:Name="PickerLastResultsVisible"
Classes="h2"
IsVisible="False"
Text="Last picker results:" />
<ItemsControl x:Name="PickerLastResults" />
<TextBox Name="BookmarkContainer" Watermark="Bookmark" />
<TextBox Name="BookmarkContainer" PlaceholderText="Bookmark" />
<TextBox Name="OpenedFileContent"
MaxLines="10"
Watermark="Picked file content" />
PlaceholderText="Picked file content" />
</StackPanel>
</UserControl>

95
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -1,23 +1,14 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security;
using System.Text;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Dialogs;
using Avalonia.Layout;
using Avalonia.Markup.Xaml;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
#pragma warning disable CS0618 // Type or member is obsolete
#nullable enable
namespace ControlCatalog.Pages
{
@ -70,14 +61,6 @@ namespace ControlCatalog.Pages
};
List<FileDialogFilter> GetFilters()
{
return GetFileTypes()?.Select(f => new FileDialogFilter
{
Name = f.Name, Extensions = f.Patterns!.ToList()
}).ToList() ?? new List<FileDialogFilter>();
}
List<FilePickerFileType>? BuildFileTypes()
{
var selectedItem = (FilterSelector.SelectedItem as ComboBoxItem)?.Content
@ -168,88 +151,12 @@ namespace ControlCatalog.Pages
void UpdateSuggestedFilterSelectorState() =>
suggestedFilterSelector.IsEnabled = useSuggestedFilter.IsChecked == true;
useSuggestedFilter.Checked += (_, _) => UpdateSuggestedFilterSelectorState();
useSuggestedFilter.Unchecked += (_, _) => UpdateSuggestedFilterSelectorState();
useSuggestedFilter.IsCheckedChanged += (_, _) => UpdateSuggestedFilterSelectorState();
UpdateSuggestedFilterSelectorState();
FilterSelector.SelectionChanged += (_, _) => UpdateSuggestedFilterSelector(BuildFileTypes());
UpdateSuggestedFilterSelector(BuildFileTypes());
OpenFile.Click += async delegate
{
// Almost guaranteed to exist
var uri = Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName;
var initialFileName = uri == null ? null : System.IO.Path.GetFileName(uri);
var initialDirectory = uri == null ? null : System.IO.Path.GetDirectoryName(uri);
var result = await new OpenFileDialog()
{
Title = "Open file",
Filters = GetFilters(),
Directory = initialDirectory,
InitialFileName = initialFileName
}.ShowAsync(GetWindow());
results.ItemsSource = result;
resultsVisible.IsVisible = result?.Any() == true;
};
OpenMultipleFiles.Click += async delegate
{
var result = await new OpenFileDialog()
{
Title = "Open multiple files",
Filters = GetFilters(),
Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null,
AllowMultiple = true
}.ShowAsync(GetWindow());
results.ItemsSource = result;
resultsVisible.IsVisible = result?.Any() == true;
};
SaveFile.Click += async delegate
{
var filters = GetFilters();
var result = await new SaveFileDialog()
{
Title = "Save file",
Filters = filters,
Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null,
DefaultExtension = filters?.Any() == true ? "txt" : null,
InitialFileName = "test.txt"
}.ShowAsync(GetWindow());
results.ItemsSource = new[] { result };
resultsVisible.IsVisible = result != null;
};
SelectFolder.Click += async delegate
{
var result = await new OpenFolderDialog()
{
Title = "Select folder",
Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null,
}.ShowAsync(GetWindow());
if (string.IsNullOrEmpty(result))
{
resultsVisible.IsVisible = false;
}
else
{
SetFolder(await GetStorageProvider().TryGetFolderFromPathAsync(result!));
results.ItemsSource = new[] { result };
resultsVisible.IsVisible = true;
}
};
OpenBoth.Click += async delegate
{
var result = await new OpenFileDialog()
{
Title = "Select both",
Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null,
AllowMultiple = true
}.ShowManagedAsync(GetWindow(), new ManagedFileDialogOptions
{
AllowDirectorySelection = true
});
results.ItemsSource = result;
resultsVisible.IsVisible = result?.Any() == true;
};
DecoratedWindow.Click += delegate
{
new DecoratedWindow().Show();

26
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="using:System"
xmlns:converter="using:ControlCatalog.Converter"
@ -48,8 +48,8 @@
<ComboBox x:Name="CultureSelector" Grid.Row="2" Grid.Column="1" ItemsSource="{Binding Cultures}"
VerticalAlignment="Center" Margin="2"/>
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="2">Watermark:</TextBlock>
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding #upDown.Watermark}" VerticalAlignment="Center" Margin="2" />
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="2">PlaceholderText:</TextBlock>
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding #upDown.PlaceholderText}" VerticalAlignment="Center" Margin="2" />
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center" Margin="2">Text:</TextBlock>
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding #upDown.Text}" VerticalAlignment="Center" Margin="2" />
@ -81,23 +81,23 @@
<NumericUpDown Name="upDown" Minimum="0" Maximum="10" Increment="0.5"
NumberFormat="{Binding #CultureSelector.SelectedItem, Converter={x:Static pages:NumericUpDownPage.CultureConverter}}"
VerticalAlignment="Center" Value="{Binding DecimalValue}"
Watermark="Enter text" FormatString="{Binding SelectedFormat.Value}"/>
PlaceholderText="Enter text" FormatString="{Binding SelectedFormat.Value}"/>
</StackPanel>
<StackPanel Orientation="Vertical" Margin="10">
<Label Target="DoubleUpDown" FontSize="14" FontWeight="Bold" VerticalAlignment="Center">Usage of double NumericUpDown:</Label>
<NumericUpDown Name="DoubleUpDown" Minimum="0" Maximum="10" Increment="0.5"
VerticalAlignment="Center" Value="{Binding DoubleValue}"
Watermark="Enter text" FormatString="{Binding SelectedFormat.Value}"/>
PlaceholderText="Enter text" FormatString="{Binding SelectedFormat.Value}"/>
</StackPanel>
<StackPanel Orientation="Vertical" Margin="10">
<Label Target="ValidationUpDown" FontSize="14" FontWeight="Bold" VerticalAlignment="Center">NumericUpDown with Validation Errors:</Label>
<NumericUpDown x:Name="ValidationUpDown" Minimum="0" Maximum="10" Increment="0.5"
VerticalAlignment="Center"
Watermark="Enter text" FormatString="{Binding SelectedFormat.Value}">
PlaceholderText="Enter text" FormatString="{Binding SelectedFormat.Value}">
<DataValidationErrors.Error>
<sys:Exception />
<sys:Exception />
</DataValidationErrors.Error>
</NumericUpDown>
</StackPanel>
@ -110,10 +110,18 @@
<converter:HexConverter></converter:HexConverter>
</NumericUpDown.TextConverter>
</NumericUpDown>
</StackPanel>
</WrapPanel>
<StackPanel Orientation="Vertical" Margin="10">
<Label FontSize="14" FontWeight="Bold" Target="PlaceholderForegroundUpDown"
Content="Placeholder with custom foreground color:"/>
<NumericUpDown x:Name="PlaceholderForegroundUpDown"
PlaceholderText="Enter amount"
PlaceholderForeground="Orange"/>
</StackPanel>
<StackPanel Margin="10">
<Label FontSize="14" FontWeight="Bold" Target="InnerContent"
Content="Inner Contents"/>

43
samples/ControlCatalog/Pages/TextBoxPage.xaml

@ -17,34 +17,41 @@
</Flyout>
</TextBox.ContextFlyout>
</TextBox>
<TextBox Width="200" Watermark="ReadOnly" IsReadOnly="True" Text="This is read only"/>
<TextBox Width="200" Watermark="Numeric with watermark" TextInputOptions.ContentType="Number" />
<TextBox Width="200" PlaceholderText="ReadOnly" IsReadOnly="True" Text="This is read only"/>
<TextBox Width="200" PlaceholderText="Numeric with placeholder" TextInputOptions.ContentType="Number" />
<TextBox Width="200"
Watermark="Floating Watermark"
UseFloatingWatermark="True"
PlaceholderText="Floating Placeholder"
UseFloatingPlaceholder="True"
Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit."/>
<TextBox Width="200"
PlaceholderText="Custom Placeholder Color"
PlaceholderForeground="Red"/>
<TextBox Width="200"
PlaceholderText="Floating Placeholder Color"
UseFloatingPlaceholder="True"
PlaceholderForeground="Purple"/>
<MaskedTextBox Width="200" ResetOnSpace="False" Mask="(LLL) 999-0000"/>
<TextBox Width="200" Text="Validation Error">
<DataValidationErrors.Error>
<sys:Exception />
<sys:Exception />
</DataValidationErrors.Error>
</TextBox>
<TextBox Width="200"
Watermark="Password Box"
PlaceholderText="Password Box"
Classes="revealPasswordButton"
TextInputOptions.ContentType="Password"
UseFloatingWatermark="True"
UseFloatingPlaceholder="True"
PasswordChar="*"
Text="Password" />
<TextBox Width="200" Watermark="Suggestions are hidden" TextInputOptions.ShowSuggestions="False" />
<TextBox Width="200" PlaceholderText="Suggestions are hidden" TextInputOptions.ShowSuggestions="False" />
<TextBox Width="200" Text="Left aligned text" TextAlignment="Left" AcceptsTab="True" />
<TextBox Width="200" Text="Center aligned text" TextAlignment="Center" />
<TextBox Width="200" Text="Right aligned text" TextAlignment="Right" />
<TextBox Width="200" Text="Custom selection brush"
SelectionStart="5" SelectionEnd="22"
SelectionBrush="Green" SelectionForegroundBrush="Yellow" ClearSelectionOnLostFocus="False"/>
SelectionStart="5" SelectionEnd="22"
SelectionBrush="Green" SelectionForegroundBrush="Yellow" ClearSelectionOnLostFocus="False"/>
<TextBox Width="200" Text="Custom caret brush" CaretBrush="DarkOrange"/>
</StackPanel>
@ -54,11 +61,11 @@
<TextBox AcceptsReturn="True" Width="200" Height="125"
Text="Multiline TextBox with no TextWrapping.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." />
<TextBox Classes="clearButton" Text="Clear Content" Width="200" FontWeight="Normal" FontStyle="Normal" Watermark="Watermark" FontFamily="avares://ControlCatalog/Assets/Fonts#Source Sans Pro"/>
<TextBox Classes="clearButton" Text="Clear Content" Width="200" FontWeight="Normal" FontStyle="Normal" PlaceholderText="Placeholder" FontFamily="avares://ControlCatalog/Assets/Fonts#Source Sans Pro"/>
<TextBox Text="IME small font" Width="200"
FontFamily="Comic Sans MS"
FontSize="10"
Foreground="Red"/>
FontFamily="Comic Sans MS"
FontSize="10"
Foreground="Red"/>
<TextBox Text="IME large font" Width="200"
FontFamily="Comic Sans MS"
FontSize="22"
@ -67,9 +74,9 @@
FontFamily="Comic Sans MS"
InputMethod.IsInputMethodEnabled="False"
Foreground="Red"/>
<TextBox AcceptsReturn="True"
TextWrapping="Wrap"
Width="200"
<TextBox AcceptsReturn="True"
TextWrapping="Wrap"
Width="200"
Height="125"
LineHeight="32"
Text="Multiline TextBox with TextWrapping and increased LineHeight.&#xD;&#xD;Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est." />

6
samples/ControlCatalog/Pages/ThemePage.axaml

@ -1,4 +1,4 @@
<UserControl x:Class="ControlCatalog.Pages.ThemePage"
<UserControl x:Class="ControlCatalog.Pages.ThemePage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
@ -70,8 +70,8 @@
</ComboBox>
<TextBlock Grid.Column="0" Grid.Row="2" Text="Username:" VerticalAlignment="Center" />
<TextBlock Grid.Column="0" Grid.Row="4" Text="Password:" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="2" Watermark="Input here" HorizontalAlignment="Stretch" />
<TextBox Grid.Column="1" Grid.Row="4" Watermark="Input here" HorizontalAlignment="Stretch" />
<TextBox Grid.Column="1" Grid.Row="2" PlaceholderText="Input here" HorizontalAlignment="Stretch" />
<TextBox Grid.Column="1" Grid.Row="4" PlaceholderText="Input here" HorizontalAlignment="Stretch" />
<Button Grid.Column="1" Grid.Row="6" Content="Login" HorizontalAlignment="Stretch" />
</Grid>
</Border>

14
samples/Generators.Sandbox/Controls/SignUpView.xaml

@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Generators.Sandbox.Controls"
xmlns:vm="clr-namespace:Generators.Sandbox.ViewModels"
@ -17,8 +17,8 @@
<controls:CustomTextBox Margin="0 10 0 0"
x:Name="UserNameTextBox"
Text="{Binding UserName}"
Watermark="Please, enter user name..."
UseFloatingWatermark="True" />
PlaceholderText="Please, enter user name..."
UseFloatingPlaceholder="True" />
<TextBlock x:Name="UserNameValidation"
Text="{Binding UserNameValidation}"
Foreground="Green"
@ -26,8 +26,8 @@
<TextBox Margin="0 10 0 0"
x:Name="PasswordTextBox"
Text="{Binding Password}"
Watermark="Please, enter your password..."
UseFloatingWatermark="True"
PlaceholderText="Please, enter your password..."
UseFloatingPlaceholder="True"
PasswordChar="*" />
<TextBlock x:Name="PasswordValidation"
Text="{Binding PasswordValidation}"
@ -36,8 +36,8 @@
<TextBox Margin="0 10 0 0"
x:Name="ConfirmPasswordTextBox"
Text="{Binding ConfirmPassword}"
Watermark="Please, confirm the password..."
UseFloatingWatermark="True"
PlaceholderText="Please, confirm the password..."
UseFloatingPlaceholder="True"
PasswordChar="*" />
<TextBlock x:Name="ConfirmPasswordValidation"
Text="{Binding ConfirmPasswordValidation}"

2
samples/IntegrationTestApp/Pages/EmbeddingPage.axaml

@ -16,5 +16,7 @@
</Popup>
</StackPanel>
<Button Name="Reset" Click="Reset_Click">Reset</Button>
<Button Name="RunNativeModalSession" Click="RunNativeModalSession_OnClick">Open Native Modal</Button>
<TextBox Name="ModalResultTextBox" />
</StackPanel>
</UserControl>

69
samples/IntegrationTestApp/Pages/EmbeddingPage.axaml.cs

@ -1,10 +1,19 @@
using System;
using Avalonia.Automation;
using Avalonia.Controls;
using Avalonia.Controls.Embedding;
using Avalonia.Interactivity;
using IntegrationTestApp.Embedding;
using MonoMac.AppKit;
using MonoMac.CoreGraphics;
using MonoMac.ObjCRuntime;
namespace IntegrationTestApp;
public partial class EmbeddingPage : UserControl
{
private const long NSModalResponseContinue = -1002;
public EmbeddingPage()
{
InitializeComponent();
@ -19,5 +28,65 @@ public partial class EmbeddingPage : UserControl
private void Reset_Click(object? sender, RoutedEventArgs e)
{
ResetText();
ModalResultTextBox.Text = "";
}
private void RunNativeModalSession_OnClick(object? sender, RoutedEventArgs e)
{
MacHelper.EnsureInitialized();
var app = NSApplication.SharedApplication;
var modalWindow = CreateNativeWindow();
var session = app.BeginModalSession(modalWindow);
while (true)
{
if (app.RunModalSession(session) != NSModalResponseContinue)
break;
}
app.EndModalSession(session);
}
private NSWindow CreateNativeWindow()
{
var button = new Button
{
Name = "ButtonInModal",
Content = "Button"
};
AutomationProperties.SetAutomationId(button, "ButtonInModal");
var root = new EmbeddableControlRoot
{
Width = 200,
Height = 200,
Content = button
};
root.Prepare();
var window = new NSWindow(
new CGRect(0, 0, root.Width, root.Height),
NSWindowStyle.Titled | NSWindowStyle.Closable,
NSBackingStore.Buffered,
false);
window.Identifier = "ModalNativeWindow";
window.WillClose += (_, _) => NSApplication.SharedApplication.StopModal();
button.Click += (_, _) =>
{
ModalResultTextBox.Text = "Clicked";
window.Close();
};
if (root.TryGetPlatformHandle() is not { } handle)
throw new InvalidOperationException("Could not get platform handle");
window.ContentView = (NSView)Runtime.GetNSObject(handle.Handle)!;
root.StartRendering();
return window;
}
}

14
samples/IntegrationTestApp/Pages/ScreensPage.axaml

@ -8,12 +8,12 @@
<Button Name="ScreenRefresh"
Content="Refresh"
Click="ScreenRefresh_Click"/>
<TextBox Name="ScreenName" Watermark="DisplayName" UseFloatingWatermark="true" />
<TextBox Name="ScreenHandle" Watermark="Handle" UseFloatingWatermark="true" />
<TextBox Name="ScreenScaling" Watermark="Scaling" UseFloatingWatermark="true" />
<TextBox Name="ScreenBounds" Watermark="Bounds" UseFloatingWatermark="true" />
<TextBox Name="ScreenWorkArea" Watermark="WorkArea" UseFloatingWatermark="true" />
<TextBox Name="ScreenOrientation" Watermark="Orientation" UseFloatingWatermark="true" />
<TextBox Name="ScreenSameReference" Watermark="Is same reference" UseFloatingWatermark="true" />
<TextBox Name="ScreenName" PlaceholderText="DisplayName" UseFloatingPlaceholder="true" />
<TextBox Name="ScreenHandle" PlaceholderText="Handle" UseFloatingPlaceholder="true" />
<TextBox Name="ScreenScaling" PlaceholderText="Scaling" UseFloatingPlaceholder="true" />
<TextBox Name="ScreenBounds" PlaceholderText="Bounds" UseFloatingPlaceholder="true" />
<TextBox Name="ScreenWorkArea" PlaceholderText="WorkArea" UseFloatingPlaceholder="true" />
<TextBox Name="ScreenOrientation" PlaceholderText="Orientation" UseFloatingPlaceholder="true" />
<TextBox Name="ScreenSameReference" PlaceholderText="Is same reference" UseFloatingPlaceholder="true" />
</StackPanel>
</UserControl>

2
samples/IntegrationTestApp/Pages/WindowDecorationsPage.axaml

@ -10,7 +10,7 @@
<CheckBox Name="WindowPreferSystemChrome" Content="Prefer SystemChrome" />
<CheckBox Name="WindowMacThickSystemChrome" Content="Mac Thick SystemChrome" />
<CheckBox Name="WindowShowTitleAreaControl" Content="Show Title Area Control" />
<TextBox Name="WindowTitleBarHeightHint" Text="-1" Watermark="In dips" />
<TextBox Name="WindowTitleBarHeightHint" Text="-1" PlaceholderText="In dips" />
<Button Name="ApplyWindowDecorations"
Content="Apply decorations on this Window"
Click="ApplyWindowDecorations_Click"/>

2
samples/IntegrationTestApp/Pages/WindowPage.axaml

@ -6,7 +6,7 @@
x:Class="IntegrationTestApp.Pages.WindowPage">
<Grid ColumnDefinitions="*,8,*">
<StackPanel Grid.Column="0">
<TextBox Name="ShowWindowSize" Watermark="Window Size"/>
<TextBox Name="ShowWindowSize" PlaceholderText="Window Size"/>
<ComboBox Name="ShowWindowMode" SelectedIndex="0">
<ComboBoxItem>NonOwned</ComboBoxItem>
<ComboBoxItem>Owned</ComboBoxItem>

4
samples/SafeAreaDemo/Views/MainView.xaml

@ -24,7 +24,7 @@
<Label Margin="5"
Foreground="Red"
VerticalAlignment="Bottom"
HorizontalContentAlignment="Right">View Bounds</Label>
HorizontalContentAlignment="Right">View Bounds</Label>
</Grid>
</Border>
<Border BorderBrush="LimeGreen"
@ -47,7 +47,7 @@
<CheckBox IsChecked="{Binding UseSafeArea}" IsEnabled="{Binding !AutoSafeAreaPadding}">Use Safe Area</CheckBox>
<CheckBox IsChecked="{Binding AutoSafeAreaPadding}">Automatic Paddings</CheckBox>
<CheckBox IsChecked="{Binding HideSystemBars}">Hide System Bars</CheckBox>
<TextBox Width="200" Watermark="Tap to Show Keyboard"/>
<TextBox Width="200" PlaceholderText="Tap to Show Keyboard"/>
</StackPanel>
</Grid>
</DockPanel>

8
samples/SingleProjectSandbox/MainView.axaml

@ -5,10 +5,10 @@
x:DataType="local:MainView">
<StackPanel Margin="100 50" Spacing="50">
<TextBlock Text="Login" Foreground="White" />
<TextBox TextInputOptions.Multiline="True" AcceptsReturn="True" Watermark="Text" Height="200" TextWrapping="Wrap"/>
<TextBox Watermark="Username" TextInputOptions.ContentType="Email" TextInputOptions.ReturnKeyType="Done" />
<TextBox Watermark="Password" PasswordChar="*" TextInputOptions.ContentType="Password" />
<TextBox Watermark="Pin" PasswordChar="*" TextInputOptions.ContentType="Digits" TextInputOptions.ReturnKeyType="Next" />
<TextBox TextInputOptions.Multiline="True" AcceptsReturn="True" PlaceholderText="Text" Height="200" TextWrapping="Wrap"/>
<TextBox PlaceholderText="Username" TextInputOptions.ContentType="Email" TextInputOptions.ReturnKeyType="Done" />
<TextBox PlaceholderText="Password" PasswordChar="*" TextInputOptions.ContentType="Password" />
<TextBox PlaceholderText="Pin" PasswordChar="*" TextInputOptions.ContentType="Digits" TextInputOptions.ReturnKeyType="Next" />
<Button Content="Login" Command="{Binding ButtonCommand}" />
</StackPanel>
</UserControl>

2
src/Android/Avalonia.Android/Platform/AndroidInsetsManager.cs

@ -318,8 +318,6 @@ namespace Avalonia.Android.Platform
}
}
public bool DisplayEdgeToEdge { get => DisplaysEdgeToEdge; set => DisplayEdgeToEdgePreference = value; }
public bool DisplaysEdgeToEdge => _displaysEdgeToEdge;
internal void ApplyStatusBarState()

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

@ -26,7 +26,7 @@ using ClipboardManager = Android.Content.ClipboardManager;
namespace Avalonia.Android.Platform.SkiaPlatform
{
class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfoWithWaitPolicy
class TopLevelImpl : ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfoWithWaitPolicy
{
private readonly AndroidKeyboardEventsHelper<TopLevelImpl> _keyboardHelper;
private readonly AndroidMotionEventsHelper _pointerHelper;

6
src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs

@ -8,7 +8,7 @@ using Avalonia.Input.Raw;
namespace Avalonia.Android.Platform.Specific.Helpers
{
internal class AndroidKeyboardEventsHelper<TView> : IDisposable where TView : TopLevelImpl, IAndroidView
internal class AndroidKeyboardEventsHelper<TView> : IDisposable where TView : TopLevelImpl
{
private readonly TView _view;
@ -65,8 +65,8 @@ namespace Avalonia.Android.Platform.Specific.Helpers
AndroidKeyboardDevice.ConvertKey(e.KeyCode),
GetModifierKeys(e),
physicalKey,
keyDeviceType,
keySymbol);
keySymbol,
keyDeviceType);
_view.Input?.Invoke(rawKeyEvent);

11
src/Android/Avalonia.Android/Platform/Specific/IAndroidView.cs

@ -1,11 +0,0 @@
using System;
using Android.Views;
namespace Avalonia.Android.Platform.Specific
{
public interface IAndroidView
{
[Obsolete("Use TopLevel.TryGetPlatformHandle instead, which can be casted to AndroidViewControlHandle.")]
View View { get; }
}
}

13
src/Avalonia.Base/Animation/Animation.AnimatorRegistry.cs

@ -7,17 +7,6 @@ namespace Avalonia.Animation;
partial class Animation
{
/// <summary>
/// Sets the value of the Animator attached property for a setter.
/// </summary>
/// <param name="setter">The animation setter.</param>
/// <param name="value">The property animator value.</param>
[Obsolete("CustomAnimatorBase will be removed before 11.0, use InterpolatingAnimator<T>", true)]
public static void SetAnimator(IAnimationSetter setter, CustomAnimatorBase value)
{
s_animators[setter] = (value.WrapperType, value.CreateWrapper);
}
/// <summary>
/// Sets the value of the Animator attached property for a setter.
/// </summary>
@ -92,4 +81,4 @@ partial class Animation
return null;
}
}
}

17
src/Avalonia.Base/Animation/Easings/CubicBezierEasing.cs

@ -1,17 +0,0 @@
using System;
namespace Avalonia.Animation.Easings;
[Obsolete("Use SplineEasing instead")]
public sealed class CubicBezierEasing : IEasing
{
private CubicBezierEasing()
{
}
public Point ControlPoint2 { get; set; }
public Point ControlPoint1 { get; set; }
double IEasing.Ease(double progress)
=> throw new NotSupportedException();
}

30
src/Avalonia.Base/Animation/ICustomAnimator.cs

@ -2,34 +2,6 @@ using System;
using Avalonia.Animation.Animators;
namespace Avalonia.Animation;
[Obsolete("This class will be removed before 11.0, use InterpolatingAnimator<T>", true)]
public abstract class CustomAnimatorBase
{
internal abstract IAnimator CreateWrapper();
internal abstract Type WrapperType { get; }
}
[Obsolete("This class will be removed before 11.0, use InterpolatingAnimator<T>", true)]
public abstract class CustomAnimatorBase<T> : CustomAnimatorBase
{
public abstract T Interpolate(double progress, T oldValue, T newValue);
internal override Type WrapperType => typeof(AnimatorWrapper);
internal override IAnimator CreateWrapper() => new AnimatorWrapper(this);
internal class AnimatorWrapper : Animator<T>
{
private readonly CustomAnimatorBase<T> _parent;
public AnimatorWrapper(CustomAnimatorBase<T> parent)
{
_parent = parent;
}
public override T Interpolate(double progress, T oldValue, T newValue) => _parent.Interpolate(progress, oldValue, newValue);
}
}
public interface ICustomAnimator
{
internal IAnimator CreateWrapper();
@ -55,4 +27,4 @@ public abstract class InterpolatingAnimator<T> : ICustomAnimator
public override T Interpolate(double progress, T oldValue, T newValue) => _parent.Interpolate(progress, oldValue, newValue);
}
}
}

27
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -227,33 +227,6 @@ namespace Avalonia
};
}
/// <summary>
/// Binds a property on an <see cref="AvaloniaObject"/> to an <see cref="BindingBase"/>.
/// </summary>
/// <param name="target">The object.</param>
/// <param name="property">The property to bind.</param>
/// <param name="binding">The binding.</param>
/// <param name="anchor">
/// An optional anchor from which to locate required context. When binding to objects that
/// are not in the logical tree, certain types of binding need an anchor into the tree in
/// order to locate named controls or resources. The <paramref name="anchor"/> parameter
/// can be used to provide this context.
/// </param>
/// <returns>An <see cref="IDisposable"/> which can be used to cancel the binding.</returns>
[Obsolete("Use AvaloniaObject.Bind(AvaloniaProperty, IBinding")]
public static IDisposable Bind(
this AvaloniaObject target,
AvaloniaProperty property,
BindingBase binding,
object? anchor = null)
{
target = target ?? throw new ArgumentNullException(nameof(target));
property = property ?? throw new ArgumentNullException(nameof(property));
binding = binding ?? throw new ArgumentNullException(nameof(binding));
return target.Bind(property, binding);
}
/// <summary>
/// Gets a <see cref="AvaloniaProperty"/> value.
/// </summary>

5
src/Avalonia.Base/Data/BindingPriority.cs

@ -46,9 +46,6 @@ namespace Avalonia.Data
/// <summary>
/// The value is uninitialized.
/// </summary>
Unset = int.MaxValue,
[Obsolete("Use Template priority"), EditorBrowsable(EditorBrowsableState.Never)]
TemplatedParent = Template,
Unset = int.MaxValue
}
}

91
src/Avalonia.Base/Data/CompiledBinding.cs

@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq.Expressions;
using Avalonia.Controls;
using Avalonia.Data.Converters;
using Avalonia.Data.Core;
@ -27,6 +29,95 @@ public class CompiledBinding : BindingBase
/// <param name="path">The binding path.</param>
public CompiledBinding(CompiledBindingPath path) => Path = path;
/// <summary>
/// Creates a <see cref="CompiledBinding"/> from a lambda expression.
/// </summary>
/// <typeparam name="TIn">The input type of the binding expression.</typeparam>
/// <typeparam name="TOut">The output type of the binding expression.</typeparam>
/// <param name="expression">
/// The lambda expression representing the binding path
/// (e.g., <c>vm => vm.PropertyName</c>).
/// </param>
/// <param name="source"
/// >The source object for the binding. If null, uses the target's DataContext.
/// </param>
/// <param name="converter">
/// Optional value converter to transform values between source and target.
/// </param>
/// <param name="mode">
/// The binding mode. Default is <see cref="BindingMode.Default"/> which resolves to the
/// property's default binding mode.
/// </param>
/// <param name="priority">The binding priority.</param>
/// <param name="converterCulture">The culture in which to evaluate the converter.</param>
/// <param name="converterParameter">A parameter to pass to the converter.</param>
/// <param name="fallbackValue">
/// The value to use when the binding is unable to produce a value.
/// </param>
/// <param name="stringFormat">The string format for the binding result.</param>
/// <param name="targetNullValue">The value to use when the binding result is null.</param>
/// <param name="updateSourceTrigger">
/// The timing of binding source updates for TwoWay/OneWayToSource bindings.
/// </param>
/// <param name="delay">
/// The amount of time, in milliseconds, to wait before updating the binding source.
/// </param>
/// <returns>
/// A configured <see cref="CompiledBinding"/> instance ready to be applied to a property.
/// </returns>
/// <exception cref="ExpressionParseException">
/// Thrown when the expression contains unsupported operations or invalid syntax for binding
/// expressions.
/// </exception>
/// <remarks>
/// This builds a <see cref="CompiledBinding"/> with a path described by a lambda expression.
/// The resulting binding avoids reflection for property access, providing better performance
/// than reflection-based bindings.
///
/// Supported expressions include:
/// <list type="bullet">
/// <item>Property access: <c>x => x.Property</c></item>
/// <item>Nested properties: <c>x => x.Property.Nested</c></item>
/// <item>Indexers: <c>x => x.Items[0]</c></item>
/// <item>Type casts: <c>x => ((DerivedType)x).Property</c></item>
/// <item>Logical NOT: <c>x => !x.BoolProperty</c></item>
/// <item>Stream bindings: <c>x => x.TaskProperty</c> (Task/Observable)</item>
/// <item>AvaloniaProperty access: <c>x => x[MyProperty]</c></item>
/// </list>
/// </remarks>
[RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)]
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
public static CompiledBinding Create<TIn, TOut>(
Expression<Func<TIn, TOut>> expression,
object? source = null,
IValueConverter? converter = null,
BindingMode mode = BindingMode.Default,
BindingPriority priority = BindingPriority.LocalValue,
CultureInfo? converterCulture = null,
object? converterParameter = null,
object? fallbackValue = null,
string? stringFormat = null,
object? targetNullValue = null,
UpdateSourceTrigger updateSourceTrigger = UpdateSourceTrigger.Default,
int delay = 0)
{
var path = BindingExpressionVisitor<TIn>.BuildPath(expression);
return new CompiledBinding(path)
{
Source = source ?? AvaloniaProperty.UnsetValue,
Converter = converter,
ConverterCulture = converterCulture,
ConverterParameter = converterParameter,
FallbackValue = fallbackValue ?? AvaloniaProperty.UnsetValue,
Mode = mode,
Priority = priority,
StringFormat = stringFormat,
TargetNullValue = targetNullValue ?? AvaloniaProperty.UnsetValue,
UpdateSourceTrigger = updateSourceTrigger,
Delay = delay
};
}
/// <summary>
/// Gets or sets the amount of time, in milliseconds, to wait before updating the binding
/// source after the value on the target changes.

6
src/Avalonia.Base/Data/CompiledBindingPath.cs

@ -203,12 +203,6 @@ namespace Avalonia.Data
return this;
}
[Obsolete("This method doesn't do anything anymore. Use Binding.Source instead.")]
public CompiledBindingPathBuilder SetRawSource(object? rawSource)
{
return this;
}
public CompiledBindingPath Build() => new CompiledBindingPath(_elements.ToArray());
}

53
src/Avalonia.Base/Data/Core/BindingExpression.cs

@ -173,59 +173,6 @@ internal class BindingExpression : UntypedBindingExpressionBase, IDescription, I
_nodes[0].SetSource(source, null);
}
/// <summary>
/// Creates an <see cref="BindingExpression"/> from an expression tree.
/// </summary>
/// <typeparam name="TIn">The input type of the binding expression.</typeparam>
/// <typeparam name="TOut">The output type of the binding expression.</typeparam>
/// <param name="source">The source from which the binding value will be read.</param>
/// <param name="expression">The expression representing the binding path.</param>
/// <param name="converter">The converter to use.</param>
/// <param name="converterCulture">The converter culture to use.</param>
/// <param name="converterParameter">The converter parameter.</param>
/// <param name="enableDataValidation">Whether data validation should be enabled for the binding.</param>
/// <param name="fallbackValue">The fallback value.</param>
/// <param name="mode">The binding mode.</param>
/// <param name="priority">The binding priority.</param>
/// <param name="targetNullValue">The null target value.</param>
/// <param name="allowReflection">Whether to allow reflection for target type conversion.</param>
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
#if NET8_0_OR_GREATER
[RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)]
#endif
internal static BindingExpression Create<TIn, TOut>(
TIn source,
Expression<Func<TIn, TOut>> expression,
IValueConverter? converter = null,
CultureInfo? converterCulture = null,
object? converterParameter = null,
bool enableDataValidation = false,
Optional<object?> fallbackValue = default,
BindingMode mode = BindingMode.OneWay,
BindingPriority priority = BindingPriority.LocalValue,
object? targetNullValue = null,
bool allowReflection = true)
where TIn : class?
{
var nodes = BindingExpressionVisitor<TIn>.BuildNodes(expression, enableDataValidation);
var fallback = fallbackValue.HasValue ? fallbackValue.Value : AvaloniaProperty.UnsetValue;
return new BindingExpression(
source,
nodes,
fallback,
converter: converter,
converterCulture: converterCulture,
converterParameter: converterParameter,
enableDataValidation: enableDataValidation,
mode: mode,
priority: priority,
targetNullValue: targetNullValue,
targetTypeConverter: allowReflection ?
TargetTypeConverter.GetReflectionConverter() :
TargetTypeConverter.GetDefaultConverter());
}
/// <summary>
/// Called by an <see cref="ExpressionNode"/> belonging to this binding when its
/// <see cref="ExpressionNode.Value"/> changes.

402
src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitor.cs

@ -1,47 +1,59 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
using Avalonia.Data.Core.ExpressionNodes;
using Avalonia.Data.Core.ExpressionNodes.Reflection;
using Avalonia.Data.Core.Plugins;
using Avalonia.Reactive;
using Avalonia.Utilities;
namespace Avalonia.Data.Core.Parsers;
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
#if NET8_0_OR_GREATER
/// <summary>
/// Visits and processes a LINQ expression to build a compiled binding path.
/// </summary>
/// <typeparam name="TIn">The input parameter type for the binding expression.</typeparam>
/// <remarks>
/// This visitor traverses lambda expressions used in compiled bindings and uses
/// <see cref="CompiledBindingPathBuilder"/> to construct a <see cref="CompiledBindingPath"/>, which
/// can then be converted into <see cref="ExpressionNode"/> instances. It supports property access,
/// indexers, AvaloniaProperty access, stream bindings, type casts, and logical operators.
/// </remarks>
[RequiresDynamicCode(TrimmingMessages.ExpressionNodeRequiresDynamicCodeMessage)]
#endif
internal class BindingExpressionVisitor<TIn> : ExpressionVisitor
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
internal class BindingExpressionVisitor<TIn>(LambdaExpression expression) : ExpressionVisitor
{
private static readonly PropertyInfo AvaloniaObjectIndexer;
private static readonly MethodInfo CreateDelegateMethod;
private const string IndexerGetterName = "get_Item";
private const string MultiDimensionalArrayGetterMethodName = "Get";
private readonly bool _enableDataValidation;
private readonly LambdaExpression _rootExpression;
private readonly List<ExpressionNode> _nodes = new();
private readonly LambdaExpression _rootExpression = expression;
private readonly CompiledBindingPathBuilder _builder = new();
private Expression? _head;
public BindingExpressionVisitor(LambdaExpression expression, bool enableDataValidation)
{
_rootExpression = expression;
_enableDataValidation = enableDataValidation;
}
static BindingExpressionVisitor()
{
AvaloniaObjectIndexer = typeof(AvaloniaObject).GetProperty("Item", new[] { typeof(AvaloniaProperty) })!;
CreateDelegateMethod = typeof(MethodInfo).GetMethod("CreateDelegate", new[] { typeof(Type), typeof(object) })!;
}
public static List<ExpressionNode> BuildNodes<TOut>(Expression<Func<TIn, TOut>> expression, bool enableDataValidation)
/// <summary>
/// Builds a compiled binding path from a lambda expression.
/// </summary>
/// <typeparam name="TOut">The output type of the binding expression.</typeparam>
/// <param name="expression">
/// The lambda expression to parse and convert into a binding path.
/// </param>
/// <returns>
/// A <see cref="CompiledBindingPath"/> representing the binding path.
/// </returns>
/// <exception cref="ExpressionParseException">
/// Thrown when the expression contains unsupported operations or invalid syntax for binding
/// expressions.
/// </exception>
public static CompiledBindingPath BuildPath<TOut>(Expression<Func<TIn, TOut>> expression)
{
var visitor = new BindingExpressionVisitor<TIn>(expression, enableDataValidation);
var visitor = new BindingExpressionVisitor<TIn>(expression);
visitor.Visit(expression);
return visitor._nodes;
return visitor._builder.Build();
}
protected override Expression VisitBinary(BinaryExpression node)
@ -49,33 +61,64 @@ internal class BindingExpressionVisitor<TIn> : ExpressionVisitor
// Indexers require more work since the compiler doesn't generate IndexExpressions:
// they weren't in System.Linq.Expressions v1 and so must be generated manually.
if (node.NodeType == ExpressionType.ArrayIndex)
return Visit(Expression.MakeIndex(node.Left, null, new[] { node.Right }));
return Visit(Expression.MakeIndex(node.Left, null, [node.Right]));
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
protected override Expression VisitIndex(IndexExpression node)
{
if (node.Indexer == AvaloniaObjectIndexer)
if (node.Indexer == BindingExpressionVisitorMembers.AvaloniaObjectIndexer)
{
var property = GetValue<AvaloniaProperty>(node.Arguments[0]);
return Add(node.Object, node, new AvaloniaPropertyAccessorNode(property));
return Add(node.Object, node, x => x.Property(property, CreateAvaloniaPropertyAccessor));
}
else
else if (node.Object?.Type.IsArray == true)
{
var indexes = node.Arguments.Select(GetValue<int>).ToArray();
return Add(node.Object, node, x => x.ArrayElement(indexes, node.Type));
}
else if (node.Indexer?.GetMethod is not null &&
node.Arguments.Count == 1 &&
node.Arguments[0].Type == typeof(int))
{
return Add(node.Object, node, new ExpressionTreeIndexerNode(node));
var getMethod = node.Indexer.GetMethod;
var setMethod = node.Indexer.SetMethod;
var index = GetValue<int>(node.Arguments[0]);
var info = new ClrPropertyInfo(
CommonPropertyNames.IndexerName,
x => getMethod.Invoke(x, new object[] { index }),
setMethod is not null ? (o, v) => setMethod.Invoke(o, new[] { index, v }) : null,
getMethod.ReturnType);
return Add(node.Object, node, x => x.Property(
info,
(weakRef, propInfo) => CreateIndexerPropertyAccessor(weakRef, propInfo, index)));
}
else if (node.Indexer?.GetMethod is not null)
{
var getMethod = node.Indexer.GetMethod;
var setMethod = node.Indexer?.SetMethod;
var indexes = node.Arguments.Select(GetValue<object>).ToArray();
var info = new ClrPropertyInfo(
CommonPropertyNames.IndexerName,
x => getMethod.Invoke(x, indexes),
setMethod is not null ? (o, v) => setMethod.Invoke(o, indexes.Append(v).ToArray()) : null,
getMethod.ReturnType);
return Add(node.Object, node, x => x.Property(
info,
CreateInpcPropertyAccessor));
}
throw new ExpressionParseException(0, $"Invalid indexer in binding expression: {node.NodeType}.");
}
protected override Expression VisitMember(MemberExpression node)
{
switch (node.Member.MemberType)
return node.Member.MemberType switch
{
case MemberTypes.Property:
return Add(node.Expression, node, new DynamicPluginPropertyAccessorNode(node.Member.Name, acceptsNull: false));
default:
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
MemberTypes.Property => AddPropertyNode(node),
_ => throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}."),
};
}
protected override Expression VisitMethodCall(MethodCallExpression node)
@ -90,20 +133,43 @@ internal class BindingExpressionVisitor<TIn> : ExpressionVisitor
else if (method.Name == MultiDimensionalArrayGetterMethodName &&
node.Object is not null)
{
var expression = Expression.MakeIndex(node.Object, null, node.Arguments);
return Add(node.Object, node, new ExpressionTreeIndexerNode(expression));
var indexes = node.Arguments.Select(GetValue<int>).ToArray();
return Add(node.Object, node, x => x.ArrayElement(indexes, node.Type));
}
else if (method.Name.StartsWith(StreamBindingExtensions.StreamBindingName) &&
method.DeclaringType == typeof(StreamBindingExtensions))
{
var instance = node.Method.IsStatic ? node.Arguments[0] : node.Object;
Add(instance, node, new DynamicPluginStreamNode());
return node;
var instanceType = instance?.Type;
var genericArgs = method.GetGenericArguments();
var genericArg = genericArgs.Length > 0 ? genericArgs[0] : typeof(object);
if (instanceType == typeof(Task) ||
(instanceType?.IsGenericType == true &&
instanceType.GetGenericTypeDefinition() == typeof(Task<>) &&
genericArg.IsAssignableFrom(instanceType.GetGenericArguments()[0])))
{
var builderMethod = typeof(CompiledBindingPathBuilder)
.GetMethod(nameof(CompiledBindingPathBuilder.StreamTask))!
.MakeGenericMethod(genericArg);
return Add(instance, node, x => builderMethod.Invoke(x, null));
}
else if (typeof(IObservable<>).MakeGenericType(genericArg).IsAssignableFrom(instance?.Type))
{
var builderMethod = typeof(CompiledBindingPathBuilder)
.GetMethod(nameof(CompiledBindingPathBuilder.StreamObservable))!
.MakeGenericMethod(genericArg);
return Add(instance, node, x => builderMethod.Invoke(x, null));
}
}
else if (method == CreateDelegateMethod)
else if (method == BindingExpressionVisitorMembers.CreateDelegateMethod)
{
var accessor = new DynamicPluginPropertyAccessorNode(GetValue<MethodInfo>(node.Object!).Name, acceptsNull: false);
return Add(node.Arguments[1], node, accessor);
var methodInfo = GetValue<MethodInfo>(node.Object!);
var delegateType = GetValue<Type>(node.Arguments[0]);
return Add(node.Arguments[1], node, x => x.Method(
methodInfo.MethodHandle,
delegateType.TypeHandle,
acceptsNull: false));
}
throw new ExpressionParseException(0, $"Invalid method call in binding expression: '{node.Method.DeclaringType}.{node.Method.Name}'.");
@ -120,20 +186,26 @@ internal class BindingExpressionVisitor<TIn> : ExpressionVisitor
{
if (node.NodeType == ExpressionType.Not && node.Type == typeof(bool))
{
return Add(node.Operand, node, new LogicalNotNode());
return Add(node.Operand, node, x => x.Not());
}
else if (node.NodeType == ExpressionType.Convert)
{
if (node.Operand.Type.IsAssignableFrom(node.Type))
// Allow reference type casts (both upcasts and downcasts) but reject value type conversions
if (!node.Type.IsValueType && !node.Operand.Type.IsValueType &&
(node.Type.IsAssignableFrom(node.Operand.Type) || node.Operand.Type.IsAssignableFrom(node.Type)))
{
// Ignore inheritance casts
return _head = base.VisitUnary(node);
var castMethod = typeof(CompiledBindingPathBuilder)
.GetMethod(nameof(CompiledBindingPathBuilder.TypeCast))!
.MakeGenericMethod(node.Type);
return Add(node.Operand, node, x => castMethod.Invoke(x, null));
}
}
else if (node.NodeType == ExpressionType.TypeAs)
{
// Ignore as operator.
return _head = base.VisitUnary(node);
var castMethod = typeof(CompiledBindingPathBuilder)
.GetMethod(nameof(CompiledBindingPathBuilder.TypeCast))!
.MakeGenericMethod(node.Type);
return Add(node.Operand, node, x => castMethod.Invoke(x, null));
}
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
@ -146,7 +218,7 @@ internal class BindingExpressionVisitor<TIn> : ExpressionVisitor
protected override CatchBlock VisitCatchBlock(CatchBlock node)
{
throw new ExpressionParseException(0, $"Catch blocks are not allowed in binding expressions.");
throw new ExpressionParseException(0, "Catch blocks are not allowed in binding expressions.");
}
protected override Expression VisitConditional(ConditionalExpression node)
@ -156,17 +228,17 @@ internal class BindingExpressionVisitor<TIn> : ExpressionVisitor
protected override Expression VisitDynamic(DynamicExpression node)
{
throw new ExpressionParseException(0, $"Dynamic expressions are not allowed in binding expressions.");
throw new ExpressionParseException(0, "Dynamic expressions are not allowed in binding expressions.");
}
protected override ElementInit VisitElementInit(ElementInit node)
{
throw new ExpressionParseException(0, $"Element init expressions are not valid in a binding expression.");
throw new ExpressionParseException(0, "Element init expressions are not valid in a binding expression.");
}
protected override Expression VisitGoto(GotoExpression node)
{
throw new ExpressionParseException(0, $"Goto expressions not supported in binding expressions.");
throw new ExpressionParseException(0, "Goto expressions are not supported in binding expressions.");
}
protected override Expression VisitInvocation(InvocationExpression node)
@ -191,7 +263,7 @@ internal class BindingExpressionVisitor<TIn> : ExpressionVisitor
protected override MemberAssignment VisitMemberAssignment(MemberAssignment node)
{
throw new ExpressionParseException(0, $"Member assignments not supported in binding expressions.");
throw new ExpressionParseException(0, "Member assignments not supported in binding expressions.");
}
protected override Expression VisitSwitch(SwitchExpression node)
@ -209,17 +281,69 @@ internal class BindingExpressionVisitor<TIn> : ExpressionVisitor
throw new ExpressionParseException(0, $"Invalid expression type in binding expression: {node.NodeType}.");
}
private Expression Add(Expression? instance, Expression expression, ExpressionNode node)
private Expression Add(Expression? instance, Expression expression, Action<CompiledBindingPathBuilder> build)
{
var visited = Visit(instance);
if (visited != _head)
{
throw new ExpressionParseException(
0,
0,
$"Unable to parse '{expression}': expected an instance of '{_head}' but got '{visited}'.");
_nodes.Add(node);
}
build(_builder);
return _head = expression;
}
private Expression AddPropertyNode(MemberExpression node)
{
// Check if it's an AvaloniaProperty accessed via CLR wrapper
if (typeof(AvaloniaObject).IsAssignableFrom(node.Expression?.Type) &&
AvaloniaPropertyRegistry.Instance.FindRegistered(node.Expression.Type, node.Member.Name) is { } avaloniaProperty)
{
return Add(
node.Expression,
node,
x => x.Property(avaloniaProperty, CreateAvaloniaPropertyAccessor));
}
else
{
var property = (PropertyInfo)node.Member;
var info = new ClrPropertyInfo(
property.Name,
CreateGetter(property),
CreateSetter(property),
property.PropertyType);
return Add(node.Expression, node, x => x.Property(info, CreateInpcPropertyAccessor));
}
}
private static Func<object, object>? CreateGetter(PropertyInfo info)
{
if (info.GetMethod == null)
return null;
var target = Expression.Parameter(typeof(object), "target");
return Expression.Lambda<Func<object, object>>(
Expression.Convert(Expression.Call(Expression.Convert(target, info.DeclaringType!), info.GetMethod),
typeof(object)),
target)
.Compile();
}
private static Action<object, object?>? CreateSetter(PropertyInfo info)
{
if (info.SetMethod == null)
return null;
var target = Expression.Parameter(typeof(object), "target");
var value = Expression.Parameter(typeof(object), "value");
return Expression.Lambda<Action<object, object?>>(
Expression.Call(Expression.Convert(target, info.DeclaringType!), info.SetMethod,
Expression.Convert(value, info.SetMethod.GetParameters()[0].ParameterType)),
target, value)
.Compile();
}
private static T GetValue<T>(Expression expr)
{
if (expr is ConstantExpression constant)
@ -232,4 +356,168 @@ internal class BindingExpressionVisitor<TIn> : ExpressionVisitor
var type = method.DeclaringType;
return type?.GetRuntimeProperties().FirstOrDefault(prop => prop.GetMethod == method);
}
// Accessor factory methods
private static IPropertyAccessor CreateInpcPropertyAccessor(WeakReference<object?> target, IPropertyInfo property)
=> new InpcPropertyAccessor(target, property);
private static IPropertyAccessor CreateAvaloniaPropertyAccessor(WeakReference<object?> target, IPropertyInfo property)
=> new AvaloniaPropertyAccessor(
new WeakReference<AvaloniaObject?>((AvaloniaObject?)(target.TryGetTarget(out var o) ? o : null)),
(AvaloniaProperty)property);
private static IPropertyAccessor CreateIndexerPropertyAccessor(WeakReference<object?> target, IPropertyInfo property, int argument)
=> new IndexerAccessor(target, property, argument);
// Accessor implementations
private class AvaloniaPropertyAccessor : PropertyAccessorBase, IWeakEventSubscriber<AvaloniaPropertyChangedEventArgs>
{
private readonly WeakReference<AvaloniaObject?> _reference;
private readonly AvaloniaProperty _property;
public AvaloniaPropertyAccessor(WeakReference<AvaloniaObject?> reference, AvaloniaProperty property)
{
_reference = reference ?? throw new ArgumentNullException(nameof(reference));
_property = property ?? throw new ArgumentNullException(nameof(property));
}
public override Type PropertyType => _property.PropertyType;
public override object? Value => _reference.TryGetTarget(out var instance) ? instance?.GetValue(_property) : null;
public override bool SetValue(object? value, BindingPriority priority)
{
if (!_property.IsReadOnly && _reference.TryGetTarget(out var instance))
{
instance.SetValue(_property, value, priority);
return true;
}
return false;
}
public void OnEvent(object? sender, WeakEvent ev, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == _property)
PublishValue(Value);
}
protected override void SubscribeCore()
{
if (_reference.TryGetTarget(out var reference) && reference is not null)
{
PublishValue(reference.GetValue(_property));
WeakEvents.AvaloniaPropertyChanged.Subscribe(reference, this);
}
}
protected override void UnsubscribeCore()
{
if (_reference.TryGetTarget(out var reference) && reference is not null)
WeakEvents.AvaloniaPropertyChanged.Unsubscribe(reference, this);
}
}
private class InpcPropertyAccessor : PropertyAccessorBase, IWeakEventSubscriber<PropertyChangedEventArgs>
{
protected readonly WeakReference<object?> _reference;
private readonly IPropertyInfo _property;
public InpcPropertyAccessor(WeakReference<object?> reference, IPropertyInfo property)
{
_reference = reference ?? throw new ArgumentNullException(nameof(reference));
_property = property ?? throw new ArgumentNullException(nameof(property));
}
public override Type PropertyType => _property.PropertyType;
public override object? Value => _reference.TryGetTarget(out var o) ? _property.Get(o) : null;
public override bool SetValue(object? value, BindingPriority priority)
{
if (_property.CanSet && _reference.TryGetTarget(out var o))
{
_property.Set(o, value);
SendCurrentValue();
return true;
}
return false;
}
public void OnEvent(object? sender, WeakEvent ev, PropertyChangedEventArgs e)
{
if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName))
SendCurrentValue();
}
protected override void SubscribeCore()
{
SendCurrentValue();
if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc)
WeakEvents.ThreadSafePropertyChanged.Subscribe(inpc, this);
}
protected override void UnsubscribeCore()
{
if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc)
WeakEvents.ThreadSafePropertyChanged.Unsubscribe(inpc, this);
}
protected void SendCurrentValue()
{
try
{
PublishValue(Value);
}
catch (Exception e)
{
PublishValue(new BindingNotification(e, BindingErrorType.Error));
}
}
}
private class IndexerAccessor : InpcPropertyAccessor, IWeakEventSubscriber<NotifyCollectionChangedEventArgs>
{
private readonly int _index;
public IndexerAccessor(WeakReference<object?> target, IPropertyInfo basePropertyInfo, int argument)
: base(target, basePropertyInfo)
{
_index = argument;
}
protected override void SubscribeCore()
{
base.SubscribeCore();
if (_reference.TryGetTarget(out var o) && o is INotifyCollectionChanged incc)
WeakEvents.CollectionChanged.Subscribe(incc, this);
}
protected override void UnsubscribeCore()
{
base.UnsubscribeCore();
if (_reference.TryGetTarget(out var o) && o is INotifyCollectionChanged incc)
WeakEvents.CollectionChanged.Unsubscribe(incc, this);
}
public void OnEvent(object? sender, WeakEvent ev, NotifyCollectionChangedEventArgs args)
{
if (ShouldNotifyListeners(args))
SendCurrentValue();
}
private bool ShouldNotifyListeners(NotifyCollectionChangedEventArgs e)
{
return e.Action switch
{
NotifyCollectionChangedAction.Add => _index >= e.NewStartingIndex,
NotifyCollectionChangedAction.Remove => _index >= e.OldStartingIndex,
NotifyCollectionChangedAction.Replace => _index >= e.NewStartingIndex &&
_index < e.NewStartingIndex + e.NewItems!.Count,
NotifyCollectionChangedAction.Move => (_index >= e.NewStartingIndex &&
_index < e.NewStartingIndex + e.NewItems!.Count) ||
(_index >= e.OldStartingIndex &&
_index < e.OldStartingIndex + e.OldItems!.Count),
NotifyCollectionChangedAction.Reset => true,
_ => false
};
}
}
}

20
src/Avalonia.Base/Data/Core/Parsers/BindingExpressionVisitorMembers.cs

@ -0,0 +1,20 @@
using System;
using System.Reflection;
namespace Avalonia.Data.Core.Parsers;
/// <summary>
/// Stores reflection members used by <see cref="BindingExpressionVisitor{TIn}"/> outside of the
/// generic class to avoid duplication for each generic instantiation.
/// </summary>
internal static class BindingExpressionVisitorMembers
{
static BindingExpressionVisitorMembers()
{
AvaloniaObjectIndexer = typeof(AvaloniaObject).GetProperty(CommonPropertyNames.IndexerName, [typeof(AvaloniaProperty)])!;
CreateDelegateMethod = typeof(MethodInfo).GetMethod(nameof(MethodInfo.CreateDelegate), [typeof(Type), typeof(object)])!;
}
public static readonly PropertyInfo AvaloniaObjectIndexer;
public static readonly MethodInfo CreateDelegateMethod;
}

37
src/Avalonia.Base/Diagnostics/StyleDiagnostics.cs

@ -1,37 +0,0 @@
using System;
using System.Collections.Generic;
using Avalonia.Metadata;
using Avalonia.Styling;
namespace Avalonia.Diagnostics;
[PrivateApi]
[Unstable("Use StyledElementExtensions.GetValueStoreDiagnostic() instead")]
public class StyleDiagnostics
{
/// <summary>
/// Currently applied styles.
/// </summary>
public IReadOnlyList<AppliedStyle> AppliedStyles { get; }
public StyleDiagnostics(IReadOnlyList<AppliedStyle> appliedStyles)
{
AppliedStyles = appliedStyles;
}
}
[PrivateApi]
[Unstable("Use StyledElementExtensions.GetValueStoreDiagnostic() instead")]
public sealed class AppliedStyle
{
private readonly StyleInstance _instance;
internal AppliedStyle(StyleInstance instance)
{
_instance = instance;
}
public bool HasActivator => _instance.HasActivator;
public bool IsActive => _instance.IsActive();
public StyleBase Style => (StyleBase)_instance.Source;
}

3
src/Avalonia.Base/Diagnostics/StyleValueFrameDiagnostic.cs

@ -39,7 +39,4 @@ internal class StyleValueFrameDiagnostic : IValueFrameDiagnostic
}
}
}
[Unstable("Compatibility with 11.x")]
public AppliedStyle AsAppliedStyle() => new AppliedStyle(_styleInstance);
}

24
src/Avalonia.Base/Diagnostics/StyledElementExtensions.cs

@ -1,24 +0,0 @@
using System;
using System.Linq;
using Avalonia.Metadata;
using Avalonia.Styling;
namespace Avalonia.Diagnostics;
/// <summary>
/// Defines diagnostic extensions on <see cref="StyledElement"/>s.
/// </summary>
[PrivateApi]
public static class StyledElementExtensions
{
[Obsolete("Use AvaloniaObjectExtensions.GetValueStoreDiagnostic instead", true)]
public static StyleDiagnostics GetStyleDiagnostics(this StyledElement styledElement)
{
var diagnostics = styledElement.GetValueStore().GetStoreDiagnostic();
return new StyleDiagnostics(diagnostics.AppliedFrames
.OfType<StyleValueFrameDiagnostic>()
.Select(f => f.AsAppliedStyle())
.ToArray());
}
}

61
src/Avalonia.Base/Input/DataFormats.cs

@ -1,56 +1,11 @@
using System;
using System.ComponentModel;
using Avalonia.Input.Platform;
namespace Avalonia.Input
{
public static class DataFormats
{
/// <summary>
/// Dataformat for plaintext
/// </summary>
[Obsolete($"Use {nameof(DataFormat)}.{nameof(DataFormat.Text)} instead.")]
public static readonly string Text = nameof(Text);
namespace Avalonia.Input;
/// <summary>
/// Dataformat for one or more files.
/// </summary>
[Obsolete($"Use {nameof(DataFormat)}.{nameof(DataFormat.File)} instead.")]
public static readonly string Files = nameof(Files);
/// <summary>
/// Dataformat for one or more filenames
/// </summary>
/// <remarks>
/// This data format is supported only on desktop platforms.
/// </remarks>
[Obsolete($"Use {nameof(DataFormat)}.{nameof(DataFormat.File)} instead."), EditorBrowsable(EditorBrowsableState.Never)]
public static readonly string FileNames = nameof(FileNames);
#pragma warning disable CS0618 // Type or member is obsolete
internal static DataFormat ToDataFormat(string format)
{
if (format == Text)
return DataFormat.Text;
if (format == Files || format == FileNames)
return DataFormat.File;
return DataFormat.CreateBytesPlatformFormat(format);
}
internal static string ToString(DataFormat format)
{
if (DataFormat.Text.Equals(format))
return Text;
if (DataFormat.File.Equals(format))
return Files;
return format.Identifier;
}
#pragma warning restore CS0618 // Type or member is obsolete
}
}
// TODO13: remove
/// <summary>
/// This class does not do anything anymore.
/// Use <see cref="DataFormat"/> instead.
/// </summary>
[Obsolete($"Use {nameof(DataFormat)} instead", true)]
public static class DataFormats;

45
src/Avalonia.Base/Input/DataObject.cs

@ -1,40 +1,11 @@
using System;
using System.Collections.Generic;
namespace Avalonia.Input
{
/// <summary>
/// Specific and mutable implementation of the IDataObject interface.
/// </summary>
[Obsolete($"Use {nameof(DataTransfer)} instead")]
public class DataObject : IDataObject
{
private readonly Dictionary<string, object> _items = new();
namespace Avalonia.Input;
/// <inheritdoc />
public bool Contains(string dataFormat)
{
return _items.ContainsKey(dataFormat);
}
/// <inheritdoc />
public object? Get(string dataFormat)
{
return _items.TryGetValue(dataFormat, out var item) ? item : null;
}
/// <inheritdoc />
public IEnumerable<string> GetDataFormats()
{
return _items.Keys;
}
/// <summary>
/// Sets a value to the internal store of the data object with <see cref="DataFormats"/> as a key.
/// </summary>
public void Set(string dataFormat, object value)
{
_items[dataFormat] = value;
}
}
}
// TODO13: remove
/// <summary>
/// This class does not do anything anymore.
/// Use <see cref="DataTransfer"/> instead.
/// </summary>
[Obsolete($"Use {nameof(DataTransfer)} instead", true)]
public sealed class DataObject;

54
src/Avalonia.Base/Input/DataObjectExtensions.cs

@ -1,54 +0,0 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Avalonia.Platform.Storage;
#pragma warning disable CS0618 // Type or member is obsolete
namespace Avalonia.Input
{
// TODO12: remove
public static class DataObjectExtensions
{
/// <summary>
/// Returns a list of files if the DataObject contains files or filenames.
/// <seealso cref="DataFormats.Files"/>.
/// </summary>
/// <returns>
/// Collection of storage items - files or folders. If format isn't available, returns null.
/// </returns>
public static IEnumerable<IStorageItem>? GetFiles(this IDataObject dataObject)
{
return dataObject.Get(DataFormats.Files) as IEnumerable<IStorageItem>;
}
/// <summary>
/// Returns a list of filenames if the DataObject contains filenames.
/// <seealso cref="DataFormats.FileNames"/>
/// </summary>
/// <returns>
/// Collection of file names. If format isn't available, returns null.
/// </returns>
[System.Obsolete("Use GetFiles, this method is supported only on desktop platforms."), EditorBrowsable(EditorBrowsableState.Never)]
public static IEnumerable<string>? GetFileNames(this IDataObject dataObject)
{
return (dataObject.Get(DataFormats.FileNames) as IEnumerable<string>)
?? dataObject.GetFiles()?
.Select(f => f.TryGetLocalPath())
.Where(p => !string.IsNullOrEmpty(p))
.OfType<string>();
}
/// <summary>
/// Returns the dragged text if the DataObject contains any text.
/// <seealso cref="DataFormats.Text"/>
/// </summary>
/// <returns>
/// A text string. If format isn't available, returns null.
/// </returns>
public static string? GetText(this IDataObject dataObject)
{
return dataObject.Get(DataFormats.Text) as string;
}
}
}

9
src/Avalonia.Base/Input/DataTransferExtensions.cs

@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Input.Platform;
using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
@ -14,11 +12,6 @@ namespace Avalonia.Input;
/// </summary>
public static class DataTransferExtensions
{
[Obsolete]
internal static IDataObject ToLegacyDataObject(this IDataTransfer dataTransfer)
=> (dataTransfer as DataObjectToDataTransferWrapper)?.DataObject
?? new DataTransferToDataObjectWrapper(dataTransfer);
/// <summary>
/// Gets whether a <see cref="IDataTransfer"/> supports a specific format.
/// </summary>

10
src/Avalonia.Base/Input/DragDrop.cs

@ -122,16 +122,6 @@ namespace Avalonia.Input
element.RemoveHandler(DropEvent, handler);
}
/// <summary>
/// Starts a dragging operation with the given <see cref="IDataObject"/> and returns the applied drop effect from the target.
/// <seealso cref="DataObject"/>
/// </summary>
[Obsolete($"Use {nameof(DoDragDropAsync)} instead.")]
public static Task<DragDropEffects> DoDragDrop(PointerEventArgs triggerEvent, IDataObject data, DragDropEffects allowedEffects)
{
return DoDragDropAsync(triggerEvent, new DataObjectToDataTransferWrapper(data), allowedEffects);
}
/// <summary>
/// Starts a dragging operation with the given <see cref="IDataTransfer"/> and returns the applied drop effect from the target.
/// <seealso cref="DataTransfer"/>

17
src/Avalonia.Base/Input/DragEventArgs.cs

@ -1,5 +1,4 @@
using System;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Metadata;
@ -9,16 +8,11 @@ namespace Avalonia.Input
{
private readonly Interactive _target;
private readonly Point _targetLocation;
[Obsolete] private IDataObject? _legacyDataObject;
public DragDropEffects DragEffects { get; set; }
public IDataTransfer DataTransfer { get; }
[Obsolete($"Use {nameof(DataTransfer)} instead.")]
public IDataObject Data
=> _legacyDataObject ??= DataTransfer.ToLegacyDataObject();
public KeyModifiers KeyModifiers { get; }
public Point GetPosition(Visual relativeTo)
@ -31,17 +25,6 @@ namespace Avalonia.Input
return _target.TranslatePoint(_targetLocation, relativeTo) ?? new Point(0, 0);
}
[Obsolete($"Use the constructor accepting a {nameof(IDataTransfer)} instance instead.")]
public DragEventArgs(
RoutedEvent<DragEventArgs> routedEvent,
IDataObject data,
Interactive target,
Point targetLocation,
KeyModifiers keyModifiers)
: this(routedEvent, new DataObjectToDataTransferWrapper(data), target, targetLocation, keyModifiers)
{
}
[Unstable("This constructor might be removed in 12.0. For unit testing, consider using DragDrop.DoDragDrop or IHeadlessWindow.DragDrop.")]
public DragEventArgs(
RoutedEvent<DragEventArgs> routedEvent,

32
src/Avalonia.Base/Input/IDataObject.cs

@ -1,32 +0,0 @@
using System;
using System.Collections.Generic;
namespace Avalonia.Input
{
/// <summary>
/// Interface to access information about the data of a drag-and-drop operation.
/// </summary>
[Obsolete($"Use {nameof(IDataTransfer)} or {nameof(IAsyncDataTransfer)} instead")]
public interface IDataObject
{
/// <summary>
/// Lists all formats which are present in the DataObject.
/// <seealso cref="DataFormats"/>
/// </summary>
IEnumerable<string> GetDataFormats();
/// <summary>
/// Checks whether a given DataFormat is present in this object
/// <seealso cref="DataFormats"/>
/// </summary>
bool Contains(string dataFormat);
/// <summary>
/// Tries to get the data of the given DataFormat.
/// </summary>
/// <returns>
/// Object data. If format isn't available, returns null.
/// </returns>
object? Get(string dataFormat);
}
}

2
src/Avalonia.Base/Input/MouseDevice.cs

@ -34,6 +34,8 @@ namespace Avalonia.Input
_pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
}
internal Pointer Pointer => _pointer;
internal static TMouseDevice GetOrCreatePrimary<TMouseDevice>() where TMouseDevice : MouseDevice, new()
{
if (_primary is TMouseDevice device)

55
src/Avalonia.Base/Input/Platform/Clipboard.cs

@ -1,9 +1,4 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Compatibility;
using Avalonia.Platform.Storage;
using System.Threading.Tasks;
namespace Avalonia.Input.Platform;
@ -15,12 +10,6 @@ internal sealed class Clipboard(IClipboardImpl clipboardImpl) : IClipboard
private readonly IClipboardImpl _clipboardImpl = clipboardImpl;
private IAsyncDataTransfer? _lastDataTransfer;
Task<string?> IClipboard.GetTextAsync()
=> this.TryGetTextAsync();
Task IClipboard.SetTextAsync(string? text)
=> this.SetValueAsync(DataFormat.Text, text);
public Task ClearAsync()
{
_lastDataTransfer?.Dispose();
@ -29,10 +18,6 @@ internal sealed class Clipboard(IClipboardImpl clipboardImpl) : IClipboard
return _clipboardImpl.ClearAsync();
}
[Obsolete($"Use {nameof(SetDataAsync)} instead.")]
Task IClipboard.SetDataObjectAsync(IDataObject data)
=> SetDataAsync(new DataObjectToDataTransferWrapper(data));
public Task SetDataAsync(IAsyncDataTransfer? dataTransfer)
{
if (dataTransfer is null)
@ -47,47 +32,9 @@ internal sealed class Clipboard(IClipboardImpl clipboardImpl) : IClipboard
public Task FlushAsync()
=> _clipboardImpl is IFlushableClipboardImpl flushable ? flushable.FlushAsync() : Task.CompletedTask;
async Task<string[]> IClipboard.GetFormatsAsync()
{
var dataTransfer = await TryGetDataAsync();
return dataTransfer is null ? [] : dataTransfer.Formats.Select(DataFormats.ToString).ToArray();
}
[Obsolete($"Use {nameof(TryGetDataAsync)} instead.")]
async Task<object?> IClipboard.GetDataAsync(string format)
{
// No ConfigureAwait(false) here: we want TryGetXxxAsync() below to be called on the initial thread.
using var dataTransfer = await TryGetDataAsync();
if (dataTransfer is null)
return null;
if (format == DataFormats.Text)
return await dataTransfer.TryGetTextAsync().ConfigureAwait(false);
if (format == DataFormats.Files)
return await dataTransfer.TryGetFilesAsync().ConfigureAwait(false);
if (format == DataFormats.FileNames)
{
return (await dataTransfer.TryGetFilesAsync().ConfigureAwait(false))
?.Select(file => file.TryGetLocalPath())
.Where(path => path is not null)
.ToArray();
}
return null;
}
public Task<IAsyncDataTransfer?> TryGetDataAsync()
=> _clipboardImpl.TryGetDataAsync();
[Obsolete($"Use {nameof(TryGetInProcessDataAsync)} instead.")]
async Task<IDataObject?> IClipboard.TryGetInProcessDataObjectAsync()
{
var dataTransfer = await TryGetInProcessDataAsync().ConfigureAwait(false);
return (dataTransfer as DataObjectToDataTransferWrapper)?.DataObject;
}
public async Task<IAsyncDataTransfer?> TryGetInProcessDataAsync()
{
if (_lastDataTransfer is null || _clipboardImpl is not IOwnedClipboardImpl ownedClipboardImpl)

76
src/Avalonia.Base/Input/Platform/DataObjectToDataTransferItemWrapper.cs

@ -1,76 +0,0 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using Avalonia.Compatibility;
namespace Avalonia.Input.Platform;
/// <summary>
/// Wraps a legacy <see cref="IDataObject"/> into a <see cref="IDataTransferItem"/>.
/// </summary>
[Obsolete]
internal sealed class DataObjectToDataTransferItemWrapper(
IDataObject dataObject,
DataFormat[] formats,
string[] formatStrings)
: PlatformDataTransferItem
{
private readonly IDataObject _dataObject = dataObject;
private readonly DataFormat[] _formats = formats;
private readonly string[] _formatStrings = formatStrings;
protected override DataFormat[] ProvideFormats()
=> _formats;
protected override object? TryGetRawCore(DataFormat format)
{
var index = Array.IndexOf(Formats, format);
if (index < 0)
return null;
// We should never have DataFormat.File here, it's been handled by DataObjectToDataTransferWrapper.
Debug.Assert(!DataFormat.File.Equals(format));
var formatString = _formatStrings[index];
var data = _dataObject.Get(formatString);
if (DataFormat.Text.Equals(format))
return Convert.ToString(data) ?? string.Empty;
if (format is DataFormat<string>)
return Convert.ToString(data);
if (format is DataFormat<byte[]>)
return ConvertLegacyDataToBytes(format, data);
return null;
}
private static byte[]? ConvertLegacyDataToBytes(DataFormat format, object? data)
{
switch (data)
{
case null:
return null;
case byte[] bytes:
return bytes;
case string str:
return OperatingSystemEx.IsWindows() || OperatingSystemEx.IsMacOS() || OperatingSystemEx.IsIOS() ?
Encoding.Unicode.GetBytes(str) :
Encoding.UTF8.GetBytes(str);
case Stream stream:
var length = (int)(stream.Length - stream.Position);
var buffer = new byte[length];
stream.ReadExactly(buffer, 0, length);
return buffer;
default:
return null;
}
}
}

95
src/Avalonia.Base/Input/Platform/DataObjectToDataTransferWrapper.cs

@ -1,95 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
namespace Avalonia.Input.Platform;
#pragma warning disable CS0618 // Type or member is obsolete: usages of IDataObject and DataFormats
// TODO12: remove
/// <summary>
/// Wraps a legacy <see cref="IDataObject"/> into a <see cref="IDataTransfer"/>.
/// </summary>
[Obsolete]
internal sealed class DataObjectToDataTransferWrapper(IDataObject dataObject)
: PlatformDataTransfer
{
public IDataObject DataObject { get; } = dataObject;
protected override DataFormat[] ProvideFormats()
=> DataObject.GetDataFormats().Select(DataFormats.ToDataFormat).Distinct().ToArray();
protected override PlatformDataTransferItem[] ProvideItems()
{
var items = new List<PlatformDataTransferItem>();
var nonFileFormats = new List<DataFormat>();
var nonFileFormatStrings = new List<string>();
var hasFiles = false;
foreach (var formatString in DataObject.GetDataFormats())
{
var format = DataFormats.ToDataFormat(formatString);
if (formatString == DataFormats.Files)
{
if (hasFiles)
continue;
// This is not ideal as we're reading the filenames ahead of time to generate the appropriate items.
// We don't really care about that for this legacy wrapper.
if (DataObject.Get(formatString) is IEnumerable<IStorageItem> storageItems)
{
hasFiles = true;
foreach (var storageItem in storageItems)
items.Add(PlatformDataTransferItem.Create(DataFormat.File, storageItem));
}
}
else if (formatString == DataFormats.FileNames)
{
if (hasFiles)
continue;
if (DataObject.Get(formatString) is IEnumerable<string> fileNames)
{
hasFiles = true;
foreach (var fileName in fileNames)
{
if (StorageProviderHelpers.TryCreateBclStorageItem(fileName) is { } storageItem)
items.Add(PlatformDataTransferItem.Create(DataFormat.File, storageItem));
}
}
}
else
{
nonFileFormats.Add(format);
nonFileFormatStrings.Add(formatString);
}
}
if (nonFileFormats.Count > 0)
{
Debug.Assert(nonFileFormats.Count == nonFileFormatStrings.Count);
// Single item containing all formats except for DataFormat.File.
items.Add(new DataObjectToDataTransferItemWrapper(
DataObject,
nonFileFormats.ToArray(),
nonFileFormatStrings.ToArray()));
}
return items.ToArray();
}
[SuppressMessage(
"ReSharper",
"SuspiciousTypeConversion.Global",
Justification = "IDisposable may be implemented externally by the IDataObject instance.")]
public override void Dispose()
=> (DataObject as IDisposable)?.Dispose();
}

42
src/Avalonia.Base/Input/Platform/DataTransferToDataObjectWrapper.cs

@ -1,42 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Platform.Storage;
namespace Avalonia.Input.Platform;
/// <summary>
/// Wraps a <see cref="IDataTransfer"/> into a legacy <see cref="IDataObject"/>.
/// </summary>
[Obsolete]
internal sealed class DataTransferToDataObjectWrapper(IDataTransfer dataTransfer) : IDataObject
{
public IDataTransfer DataTransfer { get; } = dataTransfer;
public IEnumerable<string> GetDataFormats()
=> DataTransfer.Formats.Select(DataFormats.ToString);
public bool Contains(string dataFormat)
=> DataTransfer.Contains(DataFormats.ToDataFormat(dataFormat));
public object? Get(string dataFormat)
{
if (dataFormat == DataFormats.Text)
return DataTransfer.TryGetText();
if (dataFormat == DataFormats.Files)
return DataTransfer.TryGetFiles();
if (dataFormat == DataFormats.FileNames)
{
return DataTransfer
.TryGetFiles()
?.Select(file => file.TryGetLocalPath())
.Where(path => path is not null)
.ToArray();
}
return null;
}
}

55
src/Avalonia.Base/Input/Platform/IClipboard.cs

@ -1,4 +1,3 @@
using System;
using System.Threading.Tasks;
using Avalonia.Metadata;
@ -10,41 +9,11 @@ namespace Avalonia.Input.Platform
[NotClientImplementable]
public interface IClipboard
{
// TODO12: remove, ClipboardExtensions.TryGetTextAsync exists
/// <summary>
/// Returns a string containing the text data on the clipboard.
/// </summary>
/// <returns>A string containing text data, or null if no corresponding text data is available.</returns>
[Obsolete($"Use {nameof(ClipboardExtensions)}.{nameof(ClipboardExtensions.TryGetTextAsync)} instead")]
Task<string?> GetTextAsync();
// TODO12: remove, ClipboardExtensions.SetTextAsync exists
/// <summary>
/// Places a text on the clipboard.
/// </summary>
/// <param name="text">The text value to set.</param>
/// <remarks>
/// <para>By calling this method, the clipboard will get cleared of any possible previous data.</para>
/// <para>
/// If <paramref name="text"/> is null or empty, nothing will get placed on the clipboard and this method
/// will be equivalent to <see cref="ClearAsync"/>.
/// </para>
/// </remarks>
Task SetTextAsync(string? text);
/// <summary>
/// Clears any data from the system clipboard.
/// </summary>
Task ClearAsync();
/// <summary>
/// Places a specified non-persistent data object on the system Clipboard.
/// </summary>
/// <param name="data">A data object (an object that implements <see cref="IDataObject"/>) to place on the system Clipboard.</param>
/// <exception cref="System.ArgumentNullException"><paramref name="data"/> is null.</exception>
[Obsolete($"Use {nameof(SetDataAsync)} instead.")]
Task SetDataObjectAsync(IDataObject data);
/// <summary>
/// Places a data object on the clipboard.
/// The data object is responsible for providing supported formats and data upon request.
@ -69,20 +38,6 @@ namespace Avalonia.Input.Platform
/// <remarks>This method is only supported on the Windows platform. This method will do nothing on other platforms.</remarks>
Task FlushAsync();
/// <summary>
/// Get list of available Clipboard format.
/// </summary>
[Obsolete($"Use {nameof(ClipboardExtensions.GetDataFormatsAsync)} instead.")]
Task<string[]> GetFormatsAsync();
/// <summary>
/// Retrieves data in a specified format from the Clipboard.
/// </summary>
/// <param name="format">A string that specifies the format of the data to retrieve. For a set of predefined data formats, see the <see cref="DataFormats"/> class.</param>
/// <returns></returns>
[Obsolete($"Use {nameof(TryGetDataAsync)} instead.")]
Task<object?> GetDataAsync(string format);
/// <summary>
/// Retrieves data from the clipboard.
/// </summary>
@ -95,16 +50,6 @@ namespace Avalonia.Input.Platform
/// </remarks>
Task<IAsyncDataTransfer?> TryGetDataAsync();
/// <summary>
/// If clipboard contains the IDataObject that was set by a previous call to <see cref="SetDataObjectAsync(Avalonia.Input.IDataObject)"/>,
/// return said IDataObject instance. Otherwise, return null.
/// Note that not every platform supports that method, on unsupported platforms this method will always return
/// null
/// </summary>
/// <returns></returns>
[Obsolete($"Use {nameof(TryGetInProcessDataAsync)} instead.")]
Task<IDataObject?> TryGetInProcessDataObjectAsync();
/// <summary>
/// Retrieves the exact instance of a <see cref="IAsyncDataTransfer"/> previously placed on the clipboard
/// by <see cref="SetDataAsync"/>, if any.

9
src/Avalonia.Base/Input/Platform/IPlatformDragSource.cs

@ -1,5 +1,4 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Input.Platform
@ -7,12 +6,6 @@ namespace Avalonia.Input.Platform
[NotClientImplementable]
public interface IPlatformDragSource
{
[Obsolete($"Use {nameof(DoDragDropAsync)} instead.")]
Task<DragDropEffects> DoDragDrop(
PointerEventArgs triggerEvent,
IDataObject data,
DragDropEffects allowedEffects);
Task<DragDropEffects> DoDragDropAsync(
PointerEventArgs triggerEvent,
IDataTransfer dataTransfer,

4
src/Avalonia.Base/Input/Pointer.cs

@ -77,6 +77,7 @@ namespace Avalonia.Input
if (oldVisual != null)
oldVisual.DetachedFromVisualTree -= OnCaptureDetached;
Captured = control;
CaptureSource = source;
if (source != CaptureSource.Platform)
PlatformCapture(control);
@ -115,6 +116,7 @@ namespace Avalonia.Input
public IInputElement? Captured { get; private set; }
public PointerType Type { get; }
public bool IsPrimary { get; }
/// <summary>
@ -124,6 +126,8 @@ namespace Avalonia.Input
public bool IsGestureRecognitionSkipped { get; set; }
internal CaptureSource CaptureSource { get; private set; } = CaptureSource.Platform;
public void Dispose()
{
if (Captured != null)

22
src/Avalonia.Base/Input/Raw/RawDragEvent.cs

@ -1,40 +1,20 @@
using System;
using Avalonia.Input.Platform;
using Avalonia.Metadata;
using Avalonia.Metadata;
namespace Avalonia.Input.Raw
{
[PrivateApi]
public class RawDragEvent : RawInputEventArgs
{
[Obsolete] private IDataObject? _legacyDataObject;
public Point Location { get; set; }
public IDataTransfer DataTransfer { get; }
[Obsolete($"Use {nameof(DataTransfer)} instead.")]
public IDataObject Data
=> _legacyDataObject ??= DataTransfer.ToLegacyDataObject();
public DragDropEffects Effects { get; set; }
public RawDragEventType Type { get; }
public KeyModifiers KeyModifiers { get; }
[Obsolete($"Use the constructor accepting a {nameof(IDataTransfer)} instance instead.")]
public RawDragEvent(IDragDropDevice inputDevice,
RawDragEventType type,
IInputRoot root,
Point location,
IDataObject data,
DragDropEffects effects,
RawInputModifiers modifiers)
: this(inputDevice, type, root, location, new DataObjectToDataTransferWrapper(data), effects, modifiers)
{
}
public RawDragEvent(
IDragDropDevice inputDevice,
RawDragEventType type,

33
src/Avalonia.Base/Input/Raw/RawKeyEventArgs.cs

@ -1,4 +1,3 @@
using System;
using Avalonia.Metadata;
namespace Avalonia.Input.Raw
@ -12,21 +11,6 @@ namespace Avalonia.Input.Raw
[PrivateApi]
public class RawKeyEventArgs : RawInputEventArgs
{
[Obsolete("Use the overload that takes a physical key and key symbol instead.")]
public RawKeyEventArgs(
IKeyboardDevice device,
ulong timestamp,
IInputRoot root,
RawKeyEventType type,
Key key,
RawInputModifiers modifiers)
: this(device, timestamp, root, type, key, modifiers, PhysicalKey.None, KeyDeviceType.Keyboard, null)
{
Key = key;
Type = type;
Modifiers = modifiers;
}
public RawKeyEventArgs(
IInputDevice device,
ulong timestamp,
@ -35,24 +19,13 @@ namespace Avalonia.Input.Raw
Key key,
RawInputModifiers modifiers,
PhysicalKey physicalKey,
string? keySymbol)
: this(device, timestamp, root, type, key, modifiers, physicalKey, KeyDeviceType.Keyboard, keySymbol) { }
public RawKeyEventArgs(
IInputDevice device,
ulong timestamp,
IInputRoot root,
RawKeyEventType type,
Key key,
RawInputModifiers modifiers,
PhysicalKey physicalKey,
KeyDeviceType keyDeviceType,
string? keySymbol)
string? keySymbol,
KeyDeviceType keyDeviceType = KeyDeviceType.Keyboard)
: base(device, timestamp, root)
{
Type = type;
Key = key;
Modifiers = modifiers;
Type = type;
PhysicalKey = physicalKey;
KeySymbol = keySymbol;
KeyDeviceType = keyDeviceType;

12
src/Avalonia.Base/Layout/Layoutable.cs

@ -951,17 +951,5 @@ namespace Avalonia.Layout
{
return new Size(Math.Max(size.Width, 0), Math.Max(size.Height, 0));
}
internal override void SynchronizeCompositionProperties()
{
base.SynchronizeCompositionProperties();
if (CompositionVisual is { } visual)
{
// If the visual isn't using layout rounding, it's possible that antialiasing renders to pixels
// outside the current bounds. Extend the dirty rect by 1px in all directions in this case.
visual.ShouldExtendDirtyRect = !UseLayoutRounding;
}
}
}
}

114
src/Avalonia.Base/Media/BitmapCache.cs

@ -0,0 +1,114 @@
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Server;
namespace Avalonia.Media;
/// <summary>
/// Represents the behavior of caching a visual element or tree of elements as bitmap surfaces.
/// </summary>
public class BitmapCache : CacheMode
{
private CompositionBitmapCache? _current;
public static readonly StyledProperty<double> RenderAtScaleProperty = AvaloniaProperty.Register<BitmapCache, double>(
nameof(RenderAtScale), 1);
/// <summary>
/// Use the RenderAtScale property to render the BitmapCache at a multiple of the normal bitmap size.
/// The normal size is determined by the local size of the element.
///
/// Values greater than 1 increase the resolution of the bitmap relative to the native resolution of the element,
/// and values less than 1 decrease the resolution.
/// For example, if the RenderAtScale property is set to 2.0, and you apply a scale transform that
/// enlarges the content by a factor of 2, the content will have the same visual quality as the same content
/// with RenderAtScale set to 1.0 and a transform scale of 1.
///
/// When RenderAtScale is set to 0, no bitmap is rendered. Negative values are clamped to 0.
///
/// If you change this value, the cache is regenerated at the appropriate new resolution.
/// </summary>
public double RenderAtScale
{
get => GetValue(RenderAtScaleProperty);
set => SetValue(RenderAtScaleProperty, value);
}
public static readonly StyledProperty<bool> SnapsToDevicePixelsProperty = AvaloniaProperty.Register<BitmapCache, bool>(
nameof(SnapsToDevicePixels));
/// <summary>
/// Set the SnapsToDevicePixels property when the cache displays content that requires pixel-alignment to render correctly.
/// This is the case for text with subpixel antialiasing. If you set the EnableClearType property to true,
/// consider setting SnapsToDevicePixels to true to ensure proper rendering.
///
/// When the SnapsToDevicePixels property is set to false,
/// you can move and scale the cached element by a fraction of a pixel.
///
/// When the SnapsToDevicePixels property is set to true,
/// the bitmap cache is aligned with pixel boundaries of the destination.
/// If you move or scale the cached element by a fraction of a pixel,
/// the bitmap snaps to the pixel grid
/// . In this case, the top-left corner of the bitmap is rounded up and snapped to the pixel grid,
/// but the bottom-right corner is on a fractional pixel boundary.
/// </summary>
public bool SnapsToDevicePixels
{
get => GetValue(SnapsToDevicePixelsProperty);
set => SetValue(SnapsToDevicePixelsProperty, value);
}
public static readonly StyledProperty<bool> EnableClearTypeProperty = AvaloniaProperty.Register<BitmapCache, bool>(
nameof(EnableClearType));
/// <summary>
/// Set the EnableClearType property to allow subpixel text to be rendered in the cache.
/// When the EnableClearType property is true, your application MUST render all
/// of its subpixel text on an opaque background.
///
/// When the EnableClearType property is false, text in the cache is rendered with grayscale antialiasing.
///
/// ClearType text requires correct pixel alignment of rendered characters,
/// so you should set the SnapsToDevicePixels property to true.
/// If you do not set this property, the content may not blend correctly.
///
/// Use the EnableClearType property when you know the cache is rendered on pixel boundaries,
/// so it is safe to cache ClearType text. This situation occurs commonly in text-scrolling scenarios.
/// </summary>
public bool EnableClearType
{
get => GetValue(EnableClearTypeProperty);
set => SetValue(EnableClearTypeProperty, value);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
if (change.IsEffectiveValueChange && _current != null)
{
if (change.Property == RenderAtScaleProperty)
_current.RenderAtScale = RenderAtScale;
else if (change.Property == SnapsToDevicePixelsProperty)
_current.SnapsToDevicePixels = SnapsToDevicePixels;
else if (change.Property == EnableClearTypeProperty)
_current.EnableClearType = EnableClearType;
}
base.OnPropertyChanged(change);
}
// We currently only allow visual to be attached to one compositor at a time, so keep it simple for now
internal override CompositionCacheMode GetForCompositor(Compositor c)
{
// TODO: Make it to be a multi-compositor resource once we support visuals being attached to multiple
// compositor instances (e. g. referenced via visual brush from a different WASM toplevel).
if(_current?.Compositor != c)
{
_current = new CompositionBitmapCache(c, new ServerCompositionBitmapCache(c.Server));
_current.EnableClearType = EnableClearType;
_current.RenderAtScale = RenderAtScale;
_current.SnapsToDevicePixels = SnapsToDevicePixels;
}
return _current;
}
}

21
src/Avalonia.Base/Media/CacheMode.cs

@ -0,0 +1,21 @@
using System;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Drawing;
namespace Avalonia.Media;
/// <summary>
/// Represents cached content modes for graphics acceleration features.
/// </summary>
public abstract class CacheMode : StyledElement
{
// We currently only allow visual to be attached to one compositor at a time, so keep it simple for now
internal abstract CompositionCacheMode GetForCompositor(Compositor c);
public static CacheMode Parse(string s)
{
if(s == "BitmapCache")
return new BitmapCache();
throw new ArgumentException("Unknown CacheMode: " + s);
}
}

7
src/Avalonia.Base/Media/Color.cs

@ -472,13 +472,6 @@ namespace Avalonia.Media
return ((uint)A << 24) | ((uint)R << 16) | ((uint)G << 8) | (uint)B;
}
/// <inheritdoc cref="Color.ToUInt32"/>
[Obsolete("Use Color.ToUInt32() instead."), EditorBrowsable(EditorBrowsableState.Never)]
public uint ToUint32()
{
return ToUInt32();
}
/// <summary>
/// Returns the HSL color model equivalent of this RGB color.
/// </summary>

9
src/Avalonia.Base/Media/DrawingContext.cs

@ -432,15 +432,8 @@ namespace Avalonia.Media
_states.Push(new RestoreState(this, RestoreState.PushedStateType.TextOptions));
return new PushedState(this);
}
protected abstract void PushTextOptionsCore(TextOptions textOptions);
[Obsolete("Use PushTransform"), EditorBrowsable(EditorBrowsableState.Never)]
public PushedState PushPreTransform(Matrix matrix) => PushTransform(matrix);
[Obsolete("Use PushTransform"), EditorBrowsable(EditorBrowsableState.Never)]
public PushedState PushPostTransform(Matrix matrix) => PushTransform(matrix);
[Obsolete("Use PushTransform"), EditorBrowsable(EditorBrowsableState.Never)]
public PushedState PushTransformContainer() => PushTransform(Matrix.Identity);
protected abstract void PushTextOptionsCore(TextOptions textOptions);
protected abstract void PushTransformCore(Matrix matrix);

2
src/Avalonia.Base/Media/IRadialGradientBrush.cs

@ -20,8 +20,6 @@ namespace Avalonia.Media
/// </summary>
RelativePoint GradientOrigin { get; }
[Obsolete("Use RadiusX/RadiusY")] public double Radius { get; }
/// <summary>
/// Gets the horizontal radius of the outermost circle of the radial gradient.
/// </summary>

2
src/Avalonia.Base/Media/Immutable/ImmutableRadialGradientBrush.cs

@ -85,7 +85,5 @@ namespace Avalonia.Media.Immutable
/// <inheritdoc/>
public RelativeScalar RadiusY { get; }
[Obsolete("Use RadiusX/RadiusY")] public double Radius => RadiusX.Scalar;
}
}

40
src/Avalonia.Base/Media/RadialGradientBrush.cs

@ -27,15 +27,6 @@ namespace Avalonia.Media
AvaloniaProperty.Register<RadialGradientBrush, RelativePoint>(
nameof(GradientOrigin),
RelativePoint.Center);
/// <summary>
/// Defines the <see cref="Radius"/> property.
/// </summary>
[Obsolete("Use RadiusX/RadiusY, note that those properties use _relative_ values, so Radius=0.55 would become RadiusX=55% RadiusY=55%. Radius property is always relative even if the rest of the brush uses absolute values.")]
public static readonly StyledProperty<double> RadiusProperty =
AvaloniaProperty.Register<RadialGradientBrush, double>(
nameof(Radius),
0.5);
/// <summary>
/// Defines the <see cref="RadiusX"/> property.
@ -74,9 +65,6 @@ namespace Avalonia.Media
/// Gets or sets the horizontal radius of the outermost circle of the radial
/// gradient.
/// </summary>
#pragma warning disable CS0618 // Type or member is obsolete
[DependsOn(nameof(Radius))]
#pragma warning restore CS0618 // Type or member is obsolete
public RelativeScalar RadiusX
{
get { return GetValue(RadiusXProperty); }
@ -87,25 +75,11 @@ namespace Avalonia.Media
/// Gets or sets the vertical radius of the outermost circle of the radial
/// gradient.
/// </summary>
#pragma warning disable CS0618 // Type or member is obsolete
[DependsOn(nameof(Radius))]
#pragma warning restore CS0618 // Type or member is obsolete
public RelativeScalar RadiusY
{
get { return GetValue(RadiusYProperty); }
set { SetValue(RadiusYProperty, value); }
}
/// <summary>
/// Gets or sets the horizontal and vertical radius of the outermost circle of the radial
/// gradient.
/// </summary>
[Obsolete("Use RadiusX/RadiusY, note that those properties use _relative_ values, so Radius=0.55 would become RadiusX=55% RadiusY=55%. Radius property is always relative even if the rest of the brush uses absolute values.")]
public double Radius
{
get { return GetValue(RadiusProperty); }
set { SetValue(RadiusProperty, value); }
}
/// <inheritdoc/>
public override IImmutableBrush ToImmutable()
@ -121,19 +95,5 @@ namespace Avalonia.Media
base.SerializeChanges(c, writer);
ServerCompositionSimpleRadialGradientBrush.SerializeAllChanges(writer, Center, GradientOrigin, RadiusX, RadiusY);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
#pragma warning disable CS0618 // Type or member is obsolete: compatibility code for Radius
if (change.IsEffectiveValueChange && change.Property == RadiusProperty)
{
var compatibilityValue = new RelativeScalar(Radius, RelativeUnit.Relative);
SetCurrentValue(RadiusXProperty, compatibilityValue);
SetCurrentValue(RadiusYProperty, compatibilityValue);
}
#pragma warning restore CS0618 // Type or member is obsolete
base.OnPropertyChanged(change);
}
}
}

8
src/Avalonia.Base/Platform/IPlatformRenderInterface.cs

@ -221,8 +221,9 @@ namespace Avalonia.Platform
/// </summary>
/// <param name="pixelSize">The size, in pixels, of the render target</param>
/// <param name="scaling">The scaling which will be reported by IBitmap.Dpi</param>
/// <param name="enableTextAntialiasing">Specifies if text antialiasing should be enabled</param>
/// <returns></returns>
IDrawingContextLayerImpl CreateOffscreenRenderTarget(PixelSize pixelSize, double scaling);
IDrawingContextLayerImpl CreateOffscreenRenderTarget(PixelSize pixelSize, Vector scaling, bool enableTextAntialiasing);
/// <summary>
/// Indicates that the context is no longer usable. This method should be thread-safe
@ -233,5 +234,10 @@ namespace Avalonia.Platform
/// Exposes features that should be available for consumption while context isn't active (e. g. from the UI thread)
/// </summary>
IReadOnlyDictionary<Type, object> PublicFeatures { get; }
/// <summary>
/// Maximum supported offscreen render target pixel size, or null if no limit
/// </summary>
public PixelSize? MaxOffscreenRenderTargetPixelSize { get; }
}
}

47
src/Avalonia.Base/Platform/IRenderTarget.cs

@ -1,6 +1,5 @@
using System;
using Avalonia.Metadata;
using Avalonia.Rendering;
namespace Avalonia.Platform
{
@ -13,51 +12,29 @@ namespace Avalonia.Platform
[PrivateApi]
public interface IRenderTarget : IDisposable
{
/// <summary>
/// Creates an <see cref="IDrawingContextImpl"/> for a rendering session.
/// </summary>
/// <param name="useScaledDrawing">Apply DPI reported by the render target as a hidden transform matrix</param>
IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing);
/// <summary>
/// Indicates if the render target is no longer usable and needs to be recreated
/// </summary>
public bool IsCorrupted { get; }
}
bool IsCorrupted { get; }
[PrivateApi, Obsolete("Use IRenderTarget2", true)]
// TODO12: Remove
public interface IRenderTargetWithProperties : IRenderTarget
{
RenderTargetProperties Properties { get; }
}
[PrivateApi]
// TODO12: Merge into IRenderTarget
public interface IRenderTarget2 : IRenderTarget
{
/// <summary>
/// Gets the properties of the render target.
/// </summary>
RenderTargetProperties Properties { get; }
/// <summary>
/// Creates an <see cref="IDrawingContextImpl"/> for a rendering session.
/// </summary>
/// <param name="useScaledDrawing">Apply DPI reported by the render target as a hidden transform matrix</param>
IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing);
/// <summary>
/// Creates an <see cref="IDrawingContextImpl"/> for a rendering session.
/// </summary>
/// <param name="expectedPixelSize">The pixel size of the surface</param>
/// <param name="properties">Returns various properties about the returned drawing context</param>
IDrawingContextImpl CreateDrawingContext(PixelSize expectedPixelSize,
out RenderTargetDrawingContextProperties properties);
}
internal static class RenderTargetExtensions
{
public static IDrawingContextImpl CreateDrawingContextWithProperties(
this IRenderTarget renderTarget,
IDrawingContextImpl CreateDrawingContext(
PixelSize expectedPixelSize,
out RenderTargetDrawingContextProperties properties)
{
if (renderTarget is IRenderTarget2 target)
return target.CreateDrawingContext(expectedPixelSize, out properties);
properties = default;
return renderTarget.CreateDrawingContext(false);
}
out RenderTargetDrawingContextProperties properties);
}
}

70
src/Avalonia.Base/Platform/LtrbRect.cs

@ -23,6 +23,9 @@ public struct LtrbRect
{
public double Left, Top, Right, Bottom;
public double Width => Right - Left;
public double Height => Bottom - Top;
internal LtrbRect(double x, double y, double right, double bottom)
{
Left = x;
@ -40,9 +43,37 @@ public struct LtrbRect
Bottom = rc.Bottom;
}
internal bool IsZeroSize => Left == Right && Top == Bottom;
internal static LtrbRect Infinite { get; } = new(double.NegativeInfinity, double.NegativeInfinity,
double.PositiveInfinity, double.PositiveInfinity);
internal bool IsWellOrdered => Left <= Right && Top <= Bottom;
internal bool IsZeroSize => Left == Right || Top == Bottom;
internal bool IsEmpty => IsZeroSize;
internal LtrbRect? NullIfZeroSize() => IsZeroSize ? null : this;
internal LtrbRect Intersect(LtrbRect rect)
internal LtrbRect? IntersectOrNull(LtrbRect rect)
{
var newLeft = (rect.Left > Left) ? rect.Left : Left;
var newTop = (rect.Top > Top) ? rect.Top : Top;
var newRight = (rect.Right < Right) ? rect.Right : Right;
var newBottom = (rect.Bottom < Bottom) ? rect.Bottom : Bottom;
if ((newRight > newLeft) && (newBottom > newTop))
{
return new LtrbRect(newLeft, newTop, newRight, newBottom);
}
else
{
return default;
}
}
internal LtrbRect IntersectOrEmpty(LtrbRect rect)
{
var newLeft = (rect.Left > Left) ? rect.Left : Left;
var newTop = (rect.Top > Top) ? rect.Top : Top;
@ -118,7 +149,7 @@ public struct LtrbRect
/// <summary>
/// Perform _WPF-like_ union operation
/// </summary>
private LtrbRect FullUnionCore(LtrbRect rect)
public LtrbRect Union(LtrbRect rect)
{
var x1 = Math.Min(Left, rect.Left);
var x2 = Math.Max(Right, rect.Right);
@ -134,7 +165,7 @@ public struct LtrbRect
return right;
if (right == null)
return left;
return right.Value.FullUnionCore(left.Value);
return right.Value.Union(left.Value);
}
internal static LtrbRect? FullUnion(LtrbRect? left, Rect? right)
@ -143,7 +174,7 @@ public struct LtrbRect
return left;
if (left == null)
return new(right.Value);
return left.Value.FullUnionCore(new(right.Value));
return left.Value.Union(new(right.Value));
}
public override bool Equals(object? obj)
@ -165,6 +196,18 @@ public struct LtrbRect
return hash;
}
}
public override string ToString() => $"{Left}:{Top}-{Right}:{Bottom} ({Width}x{Height})";
public bool Contains(Point point)
{
return point.X >= Left && point.X <= Right && point.Y >= Top && point.Y <= Bottom;
}
public bool Contains(LtrbRect rect)
{
return rect.Left >= Left && rect.Right <= Right && rect.Top >= Top && rect.Bottom <= Bottom;
}
}
/// <summary>
@ -184,7 +227,7 @@ public struct LtrbRect
public struct LtrbPixelRect
{
public int Left, Top, Right, Bottom;
internal LtrbPixelRect(int x, int y, int right, int bottom)
{
Left = x;
@ -218,16 +261,10 @@ public struct LtrbPixelRect
return new(x1, y1, x2, y2);
}
internal Rect ToRectWithNoScaling() => new(Left, Top, (Right - Left), (Bottom - Top));
internal bool Contains(int x, int y)
{
return x >= Left && x <= Right && y >= Top && y <= Bottom;
}
internal static LtrbPixelRect FromRectWithNoScaling(LtrbRect rect) =>
new((int)rect.Left, (int)rect.Top, (int)Math.Ceiling(rect.Right),
(int)Math.Ceiling(rect.Bottom));
public static bool operator ==(LtrbPixelRect left, LtrbPixelRect right)=>
left.Left == right.Left && left.Top == right.Top && left.Right == right.Right && left.Bottom == right.Bottom;
@ -259,10 +296,9 @@ public struct LtrbPixelRect
}
internal Rect ToRectUnscaled() => new(Left, Top, Right - Left, Bottom - Top);
internal static LtrbPixelRect FromRectUnscaled(LtrbRect rect)
{
return new LtrbPixelRect((int)rect.Left, (int)rect.Top, (int)Math.Ceiling(rect.Right),
internal LtrbRect ToLtrbRectUnscaled() => new(Left, Top, Right, Bottom);
internal static LtrbPixelRect FromRectUnscaled(LtrbRect rect) =>
new((int)rect.Left, (int)rect.Top, (int)Math.Ceiling(rect.Right),
(int)Math.Ceiling(rect.Bottom));
}
}

2
src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleCompositionBrush.cs

@ -49,7 +49,7 @@ namespace Avalonia.Rendering.Composition.Server
partial class ServerCompositionSimpleRadialGradientBrush : IRadialGradientBrush
{
public double Radius => RadiusX.Scalar;
}
partial class ServerCompositionSimpleSolidColorBrush : ISolidColorBrush

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

@ -180,11 +180,12 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester
CompositionTarget.PixelSize = PixelSize.FromSizeRounded(_root.ClientSize, _root.RenderScaling);
CompositionTarget.Scaling = _root.RenderScaling;
var commit = _compositor.RequestCommitAsync();
var commit = _compositor.RequestCompositionBatchCommitAsync();
if (!_queuedSceneInvalidation)
{
_queuedSceneInvalidation = true;
commit.ContinueWith(_ => Dispatcher.UIThread.Post(() =>
// Updated hit-test information is available after full render
commit.Rendered.ContinueWith(_ => Dispatcher.UIThread.Post(() =>
{
_queuedSceneInvalidation = false;
SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize)));

10
src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs

@ -12,6 +12,7 @@ public abstract class CompositionCustomVisualHandler
private ServerCompositionCustomVisual? _host;
private bool _inRender;
private Rect _currentTransformedClip;
private Matrix _currentTransform;
public virtual void OnMessage(object message)
{
@ -27,6 +28,7 @@ public abstract class CompositionCustomVisualHandler
{
_inRender = true;
_currentTransformedClip = currentTransformedClip;
_currentTransform = drawingContext.CurrentTransform;
try
{
OnRender(drawingContext);
@ -97,14 +99,14 @@ public abstract class CompositionCustomVisualHandler
protected bool RenderClipContains(Point pt)
{
VerifyInRender();
pt *= _host!.GlobalTransformMatrix;
return _currentTransformedClip.Contains(pt) && _host.Root!.DirtyRects.Contains(pt);
pt = pt.Transform(_currentTransform);
return _currentTransformedClip.Contains(pt);
}
protected bool RenderClipIntersectes(Rect rc)
{
VerifyInRender();
rc = rc.TransformToAABB(_host!.GlobalTransformMatrix);
return _currentTransformedClip.Intersects(rc) && _host.Root!.DirtyRects.Intersects(new (rc));
rc = rc.TransformToAABB(_currentTransform);
return _currentTransformedClip.Intersects(rc);
}
}

5
src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs

@ -25,6 +25,10 @@ internal class CompositionDrawListVisual : CompositionContainerVisual
get => _drawList;
set
{
// Nothing to do
if (value == null && _drawList == null)
return;
_drawList?.Dispose();
_drawList = value;
_drawListChanged = true;
@ -46,6 +50,7 @@ internal class CompositionDrawListVisual : CompositionContainerVisual
internal CompositionDrawListVisual(Compositor compositor, ServerCompositionDrawListVisual server, Visual visual) : base(compositor, server)
{
Visual = visual;
CustomHitTestCountInSubTree = visual is ICustomHitTest ? 1 : 0;
}
internal override bool HitTest(Point pt)

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

@ -101,12 +101,6 @@ public interface ICompositionGpuImportedObject : IAsyncDisposable
/// </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>

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

@ -87,7 +87,6 @@ abstract class CompositionGpuImportedObjectBase : ICompositionGpuImportedObject
public Task ImportCompleted { get; }
public Task ImportCompeted => ImportCompleted;
public bool IsLost => Context.IsLost;
public ValueTask DisposeAsync() => new(Compositor.InvokeServerJobAsync(() =>

18
src/Avalonia.Base/Rendering/Composition/CompositionOptions.cs

@ -1,3 +1,5 @@
using Avalonia.Metadata;
namespace Avalonia.Rendering.Composition;
public class CompositionOptions
@ -7,6 +9,22 @@ public class CompositionOptions
/// drawing context
/// </summary>
public bool? UseRegionDirtyRectClipping { get; set; }
/// <summary>
/// The maximum number of dirty rects to track when region clip is in use. Setting this to zero or negative
/// value will remove the smarter algorithm and will use underlying drawing context region support directly.
/// Default value is 8.
/// </summary>
public int? MaxDirtyRects { get; set; }
/// <summary>
/// Controls the eagerness of merging dirty rects. WPF uses 50000, Avalonia currently has a different default
/// that's a subject to change. You can play with this property to find the best value for your application.
/// </summary>
[Unstable]
public double? DirtyRectMergeEagerness { get; set; }
/// <summary>
/// Enforces dirty contents to be rendered into an extra intermediate surface before being applied onto the
/// saved frame.

76
src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs

@ -29,63 +29,26 @@ namespace Avalonia.Rendering.Composition
/// <returns></returns>
public PooledList<CompositionVisual>? TryHitTest(Point point, CompositionVisual? root, Func<CompositionVisual, bool>? filter)
{
point *= Scaling;
Server.Readback.NextRead();
Server.Compositor.Readback.NextRead();
root ??= Root;
if (root == null)
return null;
var res = new PooledList<CompositionVisual>();
// Need to convert transform the point using visual's readback since HitTestCore will use its inverse matrix
// NOTE: it can technically break hit-testing of the root visual itself if it has a non-identity transform,
// need to investigate that possibility later. We might want a separate mode for root hit-testing.
var readback = root.TryGetValidReadback();
if (readback == null)
return null;
point = point.Transform(readback.Matrix);
HitTestCore(root, point, res, filter);
return res;
}
/// <summary>
/// Attempts to transform a point to a particular CompositionVisual coordinate space
/// </summary>
/// <returns></returns>
public Point? TryTransformToVisual(CompositionVisual visual, Point point)
{
if (visual.Root != this)
return null;
var v = visual;
var m = Matrix.Identity;
while (v != null)
{
if (!TryGetInvertedTransform(v, out var cm))
return null;
m = m * cm;
v = v.Parent;
}
return point * m;
}
static bool TryGetInvertedTransform(CompositionVisual visual, out Matrix matrix)
{
var m = visual.TryGetServerGlobalTransform();
if (m == null)
{
matrix = default;
return false;
}
var m33 = m.Value;
return m33.TryInvert(out matrix);
}
static bool TryTransformTo(CompositionVisual visual, Point globalPoint, out Point v)
{
v = default;
if (TryGetInvertedTransform(visual, out var m))
{
v = globalPoint * m;
return true;
}
return false;
}
void HitTestCore(CompositionVisual visual, Point globalPoint, PooledList<CompositionVisual> result,
void HitTestCore(CompositionVisual visual, Point parentPoint, PooledList<CompositionVisual> result,
Func<CompositionVisual, bool>? filter)
{
if (visual.Visible == false)
@ -93,10 +56,21 @@ namespace Avalonia.Rendering.Composition
if (filter != null && !filter(visual))
return;
var readback = visual.TryGetValidReadback();
if(readback == null)
return;
if (!visual.DisableSubTreeBoundsHitTestOptimization &&
(readback.TransformedSubtreeBounds == null ||
!readback.TransformedSubtreeBounds.Value.Contains(parentPoint)))
return;
if (!TryTransformTo(visual, globalPoint, out var point))
if(!readback.Matrix.TryInvert(out var invMatrix))
return;
var point = parentPoint.Transform(invMatrix);
var allowChildren = true;
if (visual.ClipToBounds)
{
@ -122,7 +96,7 @@ namespace Avalonia.Rendering.Composition
for (var c = cv.Children.Count - 1; c >= 0; c--)
{
var ch = cv.Children[c];
HitTestCore(ch, globalPoint, result, filter);
HitTestCore(ch, point, result, filter);
}
// Hit-test the current node

5
src/Avalonia.Base/Rendering/Composition/Drawing/CompositorResourceHelpers.cs

@ -49,7 +49,7 @@ internal struct CompositorResourceHolder<T> where T : SimpleServerObject
public bool IsAttached => _dictionary.HasEntries;
public bool CreateOrAddRef(Compositor compositor, ICompositorSerializable owner, out T resource, Func<Compositor, T> factory)
public bool CreateOrAddRef(Compositor compositor, ICompositorSerializable? owner, out T resource, Func<Compositor, T> factory)
{
if (_dictionary.TryGetValue(compositor, out var handle))
{
@ -60,7 +60,8 @@ internal struct CompositorResourceHolder<T> where T : SimpleServerObject
resource = factory(compositor);
_dictionary.Add(compositor, new CompositorRefCountableResource<T>(resource));
compositor.RegisterForSerialization(owner);
if (owner != null)
compositor.RegisterForSerialization(owner);
return true;
}

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

@ -1,8 +1,10 @@
using Avalonia.Platform;
namespace Avalonia.Rendering.Composition;
internal interface ICompositionTargetDebugEvents
{
int RenderedVisuals { get; }
void IncrementRenderedVisuals();
void RectInvalidated(Rect rc);
int RenderedVisuals { get; set; }
int VisitedVisuals { get; set; }
void RectInvalidated(LtrbRect rc);
}

6
src/Avalonia.Base/Rendering/Composition/MatrixUtils.cs

@ -4,13 +4,13 @@ namespace Avalonia.Rendering.Composition
{
static class MatrixUtils
{
public static Matrix ComputeTransform(Vector size, Vector anchorPoint, Vector3D centerPoint,
public static Matrix? ComputeTransform(Vector size, Vector anchorPoint, Vector3D centerPoint,
Matrix transformMatrix, Vector3D scale, float rotationAngle, Quaternion orientation, Vector3D offset)
{
// The math here follows the *observed* UWP behavior since there are no docs on how it's supposed to work
var anchor = Vector.Multiply(size, anchorPoint);
var mat = Matrix.CreateTranslation(-anchor.X, -anchor.Y);
var mat = Matrix.CreateTranslation(-anchor.X, -anchor.Y);
var center = new Vector3D(centerPoint.X, centerPoint.Y, centerPoint.Z);
@ -45,6 +45,8 @@ namespace Avalonia.Rendering.Composition
mat *= ToMatrix(Matrix4x4.CreateTranslation(offset.ToVector3()));
}
if (mat.IsIdentity)
return null;
return mat;
}

15
src/Avalonia.Base/Rendering/Composition/Server/CompositionTargetOverlays.cs

@ -11,6 +11,7 @@ internal class CompositionTargetOverlays
{
private FpsCounter? _fpsCounter;
private FrameTimeGraph? _renderTimeGraph;
private FrameTimeGraph? _compositorUpdateTimeGraph;
private FrameTimeGraph? _updateTimeGraph;
private FrameTimeGraph? _layoutTimeGraph;
private Rect? _oldFpsCounterRect;
@ -36,9 +37,12 @@ internal class CompositionTargetOverlays
private FrameTimeGraph? RenderTimeGraph
=> _renderTimeGraph ??= CreateTimeGraph("Render");
private FrameTimeGraph? CompositorUpdateTimeGraph
=> _compositorUpdateTimeGraph ??= CreateTimeGraph("GUpdate");
private FrameTimeGraph? UpdateTimeGraph
=> _updateTimeGraph ??= CreateTimeGraph("RUpdate");
=> _updateTimeGraph ??= CreateTimeGraph("TUpdate");
@ -108,6 +112,12 @@ internal class CompositionTargetOverlays
if (CaptureTiming)
UpdateTimeGraph?.AddFrameValue(StopwatchHelper.GetElapsedTime(_updateStarted).TotalMilliseconds);
}
public void RecordGlobalCompositorUpdateTime(TimeSpan elapsed)
{
if (CaptureTiming)
CompositorUpdateTimeGraph?.AddFrameValue(elapsed.TotalMilliseconds);
}
private void DrawOverlays(ImmediateDrawingContext targetContext, bool hasLayer, Size logicalSize)
{
@ -122,7 +132,7 @@ internal class CompositionTargetOverlays
IntPtr.Size), false);
_oldFpsCounterRect = FpsCounter?.RenderFps(targetContext,
FormattableString.Invariant($"M:{managedMem} / N:{nativeMem} R:{_target.RenderedVisuals:0000}"),
FormattableString.Invariant($"M:{managedMem} / N:{nativeMem} V:{_target.VisitedVisuals:0000} R:{_target.RenderedVisuals:0000}"),
hasLayer, _oldFpsCounterRect);
}
@ -147,6 +157,7 @@ internal class CompositionTargetOverlays
if (DebugOverlays.HasFlag(RendererDebugOverlays.RenderTimeGraph))
{
DrawTimeGraph(RenderTimeGraph);
DrawTimeGraph(CompositorUpdateTimeGraph);
DrawTimeGraph(UpdateTimeGraph);
}
}

41
src/Avalonia.Base/Rendering/Composition/Server/CompositorPools.cs

@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Platform;
namespace Avalonia.Rendering.Composition.Server;
internal class CompositorPools
{
public class StackPool<T> : Stack<Stack<T>>
{
public Stack<T> Rent()
{
if (Count > 0)
return Pop()!;
return new Stack<T>();
}
public void Return(ref Stack<T> stack)
{
Return(stack);
stack = null!;
}
public void Return(Stack<T>? stack)
{
if (stack == null)
return;
stack.Clear();
Push(stack);
}
}
public StackPool<ServerCompositionVisual.TreeWalkerFrame> TreeWalkerFrameStackPool { get; } = new();
public StackPool<Matrix> MatrixStackPool { get; } = new();
public StackPool<LtrbRect> LtrbRectStackPool { get; } = new();
public StackPool<double> DoubleStackPool { get; } = new();
public StackPool<int> IntStackPool { get; } = new();
public StackPool<IDirtyRectCollector> DirtyRectCollectorStackPool { get; } = new();
}

105
src/Avalonia.Base/Rendering/Composition/Server/DirtyRectTracker.cs

@ -1,105 +0,0 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
using Avalonia.Reactive;
namespace Avalonia.Rendering.Composition.Server;
internal interface IDirtyRectTracker
{
void AddRect(LtrbPixelRect rect);
IDisposable BeginDraw(IDrawingContextImpl ctx);
bool IsEmpty { get; }
bool Intersects(LtrbRect rect);
bool Contains(Point pt);
void Reset();
void Visualize(IDrawingContextImpl context);
LtrbPixelRect CombinedRect { get; }
IList<LtrbPixelRect> Rects { get; }
}
internal class DirtyRectTracker : IDirtyRectTracker
{
private LtrbPixelRect _rect;
private Rect _doubleRect;
private LtrbRect _normalRect;
private LtrbPixelRect[] _rectsForApi = new LtrbPixelRect[1];
private Random _random = new();
public void AddRect(LtrbPixelRect rect)
{
_rect = _rect.Union(rect);
}
public IDisposable BeginDraw(IDrawingContextImpl ctx)
{
ctx.PushClip(_rect.ToRectWithNoScaling());
_doubleRect = _rect.ToRectWithNoScaling();
_normalRect = new(_doubleRect);
return Disposable.Create(ctx.PopClip);
}
public bool IsEmpty => _rect.IsEmpty;
public bool Intersects(LtrbRect rect) => _normalRect.Intersects(rect);
public bool Contains(Point pt) => _rect.Contains((int)pt.X, (int)pt.Y);
public void Reset() => _rect = default;
public void Visualize(IDrawingContextImpl context)
{
context.DrawRectangle(
new ImmutableSolidColorBrush(
new Color(30, (byte)_random.Next(255), (byte)_random.Next(255), (byte)_random.Next(255))),
null, _doubleRect);
}
public LtrbPixelRect CombinedRect => _rect;
public IList<LtrbPixelRect> Rects
{
get
{
if (_rect.IsEmpty)
return Array.Empty<LtrbPixelRect>();
_rectsForApi[0] = _rect;
return _rectsForApi;
}
}
}
internal class RegionDirtyRectTracker : IDirtyRectTracker
{
private readonly IPlatformRenderInterfaceRegion _region;
private Random _random = new();
public RegionDirtyRectTracker(IPlatformRenderInterface platformRender)
{
_region = platformRender.CreateRegion();
}
public void AddRect(LtrbPixelRect rect) => _region.AddRect(rect);
public IDisposable BeginDraw(IDrawingContextImpl ctx)
{
ctx.PushClip(_region);
return Disposable.Create(ctx.PopClip);
}
public bool IsEmpty => _region.IsEmpty;
public bool Intersects(LtrbRect rect) => _region.Intersects(rect);
public bool Contains(Point pt) => _region.Contains(pt);
public void Reset() => _region.Reset();
public void Visualize(IDrawingContextImpl context)
{
context.DrawRegion(
new ImmutableSolidColorBrush(
new Color(150, (byte)_random.Next(255), (byte)_random.Next(255), (byte)_random.Next(255))),
null, _region);
}
public LtrbPixelRect CombinedRect => _region.Bounds;
public IList<LtrbPixelRect> Rects => _region.Rects;
}

13
src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/DebugEventsDirtyRectCollectorProxy.cs

@ -0,0 +1,13 @@
using Avalonia.Platform;
namespace Avalonia.Rendering.Composition.Server;
internal class DebugEventsDirtyRectCollectorProxy(IDirtyRectCollector inner, ICompositionTargetDebugEvents events)
: IDirtyRectCollector
{
public void AddRect(LtrbRect rect)
{
inner.AddRect(rect);
events.RectInvalidated(rect);
}
}

24
src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/IDirtyRectTracker.cs

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using Avalonia.Platform;
namespace Avalonia.Rendering.Composition.Server;
internal interface IDirtyRectTracker : IDirtyRectCollector
{
/// <summary>
/// Post-processes the dirty rect area (e. g. to account for anti-aliasing)
/// </summary>
void FinalizeFrame(LtrbRect bounds);
IDisposable BeginDraw(IDrawingContextImpl ctx);
bool IsEmpty { get; }
bool Intersects(LtrbRect rect);
void Initialize(LtrbRect bounds);
void Visualize(IDrawingContextImpl context);
LtrbRect CombinedRect { get; }
}
internal interface IDirtyRectCollector
{
void AddRect(LtrbRect rect);
}

348
src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/MultiDirtyRectTracker.CDirtyRegion.cs

@ -0,0 +1,348 @@
using System;
using System.Diagnostics;
using Avalonia.Platform;
namespace Avalonia.Rendering.Composition.Server;
partial class MultiDirtyRectTracker
{
/// <summary>
/// This is a port of CDirtyRegion2 from WPF
/// </summary>
class CDirtyRegion2(int MaxDirtyRegionCount)
{
private readonly LtrbRect[] _dirtyRegions = new LtrbRect[MaxDirtyRegionCount];
private readonly LtrbRect[] _resolvedRegions = new LtrbRect[MaxDirtyRegionCount];
private readonly double[,] _overhead = new double[MaxDirtyRegionCount + 1, MaxDirtyRegionCount];
private LtrbRect _surfaceBounds;
private double _allowedDirtyRegionOverhead;
private int _regionCount;
private bool _optimized;
private bool _maxSurfaceFallback;
private readonly struct UnionResult
{
public readonly double Overhead;
// Left here for debugging purposes
public readonly double Area;
public readonly LtrbRect Union;
public UnionResult(double overhead, double area, LtrbRect union)
{
Overhead = overhead;
Area = area;
Union = union;
}
}
private static double RectArea(LtrbRect r)
{
return (r.Right - r.Left) * (r.Bottom - r.Top);
}
private static LtrbRect RectUnion(LtrbRect left, LtrbRect right)
{
if (left.IsZeroSize)
return right;
if (right.IsZeroSize)
return left;
return left.Union(right);
}
private static UnionResult ComputeUnion(LtrbRect r0, LtrbRect r1)
{
var unioned = RectUnion(r0, r1);
var intersected = r0.IntersectOrEmpty(r1);
double areaOfUnion = RectArea(unioned);
double overhead = areaOfUnion - (RectArea(r0) + RectArea(r1) - RectArea(intersected));
// Use 0 as overhead if computed overhead is negative or overhead
// computation returns a nan. (If more than one of the previous
// area computations overflowed then overhead could be not a
// number.)
if (!(overhead > 0))
{
overhead = 0;
}
return new UnionResult(overhead, areaOfUnion, unioned);
}
private void SetOverhead(int i, int j, double value)
{
if (i > j)
{
_overhead[i, j] = value;
}
else if (i < j)
{
_overhead[j, i] = value;
}
}
private double GetOverhead(int i, int j)
{
if (i > j)
{
return _overhead[i, j];
}
if (i < j)
{
return _overhead[j, i];
}
return double.MaxValue;
}
private void UpdateOverhead(int regionIndex)
{
ref readonly var regionAtIndex = ref _dirtyRegions[regionIndex];
for (int i = 0; i < MaxDirtyRegionCount; i++)
{
if (regionIndex != i)
{
var ur = ComputeUnion(_dirtyRegions[i], regionAtIndex);
SetOverhead(i, regionIndex, ur.Overhead);
}
}
}
/// <summary>
/// Initialize must be called before adding dirty rects. Initialize can also be called to
/// reset the dirty region.
/// </summary>
public void Initialize(LtrbRect surfaceBounds, double allowedDirtyRegionOverhead)
{
_allowedDirtyRegionOverhead = allowedDirtyRegionOverhead;
Array.Clear(_dirtyRegions);
Array.Clear(_overhead);
_optimized = false;
_maxSurfaceFallback = false;
_regionCount = 0;
_surfaceBounds = surfaceBounds;
}
/// <summary>
/// Adds a new dirty rectangle to the dirty region.
/// </summary>
public void Add(LtrbRect newRegion)
{
// // We've already fallen back to setting the whole surface as a dirty region
// // because of invalid dirty rects, so no need to add any new ones
if (_maxSurfaceFallback)
{
return;
}
// // Check if rectangle is well formed before we try to intersect it,
// // because Intersect will fail for badly formed rects
if (!newRegion.IsWellOrdered)
{
// If we're here it means that we've been passed an invalid rectangle as a dirty
// region, containing NAN or a non well ordered rectangle.
// In this case, make the dirty region the full surface size and warn in the debugger
// since this could cause a serious perf regression.
//
Debug.Assert(false);
//
// Remove all dirty regions from this object, since
// they're no longer relevant.
//
Initialize(_surfaceBounds, _allowedDirtyRegionOverhead);
_maxSurfaceFallback = true;
_regionCount = 1;
return;
}
var clippedNewRegion = newRegion.IntersectOrEmpty(_surfaceBounds);
if (clippedNewRegion.IsEmpty)
{
return;
}
// Always keep bounding boxes device space integer.
clippedNewRegion = new LtrbRect(
Math.Floor(clippedNewRegion.Left),
Math.Floor(clippedNewRegion.Top),
Math.Ceiling(clippedNewRegion.Right),
Math.Ceiling(clippedNewRegion.Bottom));
// Compute the overhead for the new region combined with all existing regions
for (int n = 0; n < MaxDirtyRegionCount; n++)
{
var ur = ComputeUnion(_dirtyRegions[n], clippedNewRegion);
SetOverhead(MaxDirtyRegionCount, n, ur.Overhead);
}
// Find the pair of dirty regions that if merged create the minimal overhead. A overhead
// of 0 is perfect in the sense that it can not get better. In that case we break early
// out of the loop.
double minimalOverhead = double.MaxValue;
int bestMatchN = 0;
int bestMatchK = 0;
bool matchFound = false;
for (int n = MaxDirtyRegionCount; n > 0; n--)
{
for (int k = 0; k < n; k++)
{
double overheadNK = GetOverhead(n, k);
if (minimalOverhead >= overheadNK)
{
minimalOverhead = overheadNK;
bestMatchN = n;
bestMatchK = k;
matchFound = true;
if (overheadNK < _allowedDirtyRegionOverhead)
{
// If the overhead is very small, we bail out early since this
// saves us some valuable cycles. Note that "small" means really
// nothing here. In fact we don't always know if that number is
// actually small. However, it the algorithm stays still correct
// in the sense that we render everything that is necessary. It
// might just be not optimal.
goto LoopExit;
}
}
}
}
if (!matchFound)
{
return;
}
LoopExit:
// Case A: The new dirty region can be combined with an existing one
if (bestMatchN == MaxDirtyRegionCount)
{
var ur = ComputeUnion(clippedNewRegion, _dirtyRegions[bestMatchK]);
var unioned = ur.Union;
if (_dirtyRegions[bestMatchK].Contains(unioned))
{
// newDirtyRegion is enclosed by dirty region bestMatchK
return;
}
_dirtyRegions[bestMatchK] = unioned;
UpdateOverhead(bestMatchK);
}
else
{
// Case B: Merge region N with region K, store new region slot K
var ur = ComputeUnion(_dirtyRegions[bestMatchN], _dirtyRegions[bestMatchK]);
_dirtyRegions[bestMatchN] = ur.Union;
_dirtyRegions[bestMatchK] = clippedNewRegion;
UpdateOverhead(bestMatchN);
UpdateOverhead(bestMatchK);
}
}
/// <summary>
/// Returns an array of dirty rectangles describing the dirty region.
/// </summary>
public ReadOnlySpan<LtrbRect> GetUninflatedDirtyRegions()
{
if (_maxSurfaceFallback)
{
return new ReadOnlySpan<LtrbRect>(in _surfaceBounds);
}
if (!_optimized)
{
Array.Clear(_resolvedRegions);
// Consolidate the dirtyRegions array
int addedDirtyRegionCount = 0;
for (int i = 0; i < MaxDirtyRegionCount; i++)
{
if (!_dirtyRegions[i].IsEmpty)
{
if (i != addedDirtyRegionCount)
{
_dirtyRegions[addedDirtyRegionCount] = _dirtyRegions[i];
UpdateOverhead(addedDirtyRegionCount);
}
addedDirtyRegionCount++;
}
}
// Merge all dirty rects that we can
bool couldMerge = true;
while (couldMerge)
{
couldMerge = false;
for (int n = 0; n < addedDirtyRegionCount; n++)
{
for (int k = n + 1; k < addedDirtyRegionCount; k++)
{
if (!_dirtyRegions[n].IsEmpty
&& !_dirtyRegions[k].IsEmpty
&& GetOverhead(n, k) < _allowedDirtyRegionOverhead)
{
var ur = ComputeUnion(_dirtyRegions[n], _dirtyRegions[k]);
_dirtyRegions[n] = ur.Union;
_dirtyRegions[k] = default;
UpdateOverhead(n);
couldMerge = true;
}
}
}
}
// Consolidate and copy into resolvedRegions
int finalRegionCount = 0;
for (int i = 0; i < addedDirtyRegionCount; i++)
{
if (!_dirtyRegions[i].IsEmpty)
{
_resolvedRegions[finalRegionCount] = _dirtyRegions[i];
finalRegionCount++;
}
}
_regionCount = finalRegionCount;
_optimized = true;
}
return _resolvedRegions.AsSpan(0, _regionCount);
}
/// <summary>
/// Checks if the dirty region is empty.
/// </summary>
public bool IsEmpty
{
get
{
for (int i = 0; i < MaxDirtyRegionCount; i++)
{
if (!_dirtyRegions[i].IsEmpty)
{
return false;
}
}
return true;
}
}
/// <summary>
/// Returns the dirty region count.
/// NOTE: The region count is NOT VALID until GetUninflatedDirtyRegions is called.
/// </summary>
public int RegionCount => _regionCount;
}
}

86
src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/MultiDirtyRectTracker.cs

@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
using Avalonia.Reactive;
namespace Avalonia.Rendering.Composition.Server;
internal partial class MultiDirtyRectTracker : IDirtyRectTracker
{
private readonly double _maxOverhead;
private readonly CDirtyRegion2 _regions;
private readonly IPlatformRenderInterfaceRegion _clipRegion;
private readonly List<LtrbRect> _inflatedRects = new();
private Random _random = new();
public MultiDirtyRectTracker(IPlatformRenderInterface platformRender, int maxDirtyRects, double maxOverhead)
{
_maxOverhead = maxOverhead;
_regions = new CDirtyRegion2(maxDirtyRects);
_clipRegion = platformRender.CreateRegion();
}
public void AddRect(LtrbRect rect) => _regions.Add(rect);
public void FinalizeFrame(LtrbRect bounds)
{
_inflatedRects.Clear();
_clipRegion.Reset();
var dirtyRegions = _regions.GetUninflatedDirtyRegions();
LtrbRect? combined = default;
foreach (var rect in dirtyRegions)
{
var inflated = rect.Inflate(new(1)).IntersectOrEmpty(bounds);
_inflatedRects.Add(inflated);
_clipRegion.AddRect(LtrbPixelRect.FromRectUnscaled(inflated));
combined = LtrbRect.FullUnion(combined, inflated);
}
CombinedRect = combined ?? default;
}
public IDisposable BeginDraw(IDrawingContextImpl ctx)
{
ctx.PushClip(_clipRegion);
return Disposable.Create(ctx.PopClip);
}
public bool IsEmpty => _regions.IsEmpty;
public bool Intersects(LtrbRect rect)
{
foreach(var r in _inflatedRects)
{
if (r.Intersects(rect))
return true;
}
return false;
}
public void Initialize(LtrbRect bounds)
{
_regions.Initialize(bounds, _maxOverhead);
_inflatedRects.Clear();
_clipRegion.Reset();
CombinedRect = default;
}
public void Visualize(IDrawingContextImpl context)
{
context.DrawRegion(
new ImmutableSolidColorBrush(
new Color(150, (byte)_random.Next(255), (byte)_random.Next(255), (byte)_random.Next(255))),
null, _clipRegion);
}
public LtrbRect CombinedRect { get; private set; }
public IReadOnlyList<LtrbRect> InflatedRects => _inflatedRects;
}

60
src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/RegionDirtyRectTracker.cs

@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
using Avalonia.Reactive;
namespace Avalonia.Rendering.Composition.Server;
internal class RegionDirtyRectTracker : IDirtyRectTracker
{
private readonly IPlatformRenderInterfaceRegion _region;
private readonly List<LtrbRect> _rects = new();
private Random _random = new();
public RegionDirtyRectTracker(IPlatformRenderInterface platformRender)
{
_region = platformRender.CreateRegion();
}
public void AddRect(LtrbRect rect) => _rects.Add(rect);
private LtrbPixelRect GetInflatedPixelRect(LtrbRect rc)
{
var inflated = rc.Inflate(new Thickness(1)).IntersectOrEmpty(rc);
var pixelRect = LtrbPixelRect.FromRectUnscaled(inflated);
return pixelRect;
}
public void FinalizeFrame(LtrbRect bounds)
{
_region.Reset();
foreach (var rc in _rects)
_region.AddRect(GetInflatedPixelRect(rc));
CombinedRect = _region.Bounds.ToLtrbRectUnscaled();
}
public IDisposable BeginDraw(IDrawingContextImpl ctx)
{
ctx.PushClip(_region);
return Disposable.Create(ctx.PopClip);
}
public bool IsEmpty => _rects.Count == 0;
public bool Intersects(LtrbRect rect) => _region.Intersects(rect);
public void Initialize(LtrbRect bounds) => _rects.Clear();
public void Visualize(IDrawingContextImpl context)
{
context.DrawRegion(
new ImmutableSolidColorBrush(
new Color(150, (byte)_random.Next(255), (byte)_random.Next(255), (byte)_random.Next(255))),
null, _region);
}
public LtrbRect CombinedRect { get; private set; }
}

50
src/Avalonia.Base/Rendering/Composition/Server/DirtyRects/SingleDirtyRectTracker.cs

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
using Avalonia.Reactive;
namespace Avalonia.Rendering.Composition.Server;
internal class SingleDirtyRectTracker : IDirtyRectTracker
{
private LtrbRect? _rect;
private LtrbRect _extendedRect;
private readonly Random _random = new();
public void AddRect(LtrbRect rect)
{
_rect = LtrbRect.FullUnion(_rect, rect);
}
public void FinalizeFrame(LtrbRect bounds)
{
_extendedRect = _rect.HasValue
? LtrbPixelRect.FromRectUnscaled(_rect.Value.Inflate(new Thickness(1)).IntersectOrEmpty(bounds))
.ToLtrbRectUnscaled()
: default;
}
public IDisposable BeginDraw(IDrawingContextImpl ctx)
{
ctx.PushClip(_extendedRect.ToRect());
return Disposable.Create(ctx.PopClip);
}
public bool IsEmpty => _rect?.IsZeroSize ?? true;
public bool Intersects(LtrbRect rect) => _extendedRect.Intersects(rect);
public void Initialize(LtrbRect bounds) => _rect = default;
public void Visualize(IDrawingContextImpl context)
{
context.DrawRectangle(
new ImmutableSolidColorBrush(
new Color(30, (byte)_random.Next(255), (byte)_random.Next(255), (byte)_random.Next(255))),
null, _extendedRect.ToRect());
}
public LtrbRect CombinedRect => _extendedRect;
}

45
src/Avalonia.Base/Rendering/Composition/Server/ReadbackIndices.cs

@ -1,3 +1,5 @@
using System.Threading;
namespace Avalonia.Rendering.Composition.Server
{
/// <summary>
@ -7,40 +9,29 @@ namespace Avalonia.Rendering.Composition.Server
/// </summary>
internal class ReadbackIndices
{
private readonly object _lock = new object();
public int ReadIndex { get; private set; } = 0;
public int WriteIndex { get; private set; } = 1;
public int WrittenIndex { get; private set; } = 0;
public readonly object _lock = new object();
public ulong ReadRevision { get; private set; }
public ulong LastWrittenRevision { get; private set; }
private ulong _nextWriteRevision = 1;
public ulong WriteRevision { get; private set; }
public ulong LastCompletedWrite { get; private set; }
public void NextRead()
{
lock (_lock)
{
if (ReadRevision < LastWrittenRevision)
{
ReadIndex = WrittenIndex;
ReadRevision = LastWrittenRevision;
}
}
ReadRevision = LastCompletedWrite;
}
public void BeginWrite()
{
Monitor.Enter(_lock);
WriteRevision = _nextWriteRevision++;
}
public void CompleteWrite(ulong writtenRevision)
public void EndWrite()
{
lock (_lock)
{
for (var c = 0; c < 3; c++)
{
if (c != WriteIndex && c != ReadIndex)
{
WrittenIndex = WriteIndex;
LastWrittenRevision = writtenRevision;
WriteIndex = c;
return;
}
}
}
LastCompletedWrite = WriteRevision;
Monitor.Exit(_lock);
}
}
}

12
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionBitmapCache.cs

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Rendering.Composition.Server;
internal partial class ServerCompositionBitmapCache
{
}

24
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionCacheMode.cs

@ -0,0 +1,24 @@
using Avalonia.Utilities;
namespace Avalonia.Rendering.Composition.Server;
partial class ServerCompositionCacheMode
{
private readonly WeakHashList<ServerCompositionVisual> _attachedVisuals = new();
public void Subscribe(ServerCompositionVisual visual) => _attachedVisuals.Add(visual);
public void Unsubscribe(ServerCompositionVisual visual) => _attachedVisuals.Remove(visual);
protected override void ValuesInvalidated()
{
using var alive = _attachedVisuals.GetAlive();
if (alive != null)
{
foreach (var v in alive.Span)
v.OnCacheModeStateChanged();
}
base.ValuesInvalidated();
}
}

95
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs

@ -12,99 +12,6 @@ namespace Avalonia.Rendering.Composition.Server
/// </summary>
internal partial class ServerCompositionContainerVisual : ServerCompositionVisual
{
public ServerCompositionVisualCollection Children { get; private set; } = null!;
private LtrbRect? _transformedContentBounds;
private IImmutableEffect? _oldEffect;
protected override void RenderCore(ServerVisualRenderContext context, LtrbRect currentTransformedClip)
{
base.RenderCore(context, currentTransformedClip);
if (context.RenderChildren)
{
foreach (var ch in Children)
{
ch.Render(context, currentTransformedClip);
}
}
}
public override UpdateResult Update(ServerCompositionTarget root, Matrix parentCombinedTransform)
{
var (combinedBounds, oldInvalidated, newInvalidated) = base.Update(root, parentCombinedTransform);
foreach (var child in Children)
{
if (child.AdornedVisual != null)
root.EnqueueAdornerUpdate(child);
else
{
var res = child.Update(root, GlobalTransformMatrix);
oldInvalidated |= res.InvalidatedOld;
newInvalidated |= res.InvalidatedNew;
combinedBounds = LtrbRect.FullUnion(combinedBounds, res.Bounds);
}
}
// If effect is changed, we need to clean both old and new bounds
var effectChanged = !Effect.EffectEquals(_oldEffect);
if (effectChanged)
oldInvalidated = newInvalidated = true;
// Expand invalidated bounds to the whole content area since we don't actually know what is being sampled
// We also ignore clip for now since we don't have means to reset it?
if (_oldEffect != null && oldInvalidated && _transformedContentBounds.HasValue)
AddEffectPaddedDirtyRect(_oldEffect, _transformedContentBounds.Value);
if (Effect != null && newInvalidated && combinedBounds.HasValue)
AddEffectPaddedDirtyRect(Effect, combinedBounds.Value);
_oldEffect = Effect;
_transformedContentBounds = combinedBounds;
IsDirtyComposition = false;
return new(_transformedContentBounds, oldInvalidated, newInvalidated);
}
protected override LtrbRect GetEffectBounds() => _transformedContentBounds ?? default;
void AddEffectPaddedDirtyRect(IImmutableEffect effect, LtrbRect transformedBounds)
{
var padding = effect.GetEffectOutputPadding();
if (padding == default)
{
AddDirtyRect(transformedBounds);
return;
}
// We are in a weird position here: bounds are in global coordinates while padding gets applied in local ones
// Since we have optimizations to AVOID recomputing transformed bounds and since visuals with effects are relatively rare
// we instead apply the transformation matrix to rescale the bounds
// If we only have translation and scale, just scale the padding
if (CombinedTransformMatrix is
{
M12: 0, M13: 0,
M21: 0, M23: 0,
M31: 0, M32: 0
})
padding = new Thickness(padding.Left * CombinedTransformMatrix.M11,
padding.Top * CombinedTransformMatrix.M22,
padding.Right * CombinedTransformMatrix.M11,
padding.Bottom * CombinedTransformMatrix.M22);
else
{
// Conservatively use the transformed rect size
var transformedPaddingRect = new Rect().Inflate(padding).TransformToAABB(CombinedTransformMatrix);
padding = new(Math.Max(transformedPaddingRect.Width, transformedPaddingRect.Height));
}
AddDirtyRect(transformedBounds.Inflate(padding));
}
partial void Initialize()
{
Children = new ServerCompositionVisualCollection(Compositor);
}
public new ServerCompositionVisualCollection Children => base.Children!;
}
}

24
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs

@ -27,7 +27,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua
#endif
}
public override LtrbRect OwnContentBounds => _renderCommands?.Bounds ?? default;
public override LtrbRect? ComputeOwnContentBounds() => _renderCommands?.Bounds;
protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt)
{
@ -36,31 +36,17 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua
_renderCommands?.Dispose();
_renderCommands = reader.ReadObject<ServerCompositionRenderData?>();
_renderCommands?.AddObserver(this);
InvalidateContent();
}
base.DeserializeChangesCore(reader, committedAt);
}
protected void RenderOwnContent(ServerVisualRenderContext context, LtrbRect currentTransformedClip)
{
if (_renderCommands != null
&& context.ShouldRenderOwnContent(this, currentTransformedClip))
{
_renderCommands.Render(context.Canvas);
}
}
protected void RenderChildren(ServerVisualRenderContext context, LtrbRect currentTransformedClip)
{
base.RenderCore(context, currentTransformedClip);
}
protected override void RenderCore(ServerVisualRenderContext context, LtrbRect currentTransformedClip)
{
RenderOwnContent(context, currentTransformedClip);
RenderChildren(context, currentTransformedClip);
_renderCommands?.Render(context.Canvas);
}
public void DependencyQueuedInvalidate(IServerRenderResource sender) => ValuesInvalidated();
public void DependencyQueuedInvalidate(IServerRenderResource sender) => InvalidateContent();
#if DEBUG
public override string ToString()

24
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs

@ -8,17 +8,25 @@ internal partial class ServerCompositionExperimentalAcrylicVisual
protected override void RenderCore(ServerVisualRenderContext context, LtrbRect currentTransformedClip)
{
var cornerRadius = CornerRadius;
context.Canvas.DrawRectangle(
Material,
new RoundedRect(
new Rect(0, 0, Size.X, Size.Y),
cornerRadius.TopLeft, cornerRadius.TopRight,
cornerRadius.BottomRight, cornerRadius.BottomLeft));
if(context.Canvas is IDrawingContextWithAcrylicLikeSupport supported)
supported.DrawRectangle(
Material,
new RoundedRect(
new Rect(0, 0, Size.X, Size.Y),
cornerRadius.TopLeft, cornerRadius.TopRight,
cornerRadius.BottomRight, cornerRadius.BottomLeft));
base.RenderCore(context, currentTransformedClip);
}
public override LtrbRect OwnContentBounds => new(0, 0, Size.X, Size.Y);
public override LtrbRect? ComputeOwnContentBounds() =>
LtrbRect.FullUnion(base.ComputeOwnContentBounds(), new LtrbRect(0, 0, Size.X, Size.Y));
protected override void SizeChanged()
{
EnqueueForOwnBoundsRecompute();
base.SizeChanged();
}
public ServerCompositionExperimentalAcrylicVisual(ServerCompositor compositor, Visual v) : base(compositor, v)
{

2
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs

@ -19,7 +19,7 @@ internal partial class ServerCompositionSurfaceVisual
}
private void OnSurfaceInvalidated() => ValuesInvalidated();
private void OnSurfaceInvalidated() => InvalidateContent();
protected override void OnAttachedToRoot(ServerCompositionTarget target)
{

48
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs

@ -1,48 +0,0 @@
using System;
using System.Collections.Generic;
using Avalonia.Collections.Pooled;
using Avalonia.Platform;
namespace Avalonia.Rendering.Composition.Server;
internal partial class ServerCompositionTarget
{
public readonly IDirtyRectTracker DirtyRects;
static int Clamp0(int value, int max) => Math.Max(Math.Min(value, max), 0);
public void AddDirtyRect(LtrbRect rect)
{
if (rect.IsZeroSize)
return;
DebugEvents?.RectInvalidated(rect.ToRect());
var snapped = LtrbPixelRect.FromRectWithNoScaling(SnapToDevicePixels(rect, Scaling));
var clamped = new LtrbPixelRect(
Clamp0(snapped.Left, _pixelSize.Width),
Clamp0(snapped.Top, _pixelSize.Height),
Clamp0(snapped.Right, _pixelSize.Width),
Clamp0(snapped.Bottom, _pixelSize.Height)
);
if (!clamped.IsEmpty)
DirtyRects.AddRect(clamped);
_redrawRequested = true;
}
public Rect SnapToDevicePixels(Rect rect) => SnapToDevicePixels(new(rect), Scaling).ToRect();
public LtrbRect SnapToDevicePixels(LtrbRect rect) => SnapToDevicePixels(rect, Scaling);
public static LtrbRect SnapToDevicePixels(LtrbRect rect, double scale)
{
return new LtrbRect(
Math.Floor(rect.Left * scale) / scale,
Math.Floor(rect.Top * scale) / scale,
Math.Ceiling(rect.Right * scale) / scale,
Math.Ceiling(rect.Bottom * scale) / scale);
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save