Browse Source

Merge branch 'master' into textlayout-wrapping-perf

pull/10068/head
Benedikt Stebner 4 years ago
committed by GitHub
parent
commit
bfed891713
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .editorconfig
  2. 3
      build/Base.props
  3. 3
      build/System.Memory.props
  4. 3
      samples/ControlCatalog.NetCore/Program.cs
  5. 22
      samples/ControlCatalog/Pages/DataGridPage.xaml
  6. 13
      samples/ControlCatalog/Pages/DataGridPage.xaml.cs
  7. 17
      samples/ControlCatalog/Pages/DialogsPage.xaml
  8. 60
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  9. 4
      samples/ControlCatalog/Pages/ExpanderPage.xaml.cs
  10. 2
      samples/ControlCatalog/Pages/ScrollSnapPage.xaml
  11. 7
      samples/GpuInterop/D3DDemo/D3D11DemoControl.cs
  12. 4
      samples/RenderDemo/App.xaml.cs
  13. 17
      src/Android/Avalonia.Android/AndroidPlatform.cs
  14. 10
      src/Android/Avalonia.Android/AvaloniaMainActivity.cs
  15. 3
      src/Android/Avalonia.Android/IActivityResultHandler.cs
  16. 4
      src/Android/Avalonia.Android/Platform/AndroidSystemNavigationManager.cs
  17. 52
      src/Android/Avalonia.Android/Platform/PlatformSupport.cs
  18. 57
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  19. 101
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
  20. 124
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs
  21. 26
      src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs
  22. 4
      src/Avalonia.Base/Avalonia.Base.csproj
  23. 5
      src/Avalonia.Base/Input/DragEventArgs.cs
  24. 7
      src/Avalonia.Base/Input/GotFocusEventArgs.cs
  25. 11
      src/Avalonia.Base/Input/KeyEventArgs.cs
  26. 1
      src/Avalonia.Base/Input/KeyboardDevice.cs
  27. 8
      src/Avalonia.Base/Input/PointerDeltaEventArgs.cs
  28. 17
      src/Avalonia.Base/Input/PointerEventArgs.cs
  29. 8
      src/Avalonia.Base/Input/PointerWheelEventArgs.cs
  30. 2
      src/Avalonia.Base/Input/Raw/RawTextInputEventArgs.cs
  31. 4
      src/Avalonia.Base/Input/ScrollGestureEventArgs.cs
  32. 3
      src/Avalonia.Base/Input/TappedEventArgs.cs
  33. 8
      src/Avalonia.Base/Input/TextInput/TransformTrackingHelper.cs
  34. 4
      src/Avalonia.Base/Input/TextInputEventArgs.cs
  35. 7
      src/Avalonia.Base/Input/VectorEventArgs.cs
  36. 39
      src/Avalonia.Base/Interactivity/CancelRoutedEventArgs.cs
  37. 2
      src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs
  38. 77
      src/Avalonia.Base/Media/Imaging/Bitmap.cs
  39. 51
      src/Avalonia.Base/Media/Imaging/BitmapMemory.cs
  40. 280
      src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs
  41. 77
      src/Avalonia.Base/Media/Imaging/WriteableBitmap.cs
  42. 34
      src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs
  43. 2
      src/Avalonia.Base/PixelRect.cs
  44. 1
      src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs
  45. 2
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  46. 7
      src/Avalonia.Base/Platform/IReadableBitmapImpl.cs
  47. 3
      src/Avalonia.Base/Platform/IWriteableBitmapImpl.cs
  48. 71
      src/Avalonia.Base/Platform/PixelFormat.cs
  49. 71
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs
  50. 63
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs
  51. 103
      src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs
  52. 16
      src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs
  53. 4
      src/Avalonia.Base/Platform/Storage/IStorageItem.cs
  54. 35
      src/Avalonia.Base/Platform/Storage/IStorageProvider.cs
  55. 6
      src/Avalonia.Base/Platform/Storage/NameCollisionOption.cs
  56. 55
      src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs
  57. 37
      src/Avalonia.Base/Platform/Storage/WellKnownFolder.cs
  58. 8
      src/Avalonia.Base/Platform/SystemNavigationManagerImpl.cs
  59. 6
      src/Avalonia.Base/Point.cs
  60. 27
      src/Avalonia.Base/Rect.cs
  61. 15
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  62. 3
      src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
  63. 3
      src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs
  64. 9
      src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
  65. 3
      src/Avalonia.Base/Rendering/Composition/Compositor.cs
  66. 780
      src/Avalonia.Base/Rendering/DeferredRenderer.cs
  67. 16
      src/Avalonia.Base/Rendering/ICustomHitTest.cs
  68. 33
      src/Avalonia.Base/Rendering/ICustomSimpleHitTest.cs
  69. 6
      src/Avalonia.Base/Rendering/IRenderRoot.cs
  70. 16
      src/Avalonia.Base/Rendering/IRendererFactory.cs
  71. 248
      src/Avalonia.Base/Rendering/ImmediateRenderer.cs
  72. 4
      src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs
  73. 48
      src/Avalonia.Base/Rendering/RenderLayer.cs
  74. 69
      src/Avalonia.Base/Rendering/RenderLayers.cs
  75. 482
      src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  76. 24
      src/Avalonia.Base/Rendering/SceneGraph/ISceneBuilder.cs
  77. 105
      src/Avalonia.Base/Rendering/SceneGraph/IVisualNode.cs
  78. 352
      src/Avalonia.Base/Rendering/SceneGraph/Scene.cs
  79. 485
      src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs
  80. 73
      src/Avalonia.Base/Rendering/SceneGraph/SceneLayer.cs
  81. 206
      src/Avalonia.Base/Rendering/SceneGraph/SceneLayers.cs
  82. 448
      src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs
  83. 2
      src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs
  84. 18
      src/Avalonia.Base/Visual.cs
  85. 3
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml
  86. 3
      src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml
  87. 3
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml
  88. 3
      src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml
  89. 2
      src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs
  90. 2
      src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs
  91. 4
      src/Avalonia.Controls/Button.cs
  92. 20
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  93. 130
      src/Avalonia.Controls/Expander.cs
  94. 19
      src/Avalonia.Controls/Flyouts/MenuFlyout.cs
  95. 1
      src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs
  96. 1
      src/Avalonia.Controls/ItemsControl.cs
  97. 2
      src/Avalonia.Controls/NativeControlHost.cs
  98. 3
      src/Avalonia.Controls/NativeMenu.Export.cs
  99. 4
      src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
  100. 14
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

3
.editorconfig

@ -137,6 +137,9 @@ space_within_single_line_array_initializer_braces = true
#Net Analyzer #Net Analyzer
dotnet_analyzer_diagnostic.category-Performance.severity = none #error - Uncomment when all violations are fixed. dotnet_analyzer_diagnostic.category-Performance.severity = none #error - Uncomment when all violations are fixed.
# CS0649: Field 'field' is never assigned to, and will always have its default value 'value'
dotnet_diagnostic.CS0649.severity = error
# CS1591: Missing XML comment for publicly visible type or member # CS1591: Missing XML comment for publicly visible type or member
dotnet_diagnostic.CS1591.severity = suggestion dotnet_diagnostic.CS1591.severity = suggestion

3
build/Base.props

@ -1,5 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Condition="'$(TargetFramework)' != 'net6'"> <!-- '!NET6_0_OR_GREATER' equivalent -->
<ItemGroup Condition="!('$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '6.0')))">
<PackageReference Include="System.ValueTuple" Version="4.5.0" /> <PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="6.0.0" /> <PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="6.0.0" />
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" /> <PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />

3
build/System.Memory.props

@ -1,5 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Condition="'$(TargetFramework)' != 'net6'"> <!-- '!NET6_0_OR_GREATER' equivalent -->
<ItemGroup Condition="!('$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '6.0')))">
<PackageReference Include="System.Memory" Version="4.5.3" /> <PackageReference Include="System.Memory" Version="4.5.3" />
</ItemGroup> </ItemGroup>
</Project> </Project>

3
samples/ControlCatalog.NetCore/Program.cs

@ -55,8 +55,7 @@ namespace ControlCatalog.NetCore
return builder return builder
.UseHeadless(new AvaloniaHeadlessPlatformOptions .UseHeadless(new AvaloniaHeadlessPlatformOptions
{ {
UseHeadlessDrawing = true, UseHeadlessDrawing = true
UseCompositor = true
}) })
.AfterSetup(_ => .AfterSetup(_ =>
{ {

22
samples/ControlCatalog/Pages/DataGridPage.xaml

@ -1,7 +1,9 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl xmlns="https://github.com/avaloniaui"
xmlns:local="using:ControlCatalog.Models" xmlns:local="using:ControlCatalog.Models"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.DataGridPage"> xmlns:pages="clr-namespace:ControlCatalog.Pages"
x:Class="ControlCatalog.Pages.DataGridPage"
x:DataType="pages:DataGridPage">
<UserControl.Resources> <UserControl.Resources>
<local:GDPValueConverter x:Key="GDPConverter" /> <local:GDPValueConverter x:Key="GDPConverter" />
<DataTemplate x:Key="Demo.DataTemplates.CountryHeader" x:DataType="local:Country"> <DataTemplate x:Key="Demo.DataTemplates.CountryHeader" x:DataType="local:Country">
@ -33,7 +35,7 @@
<DataGrid.Columns> <DataGrid.Columns>
<!-- Using HeaderTemplate --> <!-- Using HeaderTemplate -->
<DataGridTextColumn Header="Country" HeaderTemplate="{StaticResource Demo.DataTemplates.CountryHeader}" Binding="{Binding Name}" Width="6*" x:DataType="local:Country" /> <DataGridTextColumn Header="Country" HeaderTemplate="{StaticResource Demo.DataTemplates.CountryHeader}" Binding="{Binding Name}" Width="6*" x:DataType="local:Country" />
<DataGridTextColumn Header="Region" Binding="{CompiledBinding Region}" Width="4*" x:DataType="local:Country" /> <DataGridTextColumn Header="Region" Binding="{Binding Region}" Width="4*" x:DataType="local:Country" />
<DataGridTextColumn Header="Population" Binding="{Binding Population}" Width="3*" x:DataType="local:Country" /> <DataGridTextColumn Header="Population" Binding="{Binding Population}" Width="3*" x:DataType="local:Country" />
<DataGridTextColumn Header="Area" Binding="{Binding Area}" Width="3*" x:DataType="local:Country" /> <DataGridTextColumn Header="Area" Binding="{Binding Area}" Width="3*" x:DataType="local:Country" />
<DataGridTextColumn Header="GDP" Binding="{Binding GDP}" Width="3*" <DataGridTextColumn Header="GDP" Binding="{Binding GDP}" Width="3*"
@ -90,19 +92,21 @@
</TabItem> </TabItem>
<TabItem x:Name="EditableTab" Header="Editable"> <TabItem x:Name="EditableTab" Header="Editable">
<Grid RowDefinitions="*,Auto"> <Grid RowDefinitions="*,Auto">
<DataGrid Name="dataGridEdit" Margin="12" Grid.Row="0"> <!-- Example of columns inheriting the data type from the Items source -->
<DataGrid Name="dataGridEdit" Margin="12" Grid.Row="0"
Items="{Binding DataGrid3Source}">
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Header="First Name" Binding="{Binding FirstName}" Width="2*" FontSize="{Binding #FontSizeSlider.Value, Mode=OneWay}" x:DataType="local:Person" /> <DataGridTextColumn Header="First Name" Binding="{Binding FirstName}" Width="2*" FontSize="{Binding #FontSizeSlider.Value, Mode=OneWay}" />
<DataGridTextColumn Header="Last" Binding="{Binding LastName}" Width="2*" FontSize="{Binding #FontSizeSlider.Value, Mode=OneWay}" x:DataType="local:Person" /> <DataGridTextColumn Header="Last" Binding="{Binding LastName}" Width="2*" FontSize="{Binding #FontSizeSlider.Value, Mode=OneWay}" />
<DataGridCheckBoxColumn Header="Is Banned" Binding="{Binding IsBanned}" Width="*" IsThreeState="{Binding #IsThreeStateCheckBox.IsChecked, Mode=OneWay}" x:DataType="local:Person" /> <DataGridCheckBoxColumn Header="Is Banned" Binding="{Binding IsBanned}" Width="*" IsThreeState="{Binding #IsThreeStateCheckBox.IsChecked, Mode=OneWay}" />
<DataGridTemplateColumn Header="Age" > <DataGridTemplateColumn Header="Age">
<DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="local:Person"> <DataTemplate>
<TextBlock Text="{Binding Age, StringFormat='{}{0} years'}" VerticalAlignment="Center" HorizontalAlignment="Center" /> <TextBlock Text="{Binding Age, StringFormat='{}{0} years'}" VerticalAlignment="Center" HorizontalAlignment="Center" />
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate> <DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate DataType="local:Person"> <DataTemplate>
<NumericUpDown Value="{Binding Age}" FormatString="N0" HorizontalAlignment="Stretch" Minimum="0" Maximum="120" TemplateApplied="NumericUpDown_OnTemplateApplied" /> <NumericUpDown Value="{Binding Age}" FormatString="N0" HorizontalAlignment="Stretch" Minimum="0" Maximum="120" TemplateApplied="NumericUpDown_OnTemplateApplied" />
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate> </DataGridTemplateColumn.CellEditingTemplate>

13
samples/ControlCatalog/Pages/DataGridPage.xaml.cs

@ -1,5 +1,6 @@
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using Avalonia.Controls; using Avalonia.Controls;
@ -48,20 +49,22 @@ namespace ControlCatalog.Pages
var dg3 = this.Get<DataGrid>("dataGridEdit"); var dg3 = this.Get<DataGrid>("dataGridEdit");
dg3.IsReadOnly = false; dg3.IsReadOnly = false;
var items = new List<Person> var list = new ObservableCollection<Person>
{ {
new Person { FirstName = "John", LastName = "Doe" , Age = 30}, new Person { FirstName = "John", LastName = "Doe" , Age = 30},
new Person { FirstName = "Elizabeth", LastName = "Thomas", IsBanned = true , Age = 40 }, new Person { FirstName = "Elizabeth", LastName = "Thomas", IsBanned = true , Age = 40 },
new Person { FirstName = "Zack", LastName = "Ward" , Age = 50 } new Person { FirstName = "Zack", LastName = "Ward" , Age = 50 }
}; };
var collectionView3 = new DataGridCollectionView(items); DataGrid3Source = list;
dg3.Items = collectionView3;
var addButton = this.Get<Button>("btnAdd"); var addButton = this.Get<Button>("btnAdd");
addButton.Click += (a, b) => collectionView3.AddNew(); addButton.Click += (a, b) => list.Add(new Person());
DataContext = this;
} }
public IEnumerable<Person> DataGrid3Source { get; }
private void Dg1_LoadingRow(object? sender, DataGridRowEventArgs e) private void Dg1_LoadingRow(object? sender, DataGridRowEventArgs e)
{ {
e.Row.Header = e.Row.GetIndex() + 1; e.Row.Header = e.Row.GetIndex() + 1;

17
samples/ControlCatalog/Pages/DialogsPage.xaml

@ -1,6 +1,8 @@
<UserControl x:Class="ControlCatalog.Pages.DialogsPage" <UserControl x:Class="ControlCatalog.Pages.DialogsPage"
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:storage="clr-namespace:Avalonia.Platform.Storage;assembly=Avalonia.Base"
xmlns:generic="clr-namespace:System.Collections.Generic;assembly=System.Collections">
<StackPanel Margin="4" <StackPanel Margin="4"
Orientation="Vertical" Orientation="Vertical"
Spacing="4"> Spacing="4">
@ -42,6 +44,19 @@
</StackPanel> </StackPanel>
</Expander> </Expander>
<AutoCompleteBox x:Name="CurrentFolderBox" Watermark="Write full path/uri or well known folder name">
<AutoCompleteBox.Items>
<generic:List x:TypeArguments="storage:WellKnownFolder">
<storage:WellKnownFolder>Desktop</storage:WellKnownFolder>
<storage:WellKnownFolder>Documents</storage:WellKnownFolder>
<storage:WellKnownFolder>Downloads</storage:WellKnownFolder>
<storage:WellKnownFolder>Pictures</storage:WellKnownFolder>
<storage:WellKnownFolder>Videos</storage:WellKnownFolder>
<storage:WellKnownFolder>Music</storage:WellKnownFolder>
</generic:List>
</AutoCompleteBox.Items>
</AutoCompleteBox>
<TextBlock x:Name="PickerLastResultsVisible" <TextBlock x:Name="PickerLastResultsVisible"
Classes="h2" Classes="h2"
IsVisible="False" IsVisible="False"

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

@ -24,13 +24,38 @@ namespace ControlCatalog.Pages
{ {
this.InitializeComponent(); this.InitializeComponent();
IStorageFolder? lastSelectedDirectory = null;
bool ignoreTextChanged = false;
var results = this.Get<ItemsControl>("PickerLastResults"); var results = this.Get<ItemsControl>("PickerLastResults");
var resultsVisible = this.Get<TextBlock>("PickerLastResultsVisible"); var resultsVisible = this.Get<TextBlock>("PickerLastResultsVisible");
var bookmarkContainer = this.Get<TextBox>("BookmarkContainer"); var bookmarkContainer = this.Get<TextBox>("BookmarkContainer");
var openedFileContent = this.Get<TextBox>("OpenedFileContent"); var openedFileContent = this.Get<TextBox>("OpenedFileContent");
var openMultiple = this.Get<CheckBox>("OpenMultiple"); var openMultiple = this.Get<CheckBox>("OpenMultiple");
var currentFolderBox = this.Get<AutoCompleteBox>("CurrentFolderBox");
currentFolderBox.TextChanged += async (sender, args) =>
{
if (ignoreTextChanged) return;
if (Enum.TryParse<WellKnownFolder>(currentFolderBox.Text, true, out var folderEnum))
{
lastSelectedDirectory = await GetStorageProvider().TryGetWellKnownFolder(folderEnum);
}
else
{
if (!Uri.TryCreate(currentFolderBox.Text, UriKind.Absolute, out var folderLink))
{
Uri.TryCreate("file://" + currentFolderBox.Text, UriKind.Absolute, out folderLink);
}
if (folderLink is not null)
{
lastSelectedDirectory = await GetStorageProvider().TryGetFolderFromPath(folderLink);
}
}
};
IStorageFolder? lastSelectedDirectory = null;
List<FileDialogFilter> GetFilters() List<FileDialogFilter> GetFilters()
{ {
@ -84,7 +109,7 @@ namespace ControlCatalog.Pages
{ {
Title = "Open multiple files", Title = "Open multiple files",
Filters = GetFilters(), Filters = GetFilters(),
Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null, Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null,
AllowMultiple = true AllowMultiple = true
}.ShowAsync(GetWindow()); }.ShowAsync(GetWindow());
results.Items = result; results.Items = result;
@ -97,7 +122,7 @@ namespace ControlCatalog.Pages
{ {
Title = "Save file", Title = "Save file",
Filters = filters, Filters = filters,
Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null, Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null,
DefaultExtension = filters?.Any() == true ? "txt" : null, DefaultExtension = filters?.Any() == true ? "txt" : null,
InitialFileName = "test.txt" InitialFileName = "test.txt"
}.ShowAsync(GetWindow()); }.ShowAsync(GetWindow());
@ -109,7 +134,7 @@ namespace ControlCatalog.Pages
var result = await new OpenFolderDialog() var result = await new OpenFolderDialog()
{ {
Title = "Select folder", Title = "Select folder",
Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null,
}.ShowAsync(GetWindow()); }.ShowAsync(GetWindow());
if (string.IsNullOrEmpty(result)) if (string.IsNullOrEmpty(result))
{ {
@ -117,7 +142,7 @@ namespace ControlCatalog.Pages
} }
else else
{ {
lastSelectedDirectory = new BclStorageFolder(new System.IO.DirectoryInfo(result)); SetFolder(await GetStorageProvider().TryGetFolderFromPath(result));
results.Items = new[] { result }; results.Items = new[] { result };
resultsVisible.IsVisible = true; resultsVisible.IsVisible = true;
} }
@ -127,7 +152,7 @@ namespace ControlCatalog.Pages
var result = await new OpenFileDialog() var result = await new OpenFileDialog()
{ {
Title = "Select both", Title = "Select both",
Directory = lastSelectedDirectory?.TryGetUri(out var path) == true ? path.LocalPath : null, Directory = lastSelectedDirectory?.Path is {IsAbsoluteUri:true} path ? path.LocalPath : null,
AllowMultiple = true AllowMultiple = true
}.ShowManagedAsync(GetWindow(), new ManagedFileDialogOptions }.ShowManagedAsync(GetWindow(), new ManagedFileDialogOptions
{ {
@ -210,7 +235,7 @@ namespace ControlCatalog.Pages
#endif #endif
await reader.WriteLineAsync(openedFileContent.Text); await reader.WriteLineAsync(openedFileContent.Text);
lastSelectedDirectory = await file.GetParentAsync(); SetFolder(await file.GetParentAsync());
} }
await SetPickerResult(file is null ? null : new[] { file }); await SetPickerResult(file is null ? null : new[] { file });
@ -226,7 +251,7 @@ namespace ControlCatalog.Pages
await SetPickerResult(folders); await SetPickerResult(folders);
lastSelectedDirectory = folders.FirstOrDefault(); SetFolder(folders.FirstOrDefault());
}; };
this.Get<Button>("OpenFileFromBookmark").Click += async delegate this.Get<Button>("OpenFileFromBookmark").Click += async delegate
{ {
@ -243,10 +268,16 @@ namespace ControlCatalog.Pages
: null; : null;
await SetPickerResult(folder is null ? null : new[] { folder }); await SetPickerResult(folder is null ? null : new[] { folder });
SetFolder(folder);
lastSelectedDirectory = folder;
}; };
void SetFolder(IStorageFolder? folder)
{
ignoreTextChanged = true;
lastSelectedDirectory = folder;
currentFolderBox.Text = folder?.Path.LocalPath;
ignoreTextChanged = false;
}
async Task SetPickerResult(IReadOnlyCollection<IStorageItem>? items) async Task SetPickerResult(IReadOnlyCollection<IStorageItem>? items)
{ {
items ??= Array.Empty<IStorageItem>(); items ??= Array.Empty<IStorageItem>();
@ -297,10 +328,11 @@ namespace ControlCatalog.Pages
openedFileContent.Text = resultText; openedFileContent.Text = resultText;
lastSelectedDirectory = await item.GetParentAsync(); var parent = await item.GetParentAsync();
if (lastSelectedDirectory is not null) SetFolder(parent);
if (parent is not null)
{ {
mappedResults.Add(FullPathOrName(lastSelectedDirectory)); mappedResults.Add(FullPathOrName(parent));
} }
foreach (var selectedItem in items) foreach (var selectedItem in items)
@ -393,7 +425,7 @@ CanPickFolder: {storageProvider.CanPickFolder}";
private static string FullPathOrName(IStorageItem? item) private static string FullPathOrName(IStorageItem? item)
{ {
if (item is null) return "(null)"; if (item is null) return "(null)";
return item.TryGetUri(out var uri) ? uri.ToString() : item.Name; return item.Path is { IsAbsoluteUri: true } path ? path.ToString() : item.Name;
} }
Window GetWindow() => TopLevel.GetTopLevel(this) as Window ?? throw new NullReferenceException("Invalid Owner"); Window GetWindow() => TopLevel.GetTopLevel(this) as Window ?? throw new NullReferenceException("Invalid Owner");

4
samples/ControlCatalog/Pages/ExpanderPage.xaml.cs

@ -14,8 +14,8 @@ namespace ControlCatalog.Pages
var CollapsingDisabledExpander = this.Get<Expander>("CollapsingDisabledExpander"); var CollapsingDisabledExpander = this.Get<Expander>("CollapsingDisabledExpander");
var ExpandingDisabledExpander = this.Get<Expander>("ExpandingDisabledExpander"); var ExpandingDisabledExpander = this.Get<Expander>("ExpandingDisabledExpander");
CollapsingDisabledExpander.Collapsing += (s, e) => { e.Handled = true; }; CollapsingDisabledExpander.Collapsing += (s, e) => { e.Cancel = true; };
ExpandingDisabledExpander.Expanding += (s, e) => { e.Handled = true; }; ExpandingDisabledExpander.Expanding += (s, e) => { e.Cancel = true; };
} }
private void InitializeComponent() private void InitializeComponent()

2
samples/ControlCatalog/Pages/ScrollSnapPage.xaml

@ -8,7 +8,7 @@
x:DataType="pages:ScrollSnapPageViewModel"> x:DataType="pages:ScrollSnapPageViewModel">
<StackPanel Orientation="Vertical" Spacing="4"> <StackPanel Orientation="Vertical" Spacing="4">
<TextBlock TextWrapping="Wrap" <TextBlock TextWrapping="Wrap"
Classes="h2">Scrollviewer can snap supported content both vertically and horizontally. Snapping occurs from scrolling with touch or pen, or using the pointer wheel.</TextBlock> Classes="h2">Scrollviewer can snap supported content both vertically and horizontally. Snapping occurs from scrolling with touch or pen.</TextBlock>
<Grid RowDefinitions="Auto, Auto, Auto, Auto, Auto"> <Grid RowDefinitions="Auto, Auto, Auto, Auto, Auto">
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"

7
samples/GpuInterop/D3DDemo/D3D11DemoControl.cs

@ -2,21 +2,17 @@ using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
using SharpDX; using SharpDX;
using SharpDX.Direct2D1;
using SharpDX.Direct3D11; using SharpDX.Direct3D11;
using SharpDX.DXGI; using SharpDX.DXGI;
using SharpDX.Mathematics.Interop; using SharpDX.Mathematics.Interop;
using Buffer = SharpDX.Direct3D11.Buffer; using Buffer = SharpDX.Direct3D11.Buffer;
using DeviceContext = SharpDX.Direct2D1.DeviceContext;
using DxgiFactory1 = SharpDX.DXGI.Factory1; using DxgiFactory1 = SharpDX.DXGI.Factory1;
using Matrix = SharpDX.Matrix; using Matrix = SharpDX.Matrix;
using D3DDevice = SharpDX.Direct3D11.Device; using D3DDevice = SharpDX.Direct3D11.Device;
using DxgiResource = SharpDX.DXGI.Resource;
using FeatureLevel = SharpDX.Direct3D.FeatureLevel; using FeatureLevel = SharpDX.Direct3D.FeatureLevel;
using Vector3 = SharpDX.Vector3; using Vector3 = SharpDX.Vector3;
@ -80,7 +76,10 @@ public class D3D11DemoControl : DrawingSurfaceDemoBase
if (pixelSize == default) if (pixelSize == default)
return; return;
if (pixelSize != _lastSize) if (pixelSize != _lastSize)
{
_lastSize = pixelSize;
Resize(pixelSize); Resize(pixelSize);
}
using (_swapchain.BeginDraw(pixelSize, out var renderView)) using (_swapchain.BeginDraw(pixelSize, out var renderView))
{ {

4
samples/RenderDemo/App.xaml.cs

@ -29,10 +29,6 @@ namespace RenderDemo
.With(new Win32PlatformOptions .With(new Win32PlatformOptions
{ {
OverlayPopups = true, OverlayPopups = true,
})
.With(new X11PlatformOptions
{
UseCompositor = true
}) })
.UsePlatformDetect() .UsePlatformDetect()
.LogToTrace(); .LogToTrace();

17
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -32,7 +32,6 @@ namespace Avalonia.Android
public static AndroidPlatformOptions Options { get; private set; } public static AndroidPlatformOptions Options { get; private set; }
internal static Compositor Compositor { get; private set; } internal static Compositor Compositor { get; private set; }
internal static PlatformRenderInterfaceContextManager RenderInterface { get; private set; }
public static void Initialize() public static void Initialize()
{ {
@ -55,16 +54,11 @@ namespace Avalonia.Android
EglPlatformGraphics.TryInitialize(); EglPlatformGraphics.TryInitialize();
} }
if (Options.UseCompositor) Compositor = new Compositor(
{ AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(),
Compositor = new Compositor( AvaloniaLocator.Current.GetService<IPlatformGraphics>());
AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(),
AvaloniaLocator.Current.GetService<IPlatformGraphics>());
}
else
RenderInterface =
new PlatformRenderInterfaceContextManager(AvaloniaLocator.Current
.GetService<IPlatformGraphics>());
} }
} }
@ -72,6 +66,5 @@ namespace Avalonia.Android
{ {
public bool UseDeferredRendering { get; set; } = false; public bool UseDeferredRendering { get; set; } = false;
public bool UseGpu { get; set; } = true; public bool UseGpu { get; set; } = true;
public bool UseCompositor { get; set; } = true;
} }
} }

10
src/Android/Avalonia.Android/AvaloniaMainActivity.cs

@ -1,6 +1,8 @@
using System; using System;
using Android.App; using Android.App;
using Android.Content; using Android.Content;
using Android.Content.PM;
using Android.Content.Res;
using Android.OS; using Android.OS;
using Android.Runtime; using Android.Runtime;
using Android.Views; using Android.Views;
@ -13,6 +15,7 @@ namespace Avalonia.Android
internal static object ViewContent; internal static object ViewContent;
public Action<int, Result, Intent> ActivityResult { get; set; } public Action<int, Result, Intent> ActivityResult { get; set; }
public Action<int, string[], Permission[]> RequestPermissionsResult { get; set; }
internal AvaloniaView View; internal AvaloniaView View;
private GlobalLayoutListener _listener; private GlobalLayoutListener _listener;
@ -82,6 +85,13 @@ namespace Avalonia.Android
ActivityResult?.Invoke(requestCode, resultCode, data); ActivityResult?.Invoke(requestCode, resultCode, data);
} }
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, Permission[] grantResults)
{
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
RequestPermissionsResult?.Invoke(requestCode, permissions, grantResults);
}
class GlobalLayoutListener : Java.Lang.Object, ViewTreeObserver.IOnGlobalLayoutListener class GlobalLayoutListener : Java.Lang.Object, ViewTreeObserver.IOnGlobalLayoutListener
{ {
private AvaloniaView _view; private AvaloniaView _view;

3
src/Android/Avalonia.Android/IActivityResultHandler.cs

@ -1,11 +1,14 @@
using System; using System;
using Android.App; using Android.App;
using Android.Content; using Android.Content;
using Android.Content.PM;
namespace Avalonia.Android namespace Avalonia.Android
{ {
public interface IActivityResultHandler public interface IActivityResultHandler
{ {
public Action<int, Result, Intent> ActivityResult { get; set; } public Action<int, Result, Intent> ActivityResult { get; set; }
public Action<int, string[], Permission[]> RequestPermissionsResult { get; set; }
} }
} }

4
src/Android/Avalonia.Android/Platform/AndroidSystemNavigationManager.cs

@ -4,11 +4,11 @@ using Avalonia.Platform;
namespace Avalonia.Android.Platform namespace Avalonia.Android.Platform
{ {
internal class AndroidSystemNavigationManager : ISystemNavigationManager internal class AndroidSystemNavigationManagerImpl : ISystemNavigationManagerImpl
{ {
public event EventHandler<RoutedEventArgs> BackRequested; public event EventHandler<RoutedEventArgs> BackRequested;
public AndroidSystemNavigationManager(IActivityNavigationService? navigationService) public AndroidSystemNavigationManagerImpl(IActivityNavigationService? navigationService)
{ {
if(navigationService != null) if(navigationService != null)
{ {

52
src/Android/Avalonia.Android/Platform/PlatformSupport.cs

@ -0,0 +1,52 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.Content.PM;
namespace Avalonia.Android.Platform;
internal static class PlatformSupport
{
private static int s_lastRequestCode = 20000;
public static int GetNextRequestCode() => s_lastRequestCode++;
public static async Task<bool> CheckPermission(this Activity activity, string permission)
{
if (activity is not IActivityResultHandler mainActivity)
{
throw new InvalidOperationException("Main activity must implement IActivityResultHandler interface.");
}
if (!OperatingSystem.IsAndroidVersionAtLeast(23))
{
return true;
}
if (activity.CheckSelfPermission(permission) == Permission.Granted)
{
return true;
}
var currentRequestCode = GetNextRequestCode();
var tcs = new TaskCompletionSource<bool>();
mainActivity.RequestPermissionsResult += RequestPermissionsResult;
activity.RequestPermissions(new [] { permission }, currentRequestCode);
return await tcs.Task;
void RequestPermissionsResult(int requestCode, string[] arg2, Permission[] arg3)
{
if (currentRequestCode != requestCode)
{
return;
}
mainActivity.RequestPermissionsResult -= RequestPermissionsResult;
_ = tcs.TrySetResult(arg3.All(p => p == Permission.Granted));
}
}
}

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

@ -31,9 +31,7 @@ using Android.Graphics.Drawables;
namespace Avalonia.Android.Platform.SkiaPlatform namespace Avalonia.Android.Platform.SkiaPlatform
{ {
class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo, class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo
ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider,
ITopLevelWithSystemNavigationManager
{ {
private readonly IGlPlatformSurface _gl; private readonly IGlPlatformSurface _gl;
private readonly IFramebufferPlatformSurface _framebuffer; private readonly IFramebufferPlatformSurface _framebuffer;
@ -41,6 +39,9 @@ namespace Avalonia.Android.Platform.SkiaPlatform
private readonly AndroidKeyboardEventsHelper<TopLevelImpl> _keyboardHelper; private readonly AndroidKeyboardEventsHelper<TopLevelImpl> _keyboardHelper;
private readonly AndroidMotionEventsHelper _pointerHelper; private readonly AndroidMotionEventsHelper _pointerHelper;
private readonly AndroidInputMethod<ViewImpl> _textInputMethod; private readonly AndroidInputMethod<ViewImpl> _textInputMethod;
private readonly INativeControlHostImpl _nativeControlHost;
private readonly IStorageProvider _storageProvider;
private readonly ISystemNavigationManagerImpl _systemNavigationManager;
private ViewImpl _view; private ViewImpl _view;
public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false) public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false)
@ -57,10 +58,10 @@ namespace Avalonia.Android.Platform.SkiaPlatform
MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels, MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels,
_view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling); _view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling);
NativeControlHost = new AndroidNativeControlHostImpl(avaloniaView); _nativeControlHost = new AndroidNativeControlHostImpl(avaloniaView);
StorageProvider = new AndroidStorageProvider((Activity)avaloniaView.Context); _storageProvider = new AndroidStorageProvider((Activity)avaloniaView.Context);
SystemNavigationManager = new AndroidSystemNavigationManager(avaloniaView.Context as IActivityNavigationService); _systemNavigationManager = new AndroidSystemNavigationManagerImpl(avaloniaView.Context as IActivityNavigationService);
} }
public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) => public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) =>
@ -109,16 +110,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public IEnumerable<object> Surfaces => new object[] { _gl, _framebuffer, Handle }; public IEnumerable<object> Surfaces => new object[] { _gl, _framebuffer, Handle };
public IRenderer CreateRenderer(IRenderRoot root) => public IRenderer CreateRenderer(IRenderRoot root) =>
AndroidPlatform.Options.UseCompositor new CompositingRenderer(root, AndroidPlatform.Compositor, () => Surfaces);
? new CompositingRenderer(root, AndroidPlatform.Compositor, () => Surfaces)
: AndroidPlatform.Options.UseDeferredRendering
? new DeferredRenderer(root, AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(),
() => AndroidPlatform.RenderInterface.CreateRenderTarget(Surfaces),
AndroidPlatform.RenderInterface)
{ RenderOnlyOnRenderThread = true }
: new ImmediateRenderer((Visual)root,
() => AndroidPlatform.RenderInterface.CreateRenderTarget(Surfaces),
AndroidPlatform.RenderInterface);
public virtual void Hide() public virtual void Hide()
{ {
@ -303,14 +295,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public double Scaling => RenderScaling; public double Scaling => RenderScaling;
public ITextInputMethodImpl TextInputMethod => _textInputMethod;
public INativeControlHostImpl NativeControlHost { get; }
public IStorageProvider StorageProvider { get; }
public ISystemNavigationManager SystemNavigationManager { get; }
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel)
{ {
if (TransparencyLevel != transparencyLevel) if (TransparencyLevel != transparencyLevel)
@ -395,6 +379,31 @@ namespace Avalonia.Android.Platform.SkiaPlatform
} }
} }
} }
public virtual object TryGetFeature(Type featureType)
{
if (featureType == typeof(IStorageProvider))
{
return _storageProvider;
}
if (featureType == typeof(ITextInputMethodImpl))
{
return _textInputMethod;
}
if (featureType == typeof(ISystemNavigationManagerImpl))
{
return _systemNavigationManager;
}
if (featureType == typeof(INativeControlHostImpl))
{
return _nativeControlHost;
}
return null;
}
} }
internal class AvaloniaInputConnection : BaseInputConnection internal class AvaloniaInputConnection : BaseInputConnection

101
src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs

@ -2,10 +2,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Android;
using Android.App;
using Android.Content; using Android.Content;
using Android.Provider; using Android.Provider;
using Avalonia.Logging; using Avalonia.Logging;
@ -19,41 +20,48 @@ namespace Avalonia.Android.Platform.Storage;
internal abstract class AndroidStorageItem : IStorageBookmarkItem internal abstract class AndroidStorageItem : IStorageBookmarkItem
{ {
private Context? _context; private Activity? _activity;
private readonly bool _needsExternalFilesPermission;
protected AndroidStorageItem(Context context, AndroidUri uri) protected AndroidStorageItem(Activity activity, AndroidUri uri, bool needsExternalFilesPermission)
{ {
_context = context; _activity = activity;
_needsExternalFilesPermission = needsExternalFilesPermission;
Uri = uri; Uri = uri;
} }
internal AndroidUri Uri { get; } internal AndroidUri Uri { get; }
protected Activity Activity => _activity ?? throw new ObjectDisposedException(nameof(AndroidStorageItem));
protected Context Context => _context ?? throw new ObjectDisposedException(nameof(AndroidStorageItem)); public virtual string Name => GetColumnValue(Activity, Uri, MediaStore.IMediaColumns.DisplayName)
public string Name => GetColumnValue(Context, Uri, MediaStore.IMediaColumns.DisplayName)
?? Uri.PathSegments?.LastOrDefault() ?? string.Empty; ?? Uri.PathSegments?.LastOrDefault() ?? string.Empty;
public Uri Path => new(Uri.ToString()!);
public bool CanBookmark => true; public bool CanBookmark => true;
public Task<string?> SaveBookmarkAsync() public async Task<string?> SaveBookmarkAsync()
{ {
Context.ContentResolver?.TakePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission); if (!await EnsureExternalFilesPermission(false))
return Task.FromResult(Uri.ToString()); {
} return null;
}
public Task ReleaseBookmarkAsync() Activity.ContentResolver?.TakePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
{ return Uri.ToString();
Context.ContentResolver?.ReleasePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
return Task.CompletedTask;
} }
public bool TryGetUri([NotNullWhen(true)] out Uri? uri) public async Task ReleaseBookmarkAsync()
{ {
uri = new Uri(Uri.ToString()!); if (!await EnsureExternalFilesPermission(false))
return true; {
} return;
}
Activity.ContentResolver?.ReleasePersistableUriPermission(Uri, ActivityFlags.GrantWriteUriPermission | ActivityFlags.GrantReadUriPermission);
}
public abstract Task<StorageItemProperties> GetBasicPropertiesAsync(); public abstract Task<StorageItemProperties> GetBasicPropertiesAsync();
protected string? GetColumnValue(Context context, AndroidUri contentUri, string column, string? selection = null, string[]? selectionArgs = null) protected string? GetColumnValue(Context context, AndroidUri contentUri, string column, string? selection = null, string[]? selectionArgs = null)
@ -77,29 +85,44 @@ internal abstract class AndroidStorageItem : IStorageBookmarkItem
return null; return null;
} }
public Task<IStorageFolder?> GetParentAsync() public async Task<IStorageFolder?> GetParentAsync()
{ {
if (!await EnsureExternalFilesPermission(false))
{
return null;
}
using var javaFile = new JavaFile(Uri.Path!); using var javaFile = new JavaFile(Uri.Path!);
// Java file represents files AND directories. Don't be confused. // Java file represents files AND directories. Don't be confused.
if (javaFile.ParentFile is {} parentFile if (javaFile.ParentFile is {} parentFile
&& AndroidUri.FromFile(parentFile) is {} androidUri) && AndroidUri.FromFile(parentFile) is {} androidUri)
{ {
return Task.FromResult<IStorageFolder?>(new AndroidStorageFolder(Context, androidUri)); return new AndroidStorageFolder(Activity, androidUri, false);
} }
return Task.FromResult<IStorageFolder?>(null); return null;
} }
protected async Task<bool> EnsureExternalFilesPermission(bool write)
{
if (!_needsExternalFilesPermission)
{
return true;
}
return await _activity.CheckPermission(Manifest.Permission.ReadExternalStorage);
}
public void Dispose() public void Dispose()
{ {
_context = null; _activity = null;
} }
} }
internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder
{ {
public AndroidStorageFolder(Context context, AndroidUri uri) : base(context, uri) public AndroidStorageFolder(Activity activity, AndroidUri uri, bool needsExternalFilesPermission) : base(activity, uri, needsExternalFilesPermission)
{ {
} }
@ -110,6 +133,11 @@ internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmar
public async Task<IReadOnlyList<IStorageItem>> GetItemsAsync() public async Task<IReadOnlyList<IStorageItem>> GetItemsAsync()
{ {
if (!await EnsureExternalFilesPermission(false))
{
return Array.Empty<IStorageItem>();
}
using var javaFile = new JavaFile(Uri.Path!); using var javaFile = new JavaFile(Uri.Path!);
// Java file represents files AND directories. Don't be confused. // Java file represents files AND directories. Don't be confused.
@ -124,8 +152,8 @@ internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmar
.Where(t => t.uri is not null) .Where(t => t.uri is not null)
.Select(t => t.file switch .Select(t => t.file switch
{ {
{ IsFile: true } => (IStorageItem)new AndroidStorageFile(Context, t.uri!), { IsFile: true } => (IStorageItem)new AndroidStorageFile(Activity, t.uri!),
{ IsDirectory: true } => new AndroidStorageFolder(Context, t.uri!), { IsDirectory: true } => new AndroidStorageFolder(Activity, t.uri!, false),
_ => null _ => null
}) })
.Where(i => i is not null) .Where(i => i is not null)
@ -133,9 +161,20 @@ internal sealed class AndroidStorageFolder : AndroidStorageItem, IStorageBookmar
} }
} }
internal sealed class WellKnownAndroidStorageFolder : AndroidStorageFolder
{
public WellKnownAndroidStorageFolder(Activity activity, string identifier, AndroidUri uri, bool needsExternalFilesPermission)
: base(activity, uri, needsExternalFilesPermission)
{
Name = identifier;
}
public override string Name { get; }
}
internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkFile internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkFile
{ {
public AndroidStorageFile(Context context, AndroidUri uri) : base(context, uri) public AndroidStorageFile(Activity activity, AndroidUri uri) : base(activity, uri, false)
{ {
} }
@ -143,10 +182,10 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF
public bool CanOpenWrite => true; public bool CanOpenWrite => true;
public Task<Stream> OpenReadAsync() => Task.FromResult(OpenContentStream(Context, Uri, false) public Task<Stream> OpenReadAsync() => Task.FromResult(OpenContentStream(Activity, Uri, false)
?? throw new InvalidOperationException("Failed to open content stream")); ?? throw new InvalidOperationException("Failed to open content stream"));
public Task<Stream> OpenWriteAsync() => Task.FromResult(OpenContentStream(Context, Uri, true) public Task<Stream> OpenWriteAsync() => Task.FromResult(OpenContentStream(Activity, Uri, true)
?? throw new InvalidOperationException("Failed to open content stream")); ?? throw new InvalidOperationException("Failed to open content stream"));
private Stream? OpenContentStream(Context context, AndroidUri uri, bool isOutput) private Stream? OpenContentStream(Context context, AndroidUri uri, bool isOutput)
@ -210,7 +249,7 @@ internal sealed class AndroidStorageFile : AndroidStorageItem, IStorageBookmarkF
MediaStore.IMediaColumns.Size, MediaStore.IMediaColumns.DateAdded, MediaStore.IMediaColumns.Size, MediaStore.IMediaColumns.DateAdded,
MediaStore.IMediaColumns.DateModified MediaStore.IMediaColumns.DateModified
}; };
using var cursor = Context.ContentResolver!.Query(Uri, projection, null, null, null); using var cursor = Activity.ContentResolver!.Query(Uri, projection, null, null, null);
if (cursor?.MoveToFirst() == true) if (cursor?.MoveToFirst() == true)
{ {

124
src/Android/Avalonia.Android/Platform/Storage/AndroidStorageProvider.cs

@ -4,18 +4,21 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Android;
using Android.App; using Android.App;
using Android.Content; using Android.Content;
using Android.Provider; using Android.Provider;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using Java.Lang;
using AndroidUri = Android.Net.Uri; using AndroidUri = Android.Net.Uri;
using Exception = System.Exception;
using JavaFile = Java.IO.File;
namespace Avalonia.Android.Platform.Storage; namespace Avalonia.Android.Platform.Storage;
internal class AndroidStorageProvider : IStorageProvider internal class AndroidStorageProvider : IStorageProvider
{ {
private readonly Activity _activity; private readonly Activity _activity;
private int _lastRequestCode = 20000;
public AndroidStorageProvider(Activity activity) public AndroidStorageProvider(Activity activity)
{ {
@ -31,7 +34,108 @@ internal class AndroidStorageProvider : IStorageProvider
public Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark) public Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark)
{ {
var uri = AndroidUri.Parse(bookmark) ?? throw new ArgumentException("Couldn't parse Bookmark value", nameof(bookmark)); var uri = AndroidUri.Parse(bookmark) ?? throw new ArgumentException("Couldn't parse Bookmark value", nameof(bookmark));
return Task.FromResult<IStorageBookmarkFolder?>(new AndroidStorageFolder(_activity, uri)); return Task.FromResult<IStorageBookmarkFolder?>(new AndroidStorageFolder(_activity, uri, false));
}
public async Task<IStorageFile?> TryGetFileFromPath(Uri filePath)
{
if (filePath is null)
{
throw new ArgumentNullException(nameof(filePath));
}
if (filePath is not { IsAbsoluteUri: true, Scheme: "file" or "content" })
{
throw new ArgumentException("File path is expected to be an absolute link with \"file\" or \"content\" scheme.");
}
var androidUri = AndroidUri.Parse(filePath.ToString());
if (androidUri?.Path is not {} androidUriPath)
{
return null;
}
var hasPerms = await _activity.CheckPermission(Manifest.Permission.ReadExternalStorage);
if (!hasPerms)
{
throw new SecurityException("Application doesn't have ReadExternalStorage permission. Make sure android manifest has this permission defined and user allowed it.");
}
var javaFile = new JavaFile(androidUriPath);
if (javaFile.Exists() && javaFile.IsFile)
{
return null;
}
return new AndroidStorageFile(_activity, androidUri);
}
public async Task<IStorageFolder?> TryGetFolderFromPath(Uri folderPath)
{
if (folderPath is null)
{
throw new ArgumentNullException(nameof(folderPath));
}
if (folderPath is not { IsAbsoluteUri: true, Scheme: "file" or "content" })
{
throw new ArgumentException("Folder path is expected to be an absolute link with \"file\" or \"content\" scheme.");
}
var androidUri = AndroidUri.Parse(folderPath.ToString());
if (androidUri?.Path is not {} androidUriPath)
{
return null;
}
var hasPerms = await _activity.CheckPermission(Manifest.Permission.ReadExternalStorage);
if (!hasPerms)
{
throw new SecurityException("Application doesn't have ReadExternalStorage permission. Make sure android manifest has this permission defined and user allowed it.");
}
var javaFile = new JavaFile(androidUriPath);
if (javaFile.Exists() && javaFile.IsDirectory)
{
return null;
}
return new AndroidStorageFolder(_activity, androidUri, false);
}
public Task<IStorageFolder?> TryGetWellKnownFolder(WellKnownFolder wellKnownFolder)
{
var dirCode = wellKnownFolder switch
{
WellKnownFolder.Desktop => null,
WellKnownFolder.Documents => global::Android.OS.Environment.DirectoryDocuments,
WellKnownFolder.Downloads => global::Android.OS.Environment.DirectoryDownloads,
WellKnownFolder.Music => global::Android.OS.Environment.DirectoryMusic,
WellKnownFolder.Pictures => global::Android.OS.Environment.DirectoryPictures,
WellKnownFolder.Videos => global::Android.OS.Environment.DirectoryMovies,
_ => throw new ArgumentOutOfRangeException(nameof(wellKnownFolder), wellKnownFolder, null)
};
if (dirCode is null)
{
return Task.FromResult<IStorageFolder?>(null);
}
var dir = _activity.GetExternalFilesDir(dirCode);
if (dir is null || !dir.Exists())
{
return Task.FromResult<IStorageFolder?>(null);
}
var uri = AndroidUri.FromFile(dir);
if (uri is null)
{
return Task.FromResult<IStorageFolder?>(null);
}
// To make TryGetWellKnownFolder API easier to use, we don't check for the permissions.
// It will work with file picker activities, but it will fail on any direct access to the folder, like getting list of children.
// We pass "needsExternalFilesPermission" parameter here, so folder itself can check for permissions on any FS access.
return Task.FromResult<IStorageFolder?>(new WellKnownAndroidStorageFolder(_activity, dirCode, uri, true));
} }
public Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark) public Task<IStorageBookmarkFile?> OpenFileBookmarkAsync(string bookmark)
@ -110,19 +214,21 @@ internal class AndroidStorageProvider : IStorageProvider
var pickerIntent = Intent.CreateChooser(intent, options.Title ?? "Select folder"); var pickerIntent = Intent.CreateChooser(intent, options.Title ?? "Select folder");
var uris = await StartActivity(pickerIntent, false); var uris = await StartActivity(pickerIntent, false);
return uris.Select(u => new AndroidStorageFolder(_activity, u)).ToArray(); return uris.Select(u => new AndroidStorageFolder(_activity, u, false)).ToArray();
} }
private async Task<List<AndroidUri>> StartActivity(Intent? pickerIntent, bool singleResult) private async Task<List<AndroidUri>> StartActivity(Intent? pickerIntent, bool singleResult)
{ {
var resultList = new List<AndroidUri>(1); var resultList = new List<AndroidUri>(1);
var tcs = new TaskCompletionSource<Intent?>(); var tcs = new TaskCompletionSource<Intent?>();
var currentRequestCode = _lastRequestCode++; var currentRequestCode = PlatformSupport.GetNextRequestCode();
if (_activity is IActivityResultHandler mainActivity) if (!(_activity is IActivityResultHandler mainActivity))
{ {
mainActivity.ActivityResult += OnActivityResult; throw new InvalidOperationException("Main activity must implement IActivityResultHandler interface.");
} }
mainActivity.ActivityResult += OnActivityResult;
_activity.StartActivityForResult(pickerIntent, currentRequestCode); _activity.StartActivityForResult(pickerIntent, currentRequestCode);
var result = await tcs.Task; var result = await tcs.Task;
@ -161,11 +267,7 @@ internal class AndroidStorageProvider : IStorageProvider
return; return;
} }
mainActivity.ActivityResult -= OnActivityResult;
if (_activity is IActivityResultHandler mainActivity)
{
mainActivity.ActivityResult -= OnActivityResult;
}
_ = tcs.TrySetResult(resultCode == Result.Ok ? data : null); _ = tcs.TrySetResult(resultCode == Result.Ok ? data : null);
} }

26
src/Avalonia.Base/Animation/Animators/GradientBrushAnimator.cs

@ -4,6 +4,7 @@ using System.Collections.Generic;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
using Avalonia.Media.Transformation;
#nullable enable #nullable enable
@ -30,7 +31,7 @@ namespace Avalonia.Animation.Animators
return new ImmutableRadialGradientBrush( return new ImmutableRadialGradientBrush(
InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops), InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops),
s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity), s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity),
oldValue.Transform is { } ? new ImmutableTransform(oldValue.Transform.Value) : null, InterpolateTransform(progress, oldValue.Transform, newValue.Transform),
s_relativePointAnimator.Interpolate(progress, oldValue.TransformOrigin, newValue.TransformOrigin), s_relativePointAnimator.Interpolate(progress, oldValue.TransformOrigin, newValue.TransformOrigin),
oldValue.SpreadMethod, oldValue.SpreadMethod,
s_relativePointAnimator.Interpolate(progress, oldRadial.Center, newRadial.Center), s_relativePointAnimator.Interpolate(progress, oldRadial.Center, newRadial.Center),
@ -41,7 +42,7 @@ namespace Avalonia.Animation.Animators
return new ImmutableConicGradientBrush( return new ImmutableConicGradientBrush(
InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops), InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops),
s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity), s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity),
oldValue.Transform is { } ? new ImmutableTransform(oldValue.Transform.Value) : null, InterpolateTransform(progress, oldValue.Transform, newValue.Transform),
s_relativePointAnimator.Interpolate(progress, oldValue.TransformOrigin, newValue.TransformOrigin), s_relativePointAnimator.Interpolate(progress, oldValue.TransformOrigin, newValue.TransformOrigin),
oldValue.SpreadMethod, oldValue.SpreadMethod,
s_relativePointAnimator.Interpolate(progress, oldConic.Center, newConic.Center), s_relativePointAnimator.Interpolate(progress, oldConic.Center, newConic.Center),
@ -51,7 +52,7 @@ namespace Avalonia.Animation.Animators
return new ImmutableLinearGradientBrush( return new ImmutableLinearGradientBrush(
InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops), InterpolateStops(progress, oldValue.GradientStops, newValue.GradientStops),
s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity), s_doubleAnimator.Interpolate(progress, oldValue.Opacity, newValue.Opacity),
oldValue.Transform is { } ? new ImmutableTransform(oldValue.Transform.Value) : null, InterpolateTransform(progress, oldValue.Transform, newValue.Transform),
s_relativePointAnimator.Interpolate(progress, oldValue.TransformOrigin, newValue.TransformOrigin), s_relativePointAnimator.Interpolate(progress, oldValue.TransformOrigin, newValue.TransformOrigin),
oldValue.SpreadMethod, oldValue.SpreadMethod,
s_relativePointAnimator.Interpolate(progress, oldLinear.StartPoint, newLinear.StartPoint), s_relativePointAnimator.Interpolate(progress, oldLinear.StartPoint, newLinear.StartPoint),
@ -72,6 +73,25 @@ namespace Avalonia.Animation.Animators
return control.Bind((AvaloniaProperty<IBrush?>)Property, instance, BindingPriority.Animation); return control.Bind((AvaloniaProperty<IBrush?>)Property, instance, BindingPriority.Animation);
} }
private static ImmutableTransform? InterpolateTransform(double progress,
ITransform? oldTransform, ITransform? newTransform)
{
if (oldTransform is TransformOperations oldTransformOperations
&& newTransform is TransformOperations newTransformOperations)
{
return new ImmutableTransform(TransformOperations
.Interpolate(oldTransformOperations, newTransformOperations, progress).Value);
}
if (oldTransform is not null)
{
return new ImmutableTransform(oldTransform.Value);
}
return null;
}
private static IReadOnlyList<ImmutableGradientStop> InterpolateStops(double progress, IReadOnlyList<IGradientStop> oldValue, IReadOnlyList<IGradientStop> newValue) private static IReadOnlyList<ImmutableGradientStop> InterpolateStops(double progress, IReadOnlyList<IGradientStop> oldValue, IReadOnlyList<IGradientStop> newValue)
{ {
var resultCount = Math.Max(oldValue.Count, newValue.Count); var resultCount = Math.Max(oldValue.Count, newValue.Count);

4
src/Avalonia.Base/Avalonia.Base.csproj

@ -70,8 +70,4 @@
<ItemGroup Label="Build dependency"> <ItemGroup Label="Build dependency">
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Avalonia.Build.Tasks\Avalonia.Build.Tasks.csproj" SetTargetFramework="TargetFramework=netstandard2.0" ReferenceOutputAssembly="false" SkipGetTargetFrameworkProperties="true" /> <ProjectReference Include="$(MSBuildThisFileDirectory)\..\Avalonia.Build.Tasks\Avalonia.Build.Tasks.csproj" SetTargetFramework="TargetFramework=netstandard2.0" ReferenceOutputAssembly="false" SkipGetTargetFrameworkProperties="true" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Rendering\Composition\Utils" />
</ItemGroup>
</Project> </Project>

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

@ -1,5 +1,6 @@
using System; using System;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Metadata;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Input namespace Avalonia.Input
@ -32,7 +33,9 @@ namespace Avalonia.Input
return point; return point;
} }
internal DragEventArgs(RoutedEvent<DragEventArgs> routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers) [Unstable]
[Obsolete("This constructor might be removed in 12.0. For unit testing, consider using DragDrop.DoDragDrop or IHeadlessWindow.DragDrop.")]
public DragEventArgs(RoutedEvent<DragEventArgs> routedEvent, IDataObject data, Interactive target, Point targetLocation, KeyModifiers keyModifiers)
: base(routedEvent) : base(routedEvent)
{ {
Data = data; Data = data;

7
src/Avalonia.Base/Input/GotFocusEventArgs.cs

@ -7,19 +7,18 @@ namespace Avalonia.Input
/// </summary> /// </summary>
public class GotFocusEventArgs : RoutedEventArgs public class GotFocusEventArgs : RoutedEventArgs
{ {
internal GotFocusEventArgs() public GotFocusEventArgs() : base(InputElement.GotFocusEvent)
{ {
} }
/// <summary> /// <summary>
/// Gets or sets a value indicating how the change in focus occurred. /// Gets or sets a value indicating how the change in focus occurred.
/// </summary> /// </summary>
public NavigationMethod NavigationMethod { get; set; } public NavigationMethod NavigationMethod { get; init; }
/// <summary> /// <summary>
/// Gets or sets any key modifiers active at the time of focus. /// Gets or sets any key modifiers active at the time of focus.
/// </summary> /// </summary>
public KeyModifiers KeyModifiers { get; set; } public KeyModifiers KeyModifiers { get; init; }
} }
} }

11
src/Avalonia.Base/Input/KeyEventArgs.cs

@ -5,15 +5,10 @@ namespace Avalonia.Input
{ {
public class KeyEventArgs : RoutedEventArgs public class KeyEventArgs : RoutedEventArgs
{ {
public KeyEventArgs() public IKeyboardDevice? Device { get; init; }
{
} public Key Key { get; init; }
public IKeyboardDevice? Device { get; set; } public KeyModifiers KeyModifiers { get; init; }
public Key Key { get; set; }
public KeyModifiers KeyModifiers { get; set; }
} }
} }

1
src/Avalonia.Base/Input/KeyboardDevice.cs

@ -156,7 +156,6 @@ namespace Avalonia.Input
interactive?.RaiseEvent(new GotFocusEventArgs interactive?.RaiseEvent(new GotFocusEventArgs
{ {
RoutedEvent = InputElement.GotFocusEvent,
NavigationMethod = method, NavigationMethod = method,
KeyModifiers = keyModifiers, KeyModifiers = keyModifiers,
}); });

8
src/Avalonia.Base/Input/PointerDeltaEventArgs.cs

@ -1,13 +1,17 @@
using System;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Metadata;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Input namespace Avalonia.Input
{ {
public class PointerDeltaEventArgs : PointerEventArgs public class PointerDeltaEventArgs : PointerEventArgs
{ {
public Vector Delta { get; set; } public Vector Delta { get; }
internal PointerDeltaEventArgs(RoutedEvent routedEvent, object? source, [Unstable]
[Obsolete("This constructor might be removed in 12.0.")]
public PointerDeltaEventArgs(RoutedEvent routedEvent, object? source,
IPointer pointer, Visual rootVisual, Point rootVisualPosition, ulong timestamp, IPointer pointer, Visual rootVisual, Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, KeyModifiers modifiers, Vector delta) PointerPointProperties properties, KeyModifiers modifiers, Vector delta)
: base(routedEvent, source, pointer, rootVisual, rootVisualPosition, : base(routedEvent, source, pointer, rootVisual, rootVisualPosition,

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

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Metadata;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Input namespace Avalonia.Input
@ -13,7 +14,9 @@ namespace Avalonia.Input
private readonly PointerPointProperties _properties; private readonly PointerPointProperties _properties;
private readonly Lazy<IReadOnlyList<RawPointerPoint>?>? _previousPoints; private readonly Lazy<IReadOnlyList<RawPointerPoint>?>? _previousPoints;
internal PointerEventArgs(RoutedEvent routedEvent, [Unstable]
[Obsolete("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow mouse methods.")]
public PointerEventArgs(RoutedEvent routedEvent,
object? source, object? source,
IPointer pointer, IPointer pointer,
Visual? rootVisual, Point rootVisualPosition, Visual? rootVisual, Point rootVisualPosition,
@ -124,7 +127,9 @@ namespace Avalonia.Input
public class PointerPressedEventArgs : PointerEventArgs public class PointerPressedEventArgs : PointerEventArgs
{ {
internal PointerPressedEventArgs( [Unstable]
[Obsolete("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow mouse methods.")]
public PointerPressedEventArgs(
object source, object source,
IPointer pointer, IPointer pointer,
Visual rootVisual, Point rootVisualPosition, Visual rootVisual, Point rootVisualPosition,
@ -143,7 +148,9 @@ namespace Avalonia.Input
public class PointerReleasedEventArgs : PointerEventArgs public class PointerReleasedEventArgs : PointerEventArgs
{ {
internal PointerReleasedEventArgs( [Unstable]
[Obsolete("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow mouse methods.")]
public PointerReleasedEventArgs(
object source, IPointer pointer, object source, IPointer pointer,
Visual rootVisual, Point rootVisualPosition, ulong timestamp, Visual rootVisual, Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, KeyModifiers modifiers, PointerPointProperties properties, KeyModifiers modifiers,
@ -164,7 +171,9 @@ namespace Avalonia.Input
{ {
public IPointer Pointer { get; } public IPointer Pointer { get; }
internal PointerCaptureLostEventArgs(object source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent) [Unstable]
[Obsolete("This constructor might be removed in 12.0. If you need to remove capture, use stable methods on the IPointer instance.,")]
public PointerCaptureLostEventArgs(object source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent)
{ {
Pointer = pointer; Pointer = pointer;
Source = source; Source = source;

8
src/Avalonia.Base/Input/PointerWheelEventArgs.cs

@ -1,13 +1,17 @@
using System;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Metadata;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Avalonia.Input namespace Avalonia.Input
{ {
public class PointerWheelEventArgs : PointerEventArgs public class PointerWheelEventArgs : PointerEventArgs
{ {
public Vector Delta { get; set; } public Vector Delta { get; }
internal PointerWheelEventArgs(object source, IPointer pointer, Visual rootVisual, [Unstable]
[Obsolete("This constructor might be removed in 12.0. For unit testing, consider using IHeadlessWindow.MouseWheel.")]
public PointerWheelEventArgs(object source, IPointer pointer, Visual rootVisual,
Point rootVisualPosition, ulong timestamp, Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, KeyModifiers modifiers, Vector delta) PointerPointProperties properties, KeyModifiers modifiers, Vector delta)
: base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition, : base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition,

2
src/Avalonia.Base/Input/Raw/RawTextInputEventArgs.cs

@ -12,6 +12,6 @@ namespace Avalonia.Input.Raw
Text = text; Text = text;
} }
public string Text { get; set; } public string Text { get; }
} }
} }

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

@ -14,7 +14,7 @@ namespace Avalonia.Input
public static int GetNextFreeId() => _nextId++; public static int GetNextFreeId() => _nextId++;
internal ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent) public ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent)
{ {
Id = id; Id = id;
Delta = delta; Delta = delta;
@ -25,7 +25,7 @@ namespace Avalonia.Input
{ {
public int Id { get; } public int Id { get; }
internal ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent) public ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent)
{ {
Id = id; Id = id;
} }

3
src/Avalonia.Base/Input/TappedEventArgs.cs

@ -1,3 +1,4 @@
using System;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -7,7 +8,7 @@ namespace Avalonia.Input
{ {
private readonly PointerEventArgs lastPointerEventArgs; private readonly PointerEventArgs lastPointerEventArgs;
internal TappedEventArgs(RoutedEvent routedEvent, PointerEventArgs lastPointerEventArgs) public TappedEventArgs(RoutedEvent routedEvent, PointerEventArgs lastPointerEventArgs)
: base(routedEvent) : base(routedEvent)
{ {
this.lastPointerEventArgs = lastPointerEventArgs; this.lastPointerEventArgs = lastPointerEventArgs;

8
src/Avalonia.Base/Input/TextInput/TransformTrackingHelper.cs

@ -105,5 +105,13 @@ namespace Avalonia.Input.TextInput
UnsubscribeFromParents(); UnsubscribeFromParents();
UpdateMatrix(); UpdateMatrix();
} }
public static IDisposable Track(Visual visual, Action<Visual, Matrix?> cb)
{
var rv = new TransformTrackingHelper();
rv.MatrixChanged += () => cb(visual, rv.Matrix);
rv.SetVisual(visual);
return rv;
}
} }
} }

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

@ -4,10 +4,6 @@ namespace Avalonia.Input
{ {
public class TextInputEventArgs : RoutedEventArgs public class TextInputEventArgs : RoutedEventArgs
{ {
public TextInputEventArgs()
{
}
public IKeyboardDevice? Device { get; set; } public IKeyboardDevice? Device { get; set; }
public string? Text { get; set; } public string? Text { get; set; }

7
src/Avalonia.Base/Input/VectorEventArgs.cs

@ -5,11 +5,6 @@ namespace Avalonia.Input
{ {
public class VectorEventArgs : RoutedEventArgs public class VectorEventArgs : RoutedEventArgs
{ {
internal VectorEventArgs() public Vector Vector { get; init; }
{
}
public Vector Vector { get; set; }
} }
} }

39
src/Avalonia.Base/Interactivity/CancelRoutedEventArgs.cs

@ -0,0 +1,39 @@
namespace Avalonia.Interactivity
{
/// <summary>
/// Provides state information and data specific to a cancelable routed event.
/// </summary>
public class CancelRoutedEventArgs : RoutedEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="CancelRoutedEventArgs"/> class.
/// </summary>
public CancelRoutedEventArgs()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CancelRoutedEventArgs"/> class.
/// </summary>
/// <param name="routedEvent">The routed event associated with these event args.</param>
public CancelRoutedEventArgs(RoutedEvent? routedEvent)
: base(routedEvent)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CancelRoutedEventArgs"/> class.
/// </summary>
/// <param name="routedEvent">The routed event associated with these event args.</param>
/// <param name="source">The source object that raised the routed event.</param>
public CancelRoutedEventArgs(RoutedEvent? routedEvent, object? source)
: base(routedEvent, source)
{
}
/// <summary>
/// Gets or sets a value indicating whether the routed event should be canceled.
/// </summary>
public bool Cancel { get; set; } = false;
}
}

2
src/Avalonia.Base/Layout/EffectiveViewportChangedEventArgs.cs

@ -7,7 +7,7 @@ namespace Avalonia.Layout
/// </summary> /// </summary>
public class EffectiveViewportChangedEventArgs : EventArgs public class EffectiveViewportChangedEventArgs : EventArgs
{ {
internal EffectiveViewportChangedEventArgs(Rect effectiveViewport) public EffectiveViewportChangedEventArgs(Rect effectiveViewport)
{ {
EffectiveViewport = effectiveViewport; EffectiveViewport = effectiveViewport;
} }

77
src/Avalonia.Base/Media/Imaging/Bitmap.cs

@ -1,5 +1,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -10,6 +12,7 @@ namespace Avalonia.Media.Imaging
/// </summary> /// </summary>
public class Bitmap : IBitmap public class Bitmap : IBitmap
{ {
private bool _isTranscoded;
/// <summary> /// <summary>
/// Loads a Bitmap from a stream and decodes at the desired width. Aspect ratio is maintained. /// Loads a Bitmap from a stream and decodes at the desired width. Aspect ratio is maintained.
/// This is more efficient than loading and then resizing. /// This is more efficient than loading and then resizing.
@ -100,7 +103,28 @@ namespace Avalonia.Media.Imaging
/// <param name="stride">The number of bytes per row.</param> /// <param name="stride">The number of bytes per row.</param>
public Bitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride) public Bitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride)
{ {
PlatformImpl = RefCountable.Create(GetFactory().LoadBitmap(format, alphaFormat, data, size, dpi, stride)); var factory = GetFactory();
if (factory.IsSupportedBitmapPixelFormat(format))
PlatformImpl = RefCountable.Create(factory.LoadBitmap(format, alphaFormat, data, size, dpi, stride));
else
{
var transcoded = Marshal.AllocHGlobal(size.Width * size.Height * 4);
var transcodedStride = size.Width * 4;
try
{
PixelFormatReader.Transcode(transcoded, data, size, stride, transcodedStride, format);
var transcodedAlphaFormat = format.HasAlpha ? alphaFormat : AlphaFormat.Opaque;
PlatformImpl = RefCountable.Create(factory.LoadBitmap(PixelFormat.Rgba8888, transcodedAlphaFormat,
transcoded, size, dpi, transcodedStride));
}
finally
{
Marshal.FreeHGlobal(transcoded);
}
_isTranscoded = true;
}
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -145,6 +169,57 @@ namespace Avalonia.Media.Imaging
PlatformImpl.Item.Save(stream, quality); PlatformImpl.Item.Save(stream, quality);
} }
public virtual PixelFormat? Format => (PlatformImpl.Item as IReadableBitmapImpl)?.Format;
protected internal unsafe void CopyPixelsCore(PixelRect sourceRect, IntPtr buffer, int bufferSize, int stride,
ILockedFramebuffer fb)
{
if ((sourceRect.Width <= 0 || sourceRect.Height <= 0) && (sourceRect.X != 0 || sourceRect.Y != 0))
throw new ArgumentOutOfRangeException(nameof(sourceRect));
if (sourceRect.X < 0 || sourceRect.Y < 0)
throw new ArgumentOutOfRangeException(nameof(sourceRect));
if (sourceRect.Width <= 0)
sourceRect = sourceRect.WithWidth(PixelSize.Width);
if (sourceRect.Height <= 0)
sourceRect = sourceRect.WithHeight(PixelSize.Height);
if (sourceRect.Right > PixelSize.Width || sourceRect.Bottom > PixelSize.Height)
throw new ArgumentOutOfRangeException(nameof(sourceRect));
int minStride = checked(((sourceRect.Width * fb.Format.BitsPerPixel) + 7) / 8);
if (stride < minStride)
throw new ArgumentOutOfRangeException(nameof(stride));
var minBufferSize = stride * sourceRect.Height;
if (minBufferSize > bufferSize)
throw new ArgumentOutOfRangeException(nameof(bufferSize));
for (var y = 0; y < sourceRect.Height; y++)
{
var srcAddress = fb.Address + fb.RowBytes * y;
var dstAddress = buffer + stride * y;
Unsafe.CopyBlock(dstAddress.ToPointer(), srcAddress.ToPointer(), (uint)minStride);
}
}
public virtual void CopyPixels(PixelRect sourceRect, IntPtr buffer, int bufferSize, int stride)
{
if (
Format == null
|| PlatformImpl.Item is not IReadableBitmapImpl readable
|| Format != readable.Format
)
throw new NotSupportedException("CopyPixels is not supported for this bitmap type");
if (_isTranscoded)
throw new NotSupportedException("CopyPixels is not supported for transcoded bitmaps");
using (var fb = readable.Lock())
CopyPixelsCore(sourceRect, buffer, bufferSize, stride, fb);
}
/// <inheritdoc/> /// <inheritdoc/>
void IImage.Draw( void IImage.Draw(
DrawingContext context, DrawingContext context,

51
src/Avalonia.Base/Media/Imaging/BitmapMemory.cs

@ -0,0 +1,51 @@
using System;
using System.Runtime.InteropServices;
using System.Threading;
using Avalonia.Platform;
namespace Avalonia.Media.Imaging;
internal class BitmapMemory : IDisposable
{
private readonly int _memorySize;
public BitmapMemory(PixelFormat format, PixelSize size)
{
Format = format;
Size = size;
RowBytes = (size.Width * format.BitsPerPixel + 7) / 8;
_memorySize = RowBytes * size.Height;
Address = Marshal.AllocHGlobal(_memorySize);
GC.AddMemoryPressure(_memorySize);
}
private void ReleaseUnmanagedResources()
{
if (Address != IntPtr.Zero)
{
GC.RemoveMemoryPressure(_memorySize);
Marshal.FreeHGlobal(Address);
}
}
public void Dispose()
{
ReleaseUnmanagedResources();
GC.SuppressFinalize(this);
}
~BitmapMemory()
{
ReleaseUnmanagedResources();
}
public IntPtr Address { get; private set; }
public PixelSize Size { get; }
public int RowBytes { get; }
public PixelFormat Format { get; }
public void CopyToRgba(IntPtr buffer, int rowBytes) =>
PixelFormatReader.Transcode(buffer, Address, Size, RowBytes, rowBytes, Format);
}

280
src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs

@ -0,0 +1,280 @@
using System;
using Avalonia.Platform;
namespace Avalonia.Media.Imaging;
internal struct Rgba8888Pixel
{
public byte R;
public byte G;
public byte B;
public byte A;
}
static unsafe class PixelFormatReader
{
public interface IPixelFormatReader
{
Rgba8888Pixel ReadNext();
void Reset(IntPtr address);
}
private static readonly Rgba8888Pixel s_white = new Rgba8888Pixel
{
A = 255,
B = 255,
G = 255,
R = 255
};
private static readonly Rgba8888Pixel s_black = new Rgba8888Pixel
{
A = 255,
B = 0,
G = 0,
R = 0
};
public unsafe struct BlackWhitePixelReader : IPixelFormatReader
{
private int _bit;
private byte* _address;
public void Reset(IntPtr address)
{
_address = (byte*)address;
_bit = 0;
}
public Rgba8888Pixel ReadNext()
{
var shift = 7 - _bit;
var value = (*_address >> shift) & 1;
_bit++;
if (_bit == 8)
{
_address++;
_bit = 0;
}
return value == 1 ? s_white : s_black;
}
}
public unsafe struct Gray2PixelReader : IPixelFormatReader
{
private int _bit;
private byte* _address;
public void Reset(IntPtr address)
{
_address = (byte*)address;
_bit = 0;
}
private static Rgba8888Pixel[] Palette = new[]
{
s_black,
new Rgba8888Pixel
{
A = 255, B = 0x55, G = 0x55, R = 0x55
},
new Rgba8888Pixel
{
A = 255, B = 0xAA, G = 0xAA, R = 0xAA
},
s_white
};
public Rgba8888Pixel ReadNext()
{
var shift = 6 - _bit;
var value = (byte)((*_address >> shift));
value = (byte)((value & 3));
_bit += 2;
if (_bit == 8)
{
_address++;
_bit = 0;
}
return Palette[value];
}
}
public unsafe struct Gray4PixelReader : IPixelFormatReader
{
private int _bit;
private byte* _address;
public void Reset(IntPtr address)
{
_address = (byte*)address;
_bit = 0;
}
public Rgba8888Pixel ReadNext()
{
var shift = 4 - _bit;
var value = (byte)((*_address >> shift));
value = (byte)((value & 0xF));
value = (byte)(value | (value << 4));
_bit += 4;
if (_bit == 8)
{
_address++;
_bit = 0;
}
return new Rgba8888Pixel
{
A = 255,
B = value,
G = value,
R = value
};
}
}
public unsafe struct Gray8PixelReader : IPixelFormatReader
{
private byte* _address;
public void Reset(IntPtr address)
{
_address = (byte*)address;
}
public Rgba8888Pixel ReadNext()
{
var value = *_address;
_address++;
return new Rgba8888Pixel
{
A = 255,
B = value,
G = value,
R = value
};
}
}
public unsafe struct Gray16PixelReader : IPixelFormatReader
{
private ushort* _address;
public Rgba8888Pixel ReadNext()
{
var value16 = *_address;
_address++;
var value8 = (byte)(value16 >> 8);
return new Rgba8888Pixel
{
A = 255,
B = value8,
G = value8,
R = value8
};
}
public void Reset(IntPtr address) => _address = (ushort*)address;
}
public unsafe struct Gray32FloatPixelReader : IPixelFormatReader
{
private byte* _address;
public Rgba8888Pixel ReadNext()
{
var f = *(float*)_address;
var srgb = Math.Pow(f, 1 / 2.2);
var value = (byte)(srgb * 255);
_address += 4;
return new Rgba8888Pixel
{
A = 255,
B = value,
G = value,
R = value
};
}
public void Reset(IntPtr address) => _address = (byte*)address;
}
struct Rgba64
{
#pragma warning disable CS0649
public ushort R;
public ushort G;
public ushort B;
public ushort A;
#pragma warning restore CS0649
}
public unsafe struct Rgba64PixelFormatReader : IPixelFormatReader
{
private Rgba64* _address;
public Rgba8888Pixel ReadNext()
{
var value = *_address;
_address++;
return new Rgba8888Pixel
{
A = (byte)(value.A >> 8),
B = (byte)(value.B >> 8),
G = (byte)(value.G >> 8),
R = (byte)(value.R >> 8),
};
}
public void Reset(IntPtr address) => _address = (Rgba64*)address;
}
public static void Transcode(IntPtr dst, IntPtr src, PixelSize size, int strideSrc, int strideDst,
PixelFormat format)
{
if (format == PixelFormats.BlackWhite)
Transcode<BlackWhitePixelReader>(dst, src, size, strideSrc, strideDst);
else if (format == PixelFormats.Gray2)
Transcode<Gray2PixelReader>(dst, src, size, strideSrc, strideDst);
else if (format == PixelFormats.Gray4)
Transcode<Gray4PixelReader>(dst, src, size, strideSrc, strideDst);
else if (format == PixelFormats.Gray8)
Transcode<Gray8PixelReader>(dst, src, size, strideSrc, strideDst);
else if (format == PixelFormats.Gray16)
Transcode<Gray16PixelReader>(dst, src, size, strideSrc, strideDst);
else if (format == PixelFormats.Gray32Float)
Transcode<Gray32FloatPixelReader>(dst, src, size, strideSrc, strideDst);
else if (format == PixelFormats.Rgba64)
Transcode<Rgba64PixelFormatReader>(dst, src, size, strideSrc, strideDst);
else
throw new NotSupportedException($"Pixel format {format} is not supported");
}
public static bool SupportsFormat(PixelFormat format)
{
return format == PixelFormats.BlackWhite
|| format == PixelFormats.Gray2
|| format == PixelFormats.Gray4
|| format == PixelFormats.Gray8
|| format == PixelFormats.Gray16
|| format == PixelFormats.Gray32Float
|| format == PixelFormats.Rgba64;
}
public static void Transcode<TReader>(IntPtr dst, IntPtr src, PixelSize size, int strideSrc, int strideDst) where TReader : struct, IPixelFormatReader
{
var w = size.Width;
var h = size.Height;
TReader reader = default;
for (var y = 0; y < h; y++)
{
reader.Reset(src + strideSrc * y);
var dstRow = (Rgba8888Pixel*)(dst + strideDst * y);
for (var x = 0; x < w; x++)
{
*dstRow = reader.ReadNext();
dstRow++;
}
}
}
}

77
src/Avalonia.Base/Media/Imaging/WriteableBitmap.cs

@ -1,5 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Runtime.CompilerServices;
using Avalonia.Platform; using Avalonia.Platform;
namespace Avalonia.Media.Imaging namespace Avalonia.Media.Imaging
@ -9,7 +10,9 @@ namespace Avalonia.Media.Imaging
/// </summary> /// </summary>
public class WriteableBitmap : Bitmap public class WriteableBitmap : Bitmap
{ {
// Holds a buffer with pixel format that requires transcoding
private BitmapMemory? _pixelFormatMemory = null;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="WriteableBitmap"/> class. /// Initializes a new instance of the <see cref="WriteableBitmap"/> class.
/// </summary> /// </summary>
@ -19,16 +22,67 @@ namespace Avalonia.Media.Imaging
/// <param name="alphaFormat">The alpha format (optional).</param> /// <param name="alphaFormat">The alpha format (optional).</param>
/// <returns>An <see cref="IWriteableBitmapImpl"/>.</returns> /// <returns>An <see cref="IWriteableBitmapImpl"/>.</returns>
public WriteableBitmap(PixelSize size, Vector dpi, PixelFormat? format = null, AlphaFormat? alphaFormat = null) public WriteableBitmap(PixelSize size, Vector dpi, PixelFormat? format = null, AlphaFormat? alphaFormat = null)
: base(CreatePlatformImpl(size, dpi, format, alphaFormat)) : this(CreatePlatformImpl(size, dpi, format, alphaFormat))
{ {
} }
private WriteableBitmap(IWriteableBitmapImpl impl) : base(impl) private WriteableBitmap((IBitmapImpl impl, BitmapMemory? mem) bitmapWithMem) : this(bitmapWithMem.impl,
bitmapWithMem.mem)
{ {
} }
private WriteableBitmap(IBitmapImpl impl, BitmapMemory? pixelFormatMemory = null) : base(impl)
{
_pixelFormatMemory = pixelFormatMemory;
}
/// <summary>
/// Initializes a new instance of the <see cref="WriteableBitmap"/> class with existing pixel data
/// The data is copied to the bitmap
/// </summary>
/// <param name="format">The pixel format.</param>
/// <param name="alphaFormat">The alpha format.</param>
/// <param name="data">The pointer to the source bytes.</param>
/// <param name="size">The size of the bitmap in device pixels.</param>
/// <param name="dpi">The DPI of the bitmap.</param>
/// <param name="stride">The number of bytes per row.</param>
public unsafe WriteableBitmap(PixelFormat format, AlphaFormat alphaFormat, IntPtr data, PixelSize size, Vector dpi, int stride)
: this(size, dpi, format, alphaFormat)
{
var minStride = (format.BitsPerPixel * size.Width + 7) / 8;
if (minStride > stride)
throw new ArgumentOutOfRangeException(nameof(stride));
public ILockedFramebuffer Lock() => ((IWriteableBitmapImpl) PlatformImpl.Item).Lock(); using (var locked = Lock())
{
for (var y = 0; y < size.Height; y++)
Unsafe.CopyBlock((locked.Address + locked.RowBytes * y).ToPointer(),
(data + y * stride).ToPointer(), (uint)minStride);
}
}
public override PixelFormat? Format => _pixelFormatMemory?.Format ?? base.Format;
public ILockedFramebuffer Lock()
{
if (_pixelFormatMemory == null)
return ((IWriteableBitmapImpl)PlatformImpl.Item).Lock();
return new LockedFramebuffer(_pixelFormatMemory.Address, _pixelFormatMemory.Size,
_pixelFormatMemory.RowBytes,
Dpi, _pixelFormatMemory.Format, () =>
{
using var inner = ((IWriteableBitmapImpl)PlatformImpl.Item).Lock();
_pixelFormatMemory.CopyToRgba(inner.Address, inner.RowBytes);
});
}
public override void CopyPixels(PixelRect sourceRect, IntPtr buffer, int bufferSize, int stride)
{
using (var fb = Lock())
CopyPixelsCore(sourceRect, buffer, bufferSize, stride, fb);
}
public static WriteableBitmap Decode(Stream stream) public static WriteableBitmap Decode(Stream stream)
{ {
@ -67,14 +121,25 @@ namespace Avalonia.Media.Imaging
return new WriteableBitmap(ri.LoadWriteableBitmapToHeight(stream, height, interpolationMode)); return new WriteableBitmap(ri.LoadWriteableBitmapToHeight(stream, height, interpolationMode));
} }
private static IBitmapImpl CreatePlatformImpl(PixelSize size, in Vector dpi, PixelFormat? format, AlphaFormat? alphaFormat) private static (IBitmapImpl, BitmapMemory?) CreatePlatformImpl(PixelSize size, in Vector dpi, PixelFormat? format, AlphaFormat? alphaFormat)
{ {
if (size.Width <= 0 || size.Height <= 0)
throw new ArgumentException("Size should be >= (1,1)", nameof(size));
var ri = GetFactory(); var ri = GetFactory();
PixelFormat finalFormat = format ?? ri.DefaultPixelFormat; PixelFormat finalFormat = format ?? ri.DefaultPixelFormat;
AlphaFormat finalAlphaFormat = alphaFormat ?? ri.DefaultAlphaFormat; AlphaFormat finalAlphaFormat = alphaFormat ?? ri.DefaultAlphaFormat;
return ri.CreateWriteableBitmap(size, dpi, finalFormat, finalAlphaFormat); if (ri.IsSupportedBitmapPixelFormat(finalFormat))
return (ri.CreateWriteableBitmap(size, dpi, finalFormat, finalAlphaFormat), null);
if (!PixelFormatReader.SupportsFormat(finalFormat))
throw new NotSupportedException($"Pixel format {finalFormat} is not supported");
var impl = ri.CreateWriteableBitmap(size, dpi, PixelFormat.Rgba8888,
finalFormat.HasAlpha ? finalAlphaFormat : AlphaFormat.Opaque);
return (impl, new BitmapMemory(finalFormat, size));
} }
private static IPlatformRenderInterface GetFactory() private static IPlatformRenderInterface GetFactory()

34
src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs

@ -0,0 +1,34 @@
using System;
namespace Avalonia.Metadata;
/// <summary>
/// Instructs the compiler to resolve the compiled bindings data type for the item-specific properties of collection-like controls.
/// </summary>
/// <remarks>
/// A typical usage example is a ListBox control, where <see cref="InheritDataTypeFromItemsAttribute"/> is defined on the ItemTemplate property,
/// allowing the template to inherit the data type from the Items collection binding.
/// </remarks>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class InheritDataTypeFromItemsAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="InheritDataTypeFromItemsAttribute"/> class.
/// </summary>
/// <param name="ancestorItemsProperty">The name of the property whose item type should be used on the target property.</param>
public InheritDataTypeFromItemsAttribute(string ancestorItemsProperty)
{
AncestorItemsProperty = ancestorItemsProperty;
}
/// <summary>
/// The name of the property whose item type should be used on the target property.
/// </summary>
public string AncestorItemsProperty { get; }
/// <summary>
/// The ancestor type to be used in a lookup for the <see cref="AncestorProperty"/>.
/// If null, the declaring type of the target property is used.
/// </summary>
public Type? AncestorType { get; set; }
}

2
src/Avalonia.Base/PixelRect.cs

@ -351,7 +351,7 @@ namespace Avalonia
/// <returns>The new <see cref="PixelRect"/>.</returns> /// <returns>The new <see cref="PixelRect"/>.</returns>
public PixelRect WithHeight(int height) public PixelRect WithHeight(int height)
{ {
return new PixelRect(X, Y, Width, Height); return new PixelRect(X, Y, Width, height);
} }
/// <summary> /// <summary>

1
src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs

@ -23,5 +23,4 @@ public static class OptionalFeatureProviderExtensions
rv = provider.TryGetFeature<T>(); rv = provider.TryGetFeature<T>();
return rv != null; return rv != null;
} }
} }

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

@ -197,6 +197,8 @@ namespace Avalonia.Platform
/// Default <see cref="PixelFormat"/> used on this platform. /// Default <see cref="PixelFormat"/> used on this platform.
/// </summary> /// </summary>
public PixelFormat DefaultPixelFormat { get; } public PixelFormat DefaultPixelFormat { get; }
bool IsSupportedBitmapPixelFormat(PixelFormat format);
} }
[Unstable] [Unstable]

7
src/Avalonia.Base/Platform/IReadableBitmapImpl.cs

@ -0,0 +1,7 @@
namespace Avalonia.Platform;
public interface IReadableBitmapImpl
{
PixelFormat? Format { get; }
ILockedFramebuffer Lock();
}

3
src/Avalonia.Base/Platform/IWriteableBitmapImpl.cs

@ -6,8 +6,7 @@ namespace Avalonia.Platform
/// Defines the platform-specific interface for a <see cref="Avalonia.Media.Imaging.WriteableBitmap"/>. /// Defines the platform-specific interface for a <see cref="Avalonia.Media.Imaging.WriteableBitmap"/>.
/// </summary> /// </summary>
[Unstable] [Unstable]
public interface IWriteableBitmapImpl : IBitmapImpl public interface IWriteableBitmapImpl : IBitmapImpl, IReadableBitmapImpl
{ {
ILockedFramebuffer Lock();
} }
} }

71
src/Avalonia.Base/Platform/PixelFormat.cs

@ -1,9 +1,74 @@
namespace Avalonia.Platform using System;
namespace Avalonia.Platform
{ {
public enum PixelFormat internal enum PixelFormatEnum
{ {
Rgb565, Rgb565,
Rgba8888, Rgba8888,
Bgra8888 Bgra8888,
BlackWhite,
Gray2,
Gray4,
Gray8,
Gray16,
Gray32Float,
Rgba64
}
public record struct PixelFormat
{
internal PixelFormatEnum FormatEnum;
public int BitsPerPixel
{
get
{
if (FormatEnum == PixelFormatEnum.BlackWhite)
return 1;
else if (FormatEnum == PixelFormatEnum.Gray2)
return 2;
else if (FormatEnum == PixelFormatEnum.Gray4)
return 4;
else if (FormatEnum == PixelFormatEnum.Gray8)
return 8;
else if (FormatEnum == PixelFormatEnum.Rgb565
|| FormatEnum == PixelFormatEnum.Gray16)
return 16;
else if (FormatEnum == PixelFormatEnum.Rgba64)
return 64;
return 32;
}
}
internal bool HasAlpha => FormatEnum == PixelFormatEnum.Rgba8888
|| FormatEnum == PixelFormatEnum.Bgra8888
|| FormatEnum == PixelFormatEnum.Rgba64;
internal PixelFormat(PixelFormatEnum format)
{
FormatEnum = format;
}
public static PixelFormat Rgb565 => PixelFormats.Rgb565;
public static PixelFormat Rgba8888 => PixelFormats.Rgba8888;
public static PixelFormat Bgra8888 => PixelFormats.Bgra8888;
public override string ToString() => FormatEnum.ToString();
}
public static class PixelFormats
{
public static PixelFormat Rgb565 { get; } = new PixelFormat(PixelFormatEnum.Rgb565);
public static PixelFormat Rgba8888 { get; } = new PixelFormat(PixelFormatEnum.Rgba8888);
public static PixelFormat Rgba64 { get; } = new PixelFormat(PixelFormatEnum.Rgba64);
public static PixelFormat Bgra8888 { get; } = new PixelFormat(PixelFormatEnum.Bgra8888);
public static PixelFormat BlackWhite { get; } = new PixelFormat(PixelFormatEnum.BlackWhite);
public static PixelFormat Gray2 { get; } = new PixelFormat(PixelFormatEnum.Gray2);
public static PixelFormat Gray4 { get; } = new PixelFormat(PixelFormatEnum.Gray4);
public static PixelFormat Gray8 { get; } = new PixelFormat(PixelFormatEnum.Gray8);
public static PixelFormat Gray16 { get; } = new PixelFormat(PixelFormatEnum.Gray16);
public static PixelFormat Gray32Float { get; } = new PixelFormat(PixelFormatEnum.Gray32Float);
} }
} }

71
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFile.cs

@ -1,50 +1,65 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Security; using System.Security;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage.FileIO; namespace Avalonia.Platform.Storage.FileIO;
[Unstable] internal class BclStorageFile : IStorageBookmarkFile
public class BclStorageFile : IStorageBookmarkFile
{ {
private readonly FileInfo _fileInfo;
public BclStorageFile(string fileName) public BclStorageFile(string fileName)
{ {
_fileInfo = new FileInfo(fileName); FileInfo = new FileInfo(fileName);
} }
public BclStorageFile(FileInfo fileInfo) public BclStorageFile(FileInfo fileInfo)
{ {
_fileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo)); FileInfo = fileInfo ?? throw new ArgumentNullException(nameof(fileInfo));
} }
public FileInfo FileInfo { get; }
public bool CanOpenRead => true; public bool CanOpenRead => true;
public bool CanOpenWrite => true; public bool CanOpenWrite => true;
public string Name => _fileInfo.Name; public string Name => FileInfo.Name;
public virtual bool CanBookmark => true; public virtual bool CanBookmark => true;
public Uri Path
{
get
{
try
{
if (FileInfo.Directory is not null)
{
return StorageProviderHelpers.FilePathToUri(FileInfo.FullName);
}
}
catch (SecurityException)
{
}
return new Uri(FileInfo.Name, UriKind.Relative);
}
}
public Task<StorageItemProperties> GetBasicPropertiesAsync() public Task<StorageItemProperties> GetBasicPropertiesAsync()
{ {
if (_fileInfo.Exists) if (FileInfo.Exists)
{ {
return Task.FromResult(new StorageItemProperties( return Task.FromResult(new StorageItemProperties(
(ulong)_fileInfo.Length, (ulong)FileInfo.Length,
_fileInfo.CreationTimeUtc, FileInfo.CreationTimeUtc,
_fileInfo.LastAccessTimeUtc)); FileInfo.LastAccessTimeUtc));
} }
return Task.FromResult(new StorageItemProperties()); return Task.FromResult(new StorageItemProperties());
} }
public Task<IStorageFolder?> GetParentAsync() public Task<IStorageFolder?> GetParentAsync()
{ {
if (_fileInfo.Directory is { } directory) if (FileInfo.Directory is { } directory)
{ {
return Task.FromResult<IStorageFolder?>(new BclStorageFolder(directory)); return Task.FromResult<IStorageFolder?>(new BclStorageFolder(directory));
} }
@ -53,17 +68,17 @@ public class BclStorageFile : IStorageBookmarkFile
public Task<Stream> OpenReadAsync() public Task<Stream> OpenReadAsync()
{ {
return Task.FromResult<Stream>(_fileInfo.OpenRead()); return Task.FromResult<Stream>(FileInfo.OpenRead());
} }
public Task<Stream> OpenWriteAsync() public Task<Stream> OpenWriteAsync()
{ {
return Task.FromResult<Stream>(_fileInfo.OpenWrite()); return Task.FromResult<Stream>(FileInfo.OpenWrite());
} }
public virtual Task<string?> SaveBookmarkAsync() public virtual Task<string?> SaveBookmarkAsync()
{ {
return Task.FromResult<string?>(_fileInfo.FullName); return Task.FromResult<string?>(FileInfo.FullName);
} }
public Task ReleaseBookmarkAsync() public Task ReleaseBookmarkAsync()
@ -72,28 +87,6 @@ public class BclStorageFile : IStorageBookmarkFile
return Task.CompletedTask; return Task.CompletedTask;
} }
public bool TryGetUri([NotNullWhen(true)] out Uri? uri)
{
try
{
if (_fileInfo.Directory is not null)
{
uri = Path.IsPathRooted(_fileInfo.FullName) ?
new Uri(new Uri("file://"), _fileInfo.FullName) :
new Uri(_fileInfo.FullName, UriKind.Relative);
return true;
}
uri = null;
return false;
}
catch (SecurityException)
{
uri = null;
return false;
}
}
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
} }

63
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageFolder.cs

@ -1,23 +1,18 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Security; using System.Security;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Metadata;
namespace Avalonia.Platform.Storage.FileIO; namespace Avalonia.Platform.Storage.FileIO;
[Unstable] internal class BclStorageFolder : IStorageBookmarkFolder
public class BclStorageFolder : IStorageBookmarkFolder
{ {
private readonly DirectoryInfo _directoryInfo;
public BclStorageFolder(string path) public BclStorageFolder(string path)
{ {
_directoryInfo = new DirectoryInfo(path); DirectoryInfo = new DirectoryInfo(path);
if (!_directoryInfo.Exists) if (!DirectoryInfo.Exists)
{ {
throw new ArgumentException("Directory must exist"); throw new ArgumentException("Directory must exist");
} }
@ -25,29 +20,46 @@ public class BclStorageFolder : IStorageBookmarkFolder
public BclStorageFolder(DirectoryInfo directoryInfo) public BclStorageFolder(DirectoryInfo directoryInfo)
{ {
_directoryInfo = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo)); DirectoryInfo = directoryInfo ?? throw new ArgumentNullException(nameof(directoryInfo));
if (!_directoryInfo.Exists) if (!DirectoryInfo.Exists)
{ {
throw new ArgumentException("Directory must exist", nameof(directoryInfo)); throw new ArgumentException("Directory must exist", nameof(directoryInfo));
} }
} }
public string Name => _directoryInfo.Name; public string Name => DirectoryInfo.Name;
public DirectoryInfo DirectoryInfo { get; }
public bool CanBookmark => true; public bool CanBookmark => true;
public Uri Path
{
get
{
try
{
return StorageProviderHelpers.FilePathToUri(DirectoryInfo.FullName);
}
catch (SecurityException)
{
return new Uri(DirectoryInfo.Name, UriKind.Relative);
}
}
}
public Task<StorageItemProperties> GetBasicPropertiesAsync() public Task<StorageItemProperties> GetBasicPropertiesAsync()
{ {
var props = new StorageItemProperties( var props = new StorageItemProperties(
null, null,
_directoryInfo.CreationTimeUtc, DirectoryInfo.CreationTimeUtc,
_directoryInfo.LastAccessTimeUtc); DirectoryInfo.LastAccessTimeUtc);
return Task.FromResult(props); return Task.FromResult(props);
} }
public Task<IStorageFolder?> GetParentAsync() public Task<IStorageFolder?> GetParentAsync()
{ {
if (_directoryInfo.Parent is { } directory) if (DirectoryInfo.Parent is { } directory)
{ {
return Task.FromResult<IStorageFolder?>(new BclStorageFolder(directory)); return Task.FromResult<IStorageFolder?>(new BclStorageFolder(directory));
} }
@ -56,9 +68,9 @@ public class BclStorageFolder : IStorageBookmarkFolder
public Task<IReadOnlyList<IStorageItem>> GetItemsAsync() public Task<IReadOnlyList<IStorageItem>> GetItemsAsync()
{ {
var items = _directoryInfo.GetDirectories() var items = DirectoryInfo.GetDirectories()
.Select(d => (IStorageItem)new BclStorageFolder(d)) .Select(d => (IStorageItem)new BclStorageFolder(d))
.Concat(_directoryInfo.GetFiles().Select(f => new BclStorageFile(f))) .Concat(DirectoryInfo.GetFiles().Select(f => new BclStorageFile(f)))
.ToArray(); .ToArray();
return Task.FromResult<IReadOnlyList<IStorageItem>>(items); return Task.FromResult<IReadOnlyList<IStorageItem>>(items);
@ -66,7 +78,7 @@ public class BclStorageFolder : IStorageBookmarkFolder
public virtual Task<string?> SaveBookmarkAsync() public virtual Task<string?> SaveBookmarkAsync()
{ {
return Task.FromResult<string?>(_directoryInfo.FullName); return Task.FromResult<string?>(DirectoryInfo.FullName);
} }
public Task ReleaseBookmarkAsync() public Task ReleaseBookmarkAsync()
@ -75,23 +87,6 @@ public class BclStorageFolder : IStorageBookmarkFolder
return Task.CompletedTask; return Task.CompletedTask;
} }
public bool TryGetUri([NotNullWhen(true)] out Uri? uri)
{
try
{
uri = Path.IsPathRooted(_directoryInfo.FullName) ?
new Uri(new Uri("file://"), _directoryInfo.FullName) :
new Uri(_directoryInfo.FullName, UriKind.Relative);
return true;
}
catch (SecurityException)
{
uri = null;
return false;
}
}
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
{ {
} }

103
src/Avalonia.Base/Platform/Storage/FileIO/BclStorageProvider.cs

@ -1,12 +1,13 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Metadata; using Avalonia.Compatibility;
namespace Avalonia.Platform.Storage.FileIO; namespace Avalonia.Platform.Storage.FileIO;
[Unstable] internal abstract class BclStorageProvider : IStorageProvider
public abstract class BclStorageProvider : IStorageProvider
{ {
public abstract bool CanOpen { get; } public abstract bool CanOpen { get; }
public abstract Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options); public abstract Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options);
@ -32,4 +33,98 @@ public abstract class BclStorageProvider : IStorageProvider
? Task.FromResult<IStorageBookmarkFolder?>(new BclStorageFolder(folder)) ? Task.FromResult<IStorageBookmarkFolder?>(new BclStorageFolder(folder))
: Task.FromResult<IStorageBookmarkFolder?>(null); : Task.FromResult<IStorageBookmarkFolder?>(null);
} }
public virtual Task<IStorageFile?> TryGetFileFromPath(Uri filePath)
{
if (filePath.IsAbsoluteUri)
{
var file = new FileInfo(filePath.LocalPath);
if (file.Exists)
{
return Task.FromResult<IStorageFile?>(new BclStorageFile(file));
}
}
return Task.FromResult<IStorageFile?>(null);
}
public virtual Task<IStorageFolder?> TryGetFolderFromPath(Uri folderPath)
{
if (folderPath.IsAbsoluteUri)
{
var directory = new DirectoryInfo(folderPath.LocalPath);
if (directory.Exists)
{
return Task.FromResult<IStorageFolder?>(new BclStorageFolder(directory));
}
}
return Task.FromResult<IStorageFolder?>(null);
}
public virtual Task<IStorageFolder?> TryGetWellKnownFolder(WellKnownFolder wellKnownFolder)
{
// Note, this BCL API returns different values depending on the .NET version.
// We should also document it.
// https://github.com/dotnet/docs/issues/31423
// For pre-breaking change values see table:
// https://johnkoerner.com/csharp/special-folder-values-on-windows-versus-mac/
var folderPath = wellKnownFolder switch
{
WellKnownFolder.Desktop => GetFromSpecialFolder(Environment.SpecialFolder.Desktop),
WellKnownFolder.Documents => GetFromSpecialFolder(Environment.SpecialFolder.MyDocuments),
WellKnownFolder.Downloads => GetDownloadsWellKnownFolder(),
WellKnownFolder.Music => GetFromSpecialFolder(Environment.SpecialFolder.MyMusic),
WellKnownFolder.Pictures => GetFromSpecialFolder(Environment.SpecialFolder.MyPictures),
WellKnownFolder.Videos => GetFromSpecialFolder(Environment.SpecialFolder.MyVideos),
_ => throw new ArgumentOutOfRangeException(nameof(wellKnownFolder), wellKnownFolder, null)
};
if (folderPath is null)
{
return Task.FromResult<IStorageFolder?>(null);
}
var directory = new DirectoryInfo(folderPath);
if (!directory.Exists)
{
return Task.FromResult<IStorageFolder?>(null);
}
return Task.FromResult<IStorageFolder?>(new BclStorageFolder(directory));
string GetFromSpecialFolder(Environment.SpecialFolder folder) =>
Environment.GetFolderPath(folder, Environment.SpecialFolderOption.Create);
}
// TODO, replace with https://github.com/dotnet/runtime/issues/70484 when implemented.
// Normally we want to avoid platform specific code in the Avalonia.Base assembly.
protected static string? GetDownloadsWellKnownFolder()
{
if (OperatingSystemEx.IsWindows())
{
return Environment.OSVersion.Version.Major < 6 ? null :
SHGetKnownFolderPath(s_folderDownloads, 0, IntPtr.Zero);
}
if (OperatingSystemEx.IsLinux())
{
var envDir = Environment.GetEnvironmentVariable("XDG_DOWNLOAD_DIR");
if (envDir != null && Directory.Exists(envDir))
{
return envDir;
}
}
if (OperatingSystemEx.IsLinux() || OperatingSystemEx.IsMacOS())
{
return "~/Downloads";
}
return null;
}
private static readonly Guid s_folderDownloads = new Guid("374DE290-123F-4565-9164-39C4925E467B");
[DllImport("shell32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)]
private static extern string SHGetKnownFolderPath([MarshalAs(UnmanagedType.LPStruct)] Guid id, int flags, IntPtr token);
} }

16
src/Avalonia.Base/Platform/Storage/FileIO/StorageProviderHelpers.cs

@ -1,13 +1,23 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Avalonia.Metadata; using System.Text;
namespace Avalonia.Platform.Storage.FileIO; namespace Avalonia.Platform.Storage.FileIO;
[Unstable] internal static class StorageProviderHelpers
public static class StorageProviderHelpers
{ {
public static Uri FilePathToUri(string path)
{
var uriPath = new StringBuilder(path)
.Replace("%", $"%{(int)'%':X2}")
.Replace("[", $"%{(int)'[':X2}")
.Replace("]", $"%{(int)']':X2}")
.ToString();
return new UriBuilder("file", string.Empty) { Path = uriPath }.Uri;
}
public static string NameWithExtension(string path, string? defaultExtension, FilePickerFileType? filter) public static string NameWithExtension(string path, string? defaultExtension, FilePickerFileType? filter)
{ {
var name = Path.GetFileName(path); var name = Path.GetFileName(path);

4
src/Avalonia.Base/Platform/Storage/IStorageItem.cs

@ -20,13 +20,13 @@ public interface IStorageItem : IDisposable
string Name { get; } string Name { get; }
/// <summary> /// <summary>
/// Gets the full file-system path of the item, if the item has a path. /// Gets the file-system path of the item.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Android backend might return file path with "content:" scheme. /// Android backend might return file path with "content:" scheme.
/// Browser and iOS backends might return relative uris. /// Browser and iOS backends might return relative uris.
/// </remarks> /// </remarks>
bool TryGetUri([NotNullWhen(true)] out Uri? uri); Uri Path { get; }
/// <summary> /// <summary>
/// Gets the basic properties of the current item. /// Gets the basic properties of the current item.

35
src/Avalonia.Base/Platform/Storage/IStorageProvider.cs

@ -1,4 +1,6 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Runtime.Versioning;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Metadata; using Avalonia.Metadata;
@ -53,4 +55,35 @@ public interface IStorageProvider
/// <param name="bookmark">Bookmark ID.</param> /// <param name="bookmark">Bookmark ID.</param>
/// <returns>Bookmarked folder or null if OS denied request.</returns> /// <returns>Bookmarked folder or null if OS denied request.</returns>
Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark); Task<IStorageBookmarkFolder?> OpenFolderBookmarkAsync(string bookmark);
/// <summary>
/// Attempts to read file from the file-system by its path.
/// </summary>
/// <param name="filePath">The path of the item to retrieve in Uri format.</param>
/// <remarks>
/// Uri path is usually expected to be an absolute path with "file" scheme.
/// But it can be an uri with "content" scheme on the Android.
/// It also might ask user for the permission, and throw an exception if it was denied.
/// </remarks>
/// <returns>File or null if it doesn't exist.</returns>
Task<IStorageFile?> TryGetFileFromPath(Uri filePath);
/// <summary>
/// Attempts to read folder from the file-system by its path.
/// </summary>
/// <param name="folderPath">The path of the folder to retrieve in Uri format.</param>
/// <remarks>
/// Uri path is usually expected to be an absolute path with "file" scheme.
/// But it can be an uri with "content" scheme on the Android.
/// It also might ask user for the permission, and throw an exception if it was denied.
/// </remarks>
/// <returns>Folder or null if it doesn't exist.</returns>
Task<IStorageFolder?> TryGetFolderFromPath(Uri folderPath);
/// <summary>
/// Attempts to read folder from the file-system by its path
/// </summary>
/// <param name="wellKnownFolder">Well known folder identifier.</param>
/// <returns>Folder or null if it doesn't exist.</returns>
Task<IStorageFolder?> TryGetWellKnownFolder(WellKnownFolder wellKnownFolder);
} }

6
src/Avalonia.Base/Platform/Storage/NameCollisionOption.cs

@ -0,0 +1,6 @@
namespace Avalonia.Platform.Storage;
public class NameCollisionOption
{
}

55
src/Avalonia.Base/Platform/Storage/StorageProviderExtensions.cs

@ -0,0 +1,55 @@
using System.Threading.Tasks;
using Avalonia.Platform.Storage.FileIO;
namespace Avalonia.Platform.Storage;
/// <summary>
/// Group of public extensions for <see cref="IStorageProvider"/> class.
/// </summary>
public static class StorageProviderExtensions
{
/// <inheritdoc cref="IStorageProvider.TryGetFileFromPath"/>
public static Task<IStorageFile?> TryGetFileFromPath(this IStorageProvider provider, string filePath)
{
return provider.TryGetFileFromPath(StorageProviderHelpers.FilePathToUri(filePath));
}
/// <inheritdoc cref="IStorageProvider.TryGetFolderFromPath"/>
public static Task<IStorageFolder?> TryGetFolderFromPath(this IStorageProvider provider, string folderPath)
{
return provider.TryGetFolderFromPath(StorageProviderHelpers.FilePathToUri(folderPath));
}
internal static string? TryGetFullPath(this IStorageFolder folder)
{
// We can avoid double escaping of the path by checking for BclStorageFolder.
// Ideally, `folder.Path.LocalPath` should also work, as that's only available way for the users.
if (folder is BclStorageFolder storageFolder)
{
return storageFolder.DirectoryInfo.FullName;
}
if (folder.Path is { IsAbsoluteUri: true, Scheme: "file" } absolutePath)
{
return absolutePath.LocalPath;
}
// android "content:", browser and ios relative links go here.
return null;
}
internal static string? TryGetFullPath(this IStorageFile file)
{
if (file is BclStorageFile storageFolder)
{
return storageFolder.FileInfo.FullName;
}
if (file.Path is { IsAbsoluteUri: true, Scheme: "file" } absolutePath)
{
return absolutePath.LocalPath;
}
return null;
}
}

37
src/Avalonia.Base/Platform/Storage/WellKnownFolder.cs

@ -0,0 +1,37 @@
namespace Avalonia.Platform.Storage;
/// <summary>
/// Specifies enumerated constants used to retrieve directory paths to system well known folders.
/// </summary>
public enum WellKnownFolder
{
/// <summary>
/// Current user desktop folder.
/// </summary>
Desktop,
/// <summary>
/// Current user documents folder.
/// </summary>
Documents,
/// <summary>
/// Current user downloads folder.
/// </summary>
Downloads,
/// <summary>
/// Current user music folder
/// </summary>
Music,
/// <summary>
/// Current user pictures folder
/// </summary>
Pictures,
/// <summary>
/// Current user videos folder
/// </summary>
Videos,
}

8
src/Avalonia.Base/Platform/SystemNavigationManager.cs → src/Avalonia.Base/Platform/SystemNavigationManagerImpl.cs

@ -5,13 +5,7 @@ using Avalonia.Metadata;
namespace Avalonia.Platform namespace Avalonia.Platform
{ {
[Unstable] [Unstable]
public interface ITopLevelWithSystemNavigationManager public interface ISystemNavigationManagerImpl
{
ISystemNavigationManager SystemNavigationManager { get; }
}
[Unstable]
public interface ISystemNavigationManager
{ {
public event EventHandler<RoutedEventArgs>? BackRequested; public event EventHandler<RoutedEventArgs>? BackRequested;
} }

6
src/Avalonia.Base/Point.cs

@ -251,6 +251,12 @@ namespace Avalonia
/// <param name="transform">The transform.</param> /// <param name="transform">The transform.</param>
/// <returns>The transformed point.</returns> /// <returns>The transformed point.</returns>
public Point Transform(Matrix transform) => transform.Transform(this); public Point Transform(Matrix transform) => transform.Transform(this);
internal Point Transform(Matrix4x4 matrix)
{
var vec = Vector2.Transform(new Vector2((float)X, (float)Y), matrix);
return new Point(vec.X, vec.Y);
}
/// <summary> /// <summary>
/// Returns a new point with the specified X coordinate. /// Returns a new point with the specified X coordinate.

27
src/Avalonia.Base/Rect.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Numerics;
using Avalonia.Animation.Animators; using Avalonia.Animation.Animators;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -441,6 +442,32 @@ namespace Avalonia
return new Rect(new Point(left, top), new Point(right, bottom)); return new Rect(new Point(left, top), new Point(right, bottom));
} }
internal Rect TransformToAABB(Matrix4x4 matrix)
{
ReadOnlySpan<Point> points = stackalloc Point[4]
{
TopLeft.Transform(matrix),
TopRight.Transform(matrix),
BottomRight.Transform(matrix),
BottomLeft.Transform(matrix)
};
var left = double.MaxValue;
var right = double.MinValue;
var top = double.MaxValue;
var bottom = double.MinValue;
foreach (var p in points)
{
if (p.X < left) left = p.X;
if (p.X > right) right = p.X;
if (p.Y < top) top = p.Y;
if (p.Y > bottom) bottom = p.Y;
}
return new Rect(new Point(left, top), new Point(right, bottom));
}
/// <summary> /// <summary>
/// Translates the rectangle by an offset. /// Translates the rectangle by an offset.

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

@ -83,8 +83,16 @@ public class CompositingRenderer : IRendererWithCompositor
} }
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<Visual> HitTest(Point p, Visual root, Func<Visual, bool>? filter) public IEnumerable<Visual> HitTest(Point p, Visual? root, Func<Visual, bool>? filter)
{ {
CompositionVisual? rootVisual = null;
if (root != null)
{
if (root.CompositionVisual == null)
yield break;
rootVisual = root.CompositionVisual;
}
Func<CompositionVisual, bool>? f = null; Func<CompositionVisual, bool>? f = null;
if (filter != null) if (filter != null)
f = v => f = v =>
@ -93,8 +101,8 @@ public class CompositingRenderer : IRendererWithCompositor
return filter(dlv.Visual); return filter(dlv.Visual);
return true; return true;
}; };
var res = CompositionTarget.TryHitTest(p, f); var res = CompositionTarget.TryHitTest(p, rootVisual, f);
if(res == null) if(res == null)
yield break; yield break;
foreach(var v in res) foreach(var v in res)
@ -257,6 +265,7 @@ public class CompositingRenderer : IRendererWithCompositor
_recalculateChildren.Clear(); _recalculateChildren.Clear();
CompositionTarget.Size = _root.ClientSize; CompositionTarget.Size = _root.ClientSize;
CompositionTarget.Scaling = _root.RenderScaling; CompositionTarget.Scaling = _root.RenderScaling;
TriggerSceneInvalidatedOnBatchCompletion(_compositor.RequestCommitAsync());
} }
private async void TriggerSceneInvalidatedOnBatchCompletion(Task batchCompletion) private async void TriggerSceneInvalidatedOnBatchCompletion(Task batchCompletion)

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

@ -61,9 +61,6 @@ internal class CompositionDrawListVisual : CompositionContainerVisual
return false; return false;
if (custom != null) if (custom != null)
{ {
// Simulate the old behavior
// TODO: Change behavior once legacy renderers are removed
pt += new Point(Offset.X, Offset.Y);
return custom.HitTest(pt); return custom.HitTest(pt);
} }

3
src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Server;
using Avalonia.Threading;
namespace Avalonia.Rendering.Composition; namespace Avalonia.Rendering.Composition;
@ -55,6 +56,6 @@ public class CompositionDrawingSurface : CompositionSurface
~CompositionDrawingSurface() ~CompositionDrawingSurface()
{ {
Dispose(); Dispatcher.UIThread.Post(Dispose);
} }
} }

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

@ -28,16 +28,15 @@ namespace Avalonia.Rendering.Composition
/// <summary> /// <summary>
/// Attempts to perform a hit-tst /// Attempts to perform a hit-tst
/// </summary> /// </summary>
/// <param name="point"></param>
/// <param name="filter"></param>
/// <returns></returns> /// <returns></returns>
public PooledList<CompositionVisual>? TryHitTest(Point point, Func<CompositionVisual, bool>? filter) public PooledList<CompositionVisual>? TryHitTest(Point point, CompositionVisual? root, Func<CompositionVisual, bool>? filter)
{ {
Server.Readback.NextRead(); Server.Readback.NextRead();
if (Root == null) root ??= Root;
if (root == null)
return null; return null;
var res = new PooledList<CompositionVisual>(); var res = new PooledList<CompositionVisual>();
HitTestCore(Root, point, res, filter); HitTestCore(root, point, res, filter);
return res; return res;
} }

3
src/Avalonia.Base/Rendering/Composition/Compositor.cs

@ -37,6 +37,8 @@ namespace Avalonia.Rendering.Composition
private List<Action> _pendingServerCompositorJobs = new(); private List<Action> _pendingServerCompositorJobs = new();
internal IEasing DefaultEasing { get; } internal IEasing DefaultEasing { get; }
internal event Action? AfterCommit;
/// <summary> /// <summary>
@ -88,6 +90,7 @@ namespace Avalonia.Rendering.Composition
{ {
if (_invokeBeforeCommitWrite.Count > 0) if (_invokeBeforeCommitWrite.Count > 0)
RequestCommitAsync(); RequestCommitAsync();
AfterCommit?.Invoke();
} }
} }

780
src/Avalonia.Base/Rendering/DeferredRenderer.cs

@ -1,780 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Logging;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Threading;
using Avalonia.Utilities;
using Avalonia.VisualTree;
namespace Avalonia.Rendering
{
/// <summary>
/// A renderer which renders the state of the visual tree to an intermediate scene graph
/// representation which is then rendered on a rendering thread.
/// </summary>
public class DeferredRenderer : RendererBase, IRenderer, IRenderLoopTask, IVisualBrushRenderer
{
private readonly IDispatcher? _dispatcher;
private readonly IRenderLoop? _renderLoop;
private readonly Func<IRenderTarget>? _renderTargetFactory;
private readonly PlatformRenderInterfaceContextManager? _renderInterface;
private readonly Visual _root;
private readonly ISceneBuilder _sceneBuilder;
private bool _running;
private bool _disposed;
private volatile IRef<Scene>? _scene;
private DirtyVisuals? _dirty;
private HashSet<Visual>? _recalculateChildren;
private IRef<IRenderTargetBitmapImpl>? _overlay;
private int _lastSceneId = -1;
private DisplayDirtyRects _dirtyRectsDisplay = new DisplayDirtyRects();
private IRef<IDrawOperation>? _currentDraw;
private readonly IDeferredRendererLock _lock;
private readonly object _sceneLock = new object();
private readonly object _startStopLock = new object();
private readonly object _renderLoopIsRenderingLock = new object();
private readonly Action _updateSceneIfNeededDelegate;
private List<Action>? _pendingRenderThreadJobs;
/// <summary>
/// Initializes a new instance of the <see cref="DeferredRenderer"/> class.
/// </summary>
/// <param name="root">The control to render.</param>
/// <param name="renderLoop">The render loop.</param>
/// <param name="renderTargetFactory">The target render factory.</param>
/// <param name="renderInterface">The Platform Render Context.</param>
/// <param name="sceneBuilder">The scene builder to use. Optional.</param>
/// <param name="dispatcher">The dispatcher to use. Optional.</param>
/// <param name="rendererLock">Lock object used before trying to access render target</param>
public DeferredRenderer(
IRenderRoot root,
IRenderLoop renderLoop,
Func<IRenderTarget> renderTargetFactory,
PlatformRenderInterfaceContextManager? renderInterface = null,
ISceneBuilder? sceneBuilder = null,
IDispatcher? dispatcher = null,
IDeferredRendererLock? rendererLock = null) : base(true)
{
_dispatcher = dispatcher ?? Dispatcher.UIThread;
_root = root as Visual ?? throw new ArgumentNullException(nameof(root));
_sceneBuilder = sceneBuilder ?? new SceneBuilder();
Layers = new RenderLayers();
_renderLoop = renderLoop;
_renderTargetFactory = renderTargetFactory;
_renderInterface = renderInterface;
_lock = rendererLock ?? new ManagedDeferredRendererLock();
_updateSceneIfNeededDelegate = UpdateSceneIfNeeded;
}
/// <summary>
/// Initializes a new instance of the <see cref="DeferredRenderer"/> class.
/// </summary>
/// <param name="root">The control to render.</param>
/// <param name="renderTarget">The render target.</param>
/// <param name="sceneBuilder">The scene builder to use. Optional.</param>
/// <remarks>
/// This constructor is intended to be used for unit testing.
/// </remarks>
public DeferredRenderer(
Visual root,
IRenderTarget renderTarget,
ISceneBuilder? sceneBuilder = null) : base(true)
{
_root = root ?? throw new ArgumentNullException(nameof(root));
RenderTarget = renderTarget ?? throw new ArgumentNullException(nameof(renderTarget));
_sceneBuilder = sceneBuilder ?? new SceneBuilder();
Layers = new RenderLayers();
_lock = new ManagedDeferredRendererLock();
_updateSceneIfNeededDelegate = UpdateSceneIfNeeded;
}
/// <inheritdoc/>
public bool DrawFps { get; set; }
/// <inheritdoc/>
public bool DrawDirtyRects { get; set; }
/// <summary>
/// Gets or sets a path to which rendered frame should be rendered for debugging.
/// </summary>
public string? DebugFramesPath { get; set; }
/// <summary>
/// Forces the renderer to only draw frames on the render thread. Makes Paint to wait until frame is rendered
/// </summary>
public bool RenderOnlyOnRenderThread { get; set; }
/// <inheritdoc/>
public event EventHandler<SceneInvalidatedEventArgs>? SceneInvalidated;
/// <summary>
/// Gets the render layers.
/// </summary>
internal RenderLayers Layers { get; }
/// <summary>
/// Gets the current render target.
/// </summary>
internal IRenderTarget? RenderTarget { get; private set; }
/// <inheritdoc/>
public void AddDirty(Visual visual)
{
_dirty?.Add(visual);
}
/// <summary>
/// Disposes of the renderer and detaches from the render loop.
/// </summary>
public void Dispose()
{
lock (_sceneLock)
{
if (_disposed)
return;
_disposed = true;
var scene = _scene;
_scene = null;
scene?.Dispose();
}
Stop();
// Wait for any in-progress rendering to complete
lock(_renderLoopIsRenderingLock){}
DisposeRenderTarget();
}
public void RecalculateChildren(Visual visual) => _recalculateChildren?.Add(visual);
void DisposeRenderTarget()
{
using (var l = _lock.TryLock())
{
if(l == null)
{
// We are still trying to render on the render thread, try again a bit later
DispatcherTimer.RunOnce(DisposeRenderTarget, TimeSpan.FromMilliseconds(50),
DispatcherPriority.Background);
return;
}
Layers.Clear();
RenderTarget?.Dispose();
RenderTarget = null;
}
}
/// <inheritdoc/>
public IEnumerable<Visual> HitTest(Point p, Visual root, Func<Visual, bool>? filter)
{
EnsureCanHitTest();
//It's safe to access _scene here without a lock since
//it's only changed from UI thread which we are currently on
return _scene?.Item.HitTest(p, root, filter) ?? Enumerable.Empty<Visual>();
}
/// <inheritdoc/>
public Visual? HitTestFirst(Point p, Visual root, Func<Visual, bool>? filter)
{
EnsureCanHitTest();
//It's safe to access _scene here without a lock since
//it's only changed from UI thread which we are currently on
return _scene?.Item.HitTestFirst(p, root, filter);
}
/// <inheritdoc/>
public void Paint(Rect rect)
{
if (RenderOnlyOnRenderThread)
{
// Renderer is stopped and doesn't tick on the render thread
// This indicates a bug somewhere in our code
// (currently happens when a window gets minimized with Show desktop on Windows 10)
if(!_running)
return;
while (true)
{
Scene? scene;
bool? updated;
lock (_sceneLock)
{
updated = UpdateScene();
scene = _scene?.Item;
}
// Renderer is in invalid state, skip drawing
if(updated == null)
return;
// Wait for the scene to be rendered or disposed
scene?.Rendered.Wait();
// That was an up-to-date scene, we can return immediately
if (updated == true)
return;
}
}
else
{
var t = (IRenderLoopTask)this;
if (t.NeedsUpdate)
UpdateScene();
if (_scene?.Item != null)
Render(true);
}
}
/// <inheritdoc/>
public void Resized(Size size)
{
}
/// <inheritdoc/>
public void Start()
{
lock (_startStopLock)
{
if (!_running && _renderLoop != null)
{
_renderLoop.Add(this);
_running = true;
}
}
}
/// <inheritdoc/>
public void Stop()
{
lock (_startStopLock)
{
if (_running && _renderLoop != null)
{
_renderLoop.Remove(this);
_running = false;
}
}
}
public ValueTask<object?> TryGetRenderInterfaceFeature(Type featureType)
{
if (_renderInterface == null)
return new((object?)null);
var tcs = new TaskCompletionSource<object?>();
_pendingRenderThreadJobs ??= new();
_pendingRenderThreadJobs.Add(() =>
{
try
{
using (_renderInterface.EnsureCurrent())
{
tcs.TrySetResult(_renderInterface.Value.TryGetFeature(featureType));
}
}
catch (Exception e)
{
tcs.TrySetException(e);
}
});
return new ValueTask<object?>(tcs.Task);
}
bool NeedsUpdate => _dirty == null || _dirty.Count > 0;
bool IRenderLoopTask.NeedsUpdate => NeedsUpdate;
void IRenderLoopTask.Update(TimeSpan time) => UpdateScene();
void IRenderLoopTask.Render()
{
lock (_renderLoopIsRenderingLock)
{
lock(_startStopLock)
if(!_running)
return;
Render(false);
}
}
static Scene? TryGetChildScene(IRef<IDrawOperation>? op) => (op?.Item as BrushDrawOperation)?.Aux as Scene;
/// <inheritdoc/>
Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
{
return TryGetChildScene(_currentDraw)?.Size ?? default;
}
/// <inheritdoc/>
void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush)
{
var childScene = TryGetChildScene(_currentDraw);
if (childScene != null)
{
Render(context, (VisualNode)childScene.Root, null, new Rect(childScene.Size));
}
}
internal void UnitTestUpdateScene() => UpdateScene();
internal void UnitTestRender() => Render(false);
internal Scene? UnitTestScene() => _scene?.Item;
private void EnsureCanHitTest()
{
if (_renderLoop == null && (_dirty == null || _dirty.Count > 0))
{
// When unit testing the renderLoop may be null, so update the scene manually.
UpdateScene();
}
}
internal void Render(bool forceComposite)
{
using (var l = _lock.TryLock())
{
if (l == null)
return;
IDrawingContextImpl? context = null;
try
{
try
{
var (scene, updated) = UpdateRenderLayersAndConsumeSceneIfNeeded(ref context);
if (updated)
FpsTick();
using (scene)
{
if (scene?.Item != null)
{
try
{
var overlay = DrawDirtyRects || DrawFps;
if (DrawDirtyRects)
_dirtyRectsDisplay.Tick();
if (overlay)
RenderOverlay(scene.Item, ref context);
if (updated || forceComposite || overlay)
RenderComposite(scene.Item, ref context);
}
finally
{
try
{
if(scene.Item.RenderThreadJobs!=null)
foreach (var job in scene.Item.RenderThreadJobs)
job();
}
finally
{
scene.Item.MarkAsRendered();
}
}
}
}
}
finally
{
context?.Dispose();
}
}
catch (RenderTargetCorruptedException ex)
{
Logger.TryGet(LogEventLevel.Information, LogArea.Animations)?.Log(this, "Render target was corrupted. Exception: {0}", ex);
RenderTarget?.Dispose();
RenderTarget = null;
}
}
}
private (IRef<Scene>? scene, bool updated) UpdateRenderLayersAndConsumeSceneIfNeeded(ref IDrawingContextImpl? context,
bool recursiveCall = false)
{
IRef<Scene>? sceneRef;
lock (_sceneLock)
sceneRef = _scene?.Clone();
if (sceneRef == null)
return (null, false);
using (sceneRef)
{
var scene = sceneRef.Item;
if (scene.Generation != _lastSceneId)
{
EnsureDrawingContext(ref context);
Layers.Update(scene, context);
RenderToLayers(scene);
if (DebugFramesPath != null)
{
SaveDebugFrames(scene.Generation);
}
lock (_sceneLock)
_lastSceneId = scene.Generation;
var isUiThread = Dispatcher.UIThread.CheckAccess();
// We have consumed the previously available scene, but there might be some dirty
// rects since the last update. *If* we are on UI thread, we can force immediate scene
// rebuild before rendering anything on-screen
// We are calling the same method recursively here
if (!recursiveCall && isUiThread && NeedsUpdate)
{
UpdateScene();
var (rs, _) = UpdateRenderLayersAndConsumeSceneIfNeeded(ref context, true);
return (rs, true);
}
// We are rendering a new scene version, so it's highly likely
// that there is already a pending update for animations
// So we are scheduling an update call so UI thread could prepare a scene before
// the next render timer tick
if (!recursiveCall && !isUiThread)
Dispatcher.UIThread.Post(_updateSceneIfNeededDelegate, DispatcherPriority.Render);
// Indicate that we have updated the layers
return (sceneRef.Clone(), true);
}
// Just return scene, layers weren't updated
return (sceneRef.Clone(), false);
}
}
private void Render(IDrawingContextImpl context, VisualNode node, Visual? layer, Rect clipBounds)
{
if (layer == null || node.LayerRoot == layer)
{
clipBounds = node.ClipBounds.Intersect(clipBounds);
if (!clipBounds.IsDefault && node.Opacity > 0)
{
var isLayerRoot = node.Visual == layer;
node.BeginRender(context, isLayerRoot);
var drawOperations = node.DrawOperations;
var drawOperationsCount = drawOperations.Count;
for (int i = 0; i < drawOperationsCount; i++)
{
var operation = drawOperations[i];
_currentDraw = operation;
operation.Item.Render(context);
_currentDraw = null;
}
var children = node.Children;
var childrenCount = children.Count;
for (int i = 0; i < childrenCount; i++)
{
var child = children[i];
Render(context, (VisualNode)child, layer, clipBounds);
}
node.EndRender(context, isLayerRoot);
}
}
}
private void RenderToLayers(Scene scene)
{
foreach (var layer in scene.Layers)
{
var renderLayer = Layers[layer.LayerRoot];
if (layer.Dirty.IsEmpty && !renderLayer.IsEmpty)
continue;
var renderTarget = renderLayer.Bitmap;
var node = (VisualNode?)scene.FindNode(layer.LayerRoot);
if (node != null)
{
using (var context = renderTarget.Item.CreateDrawingContext(this))
{
if (renderLayer.IsEmpty)
{
// Render entire layer root node
context.Clear(Colors.Transparent);
context.Transform = Matrix.Identity;
context.PushClip(node.ClipBounds);
Render(context, node, layer.LayerRoot, node.ClipBounds);
context.PopClip();
if (DrawDirtyRects)
{
_dirtyRectsDisplay.Add(node.ClipBounds);
}
renderLayer.IsEmpty = false;
}
else
{
var scale = scene.Scaling;
foreach (var rect in layer.Dirty)
{
var snappedRect = SnapToDevicePixels(rect, scale);
context.Transform = Matrix.Identity;
context.PushClip(snappedRect);
context.Clear(Colors.Transparent);
Render(context, node, layer.LayerRoot, snappedRect);
context.PopClip();
if (DrawDirtyRects)
{
_dirtyRectsDisplay.Add(snappedRect);
}
}
}
}
}
}
}
private static Rect SnapToDevicePixels(Rect rect, double scale)
{
return new Rect(
new Point(
Math.Floor(rect.X * scale) / scale,
Math.Floor(rect.Y * scale) / scale),
new Point(
Math.Ceiling(rect.Right * scale) / scale,
Math.Ceiling(rect.Bottom * scale) / scale));
}
private void RenderOverlay(Scene scene, ref IDrawingContextImpl? parentContent)
{
EnsureDrawingContext(ref parentContent);
if (DrawDirtyRects)
{
var overlay = GetOverlay(parentContent, scene.Size, scene.Scaling);
using (var context = overlay.Item.CreateDrawingContext(this))
{
context.Clear(Colors.Transparent);
RenderDirtyRects(context);
}
}
else
{
_overlay?.Dispose();
_overlay = null;
}
}
private void RenderDirtyRects(IDrawingContextImpl context)
{
foreach (var r in _dirtyRectsDisplay)
{
var brush = new ImmutableSolidColorBrush(Colors.Magenta, r.Opacity);
context.DrawRectangle(brush,null, r.Rect);
}
}
private void RenderComposite(Scene scene, ref IDrawingContextImpl? context)
{
EnsureDrawingContext(ref context);
context.Clear(Colors.Transparent);
var clientRect = new Rect(scene.Size);
var firstLayer = true;
foreach (var layer in scene.Layers)
{
var bitmap = Layers[layer.LayerRoot].Bitmap;
var sourceRect = new Rect(0, 0, bitmap.Item.PixelSize.Width, bitmap.Item.PixelSize.Height);
if (layer.GeometryClip != null)
{
context.PushGeometryClip(layer.GeometryClip);
}
if (layer.OpacityMask == null)
{
if (firstLayer && bitmap.Item.CanBlit)
bitmap.Item.Blit(context);
else
context.DrawBitmap(bitmap, layer.Opacity, sourceRect, clientRect);
}
else
{
context.DrawBitmap(bitmap, layer.OpacityMask, layer.OpacityMaskRect, sourceRect);
}
if (layer.GeometryClip != null)
{
context.PopGeometryClip();
}
firstLayer = false;
}
if (_overlay != null)
{
var sourceRect = new Rect(0, 0, _overlay.Item.PixelSize.Width, _overlay.Item.PixelSize.Height);
context.DrawBitmap(_overlay, 0.5, sourceRect, clientRect);
}
if (DrawFps)
{
using (var c = new DrawingContext(context, false))
{
RenderFps(c, clientRect, scene.Layers.Count);
}
}
}
private void EnsureDrawingContext([NotNull] ref IDrawingContextImpl? context)
{
if (context != null)
{
return;
}
if (RenderTarget?.IsCorrupted == true)
{
RenderTarget!.Dispose();
RenderTarget = null;
}
if (RenderTarget == null)
{
RenderTarget = _renderTargetFactory!();
}
context = RenderTarget.CreateDrawingContext(this);
}
private void UpdateSceneIfNeeded()
{
if(NeedsUpdate)
UpdateScene();
}
private bool? UpdateScene()
{
Dispatcher.UIThread.VerifyAccess();
using var noPump = NonPumpingLockHelper.Use();
lock (_sceneLock)
{
if (_disposed)
return null;
if (_scene?.Item.Generation > _lastSceneId)
return false;
}
if (_root.IsVisible)
{
var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root)
{
RenderThreadJobs = _pendingRenderThreadJobs
});
_pendingRenderThreadJobs = null;
var scene = sceneRef.Item;
if (_dirty == null)
{
_dirty = new DirtyVisuals();
_recalculateChildren = new HashSet<Visual>();
_sceneBuilder.UpdateAll(scene);
}
else
{
foreach (var visual in _recalculateChildren!)
{
var node = scene.FindNode(visual);
((VisualNode?)node)?.SortChildren(scene);
}
_recalculateChildren.Clear();
foreach (var visual in _dirty)
{
_sceneBuilder.Update(scene, visual);
}
}
lock (_sceneLock)
{
var oldScene = _scene;
_scene = sceneRef;
oldScene?.Dispose();
}
_dirty.Clear();
if (SceneInvalidated != null)
{
var rect = new Rect();
foreach (var layer in scene.Layers)
{
foreach (var dirty in layer.Dirty)
{
rect = rect.Union(dirty);
}
}
SceneInvalidated(this, new SceneInvalidatedEventArgs((IRenderRoot)_root, rect));
}
return true;
}
else
{
lock (_sceneLock)
{
var oldScene = _scene;
_scene = null;
oldScene?.Dispose();
}
return null;
}
}
private IRef<IRenderTargetBitmapImpl> GetOverlay(
IDrawingContextImpl parentContext,
Size size,
double scaling)
{
var pixelSize = size * scaling;
if (_overlay == null ||
_overlay.Item.PixelSize.Width != pixelSize.Width ||
_overlay.Item.PixelSize.Height != pixelSize.Height)
{
_overlay?.Dispose();
_overlay = RefCountable.Create(parentContext.CreateLayer(size));
}
return _overlay;
}
private void SaveDebugFrames(int id)
{
var index = 0;
foreach (var layer in Layers)
{
var fileName = Path.Combine(DebugFramesPath ?? string.Empty, FormattableString.Invariant($"frame-{id}-layer-{index++}.png"));
layer.Bitmap.Item.Save(fileName);
}
}
}
}

16
src/Avalonia.Base/Rendering/ICustomHitTest.cs

@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.VisualTree;
namespace Avalonia.Rendering
{
/// <summary>
/// Allows customization of hit-testing
/// </summary>
public interface ICustomHitTest
{
/// <param name="point">The point to hit test in global coordinate space.</param>
bool HitTest(Point point);
}
}

33
src/Avalonia.Base/Rendering/ICustomSimpleHitTest.cs

@ -1,33 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.VisualTree;
namespace Avalonia.Rendering
{
/// <summary>
/// An interface to allow non-templated controls to customize their hit-testing
/// when using a renderer with a simple hit-testing algorithm without a scene graph,
/// such as <see cref="ImmediateRenderer" />
/// </summary>
public interface ICustomSimpleHitTest
{
/// <param name="point">The point to hit test in global coordinate space.</param>
bool HitTest(Point point);
}
/// <summary>
/// Allows customization of hit-testing for all renderers.
/// </summary>
public interface ICustomHitTest : ICustomSimpleHitTest
{
}
public static class CustomSimpleHitTestExtensions
{
public static bool HitTestCustom(this Visual visual, Point point)
=> (visual as ICustomSimpleHitTest)?.HitTest(point) ?? visual.TransformedBounds?.Contains(point) == true;
public static bool HitTestCustom(this IEnumerable<Visual> children, Point point)
=> children.Any(ctrl => ctrl.HitTestCustom(point));
}
}

6
src/Avalonia.Base/Rendering/IRenderRoot.cs

@ -25,12 +25,6 @@ namespace Avalonia.Rendering
/// </summary> /// </summary>
double RenderScaling { get; } double RenderScaling { get; }
/// <summary>
/// Adds a rectangle to the window's dirty region.
/// </summary>
/// <param name="rect">The rectangle.</param>
void Invalidate(Rect rect);
/// <summary> /// <summary>
/// Converts a point from screen to client coordinates. /// Converts a point from screen to client coordinates.
/// </summary> /// </summary>

16
src/Avalonia.Base/Rendering/IRendererFactory.cs

@ -1,16 +0,0 @@

namespace Avalonia.Rendering
{
/// <summary>
/// Defines the interface for a renderer factory.
/// </summary>
public interface IRendererFactory
{
/// <summary>
/// Creates a renderer.
/// </summary>
/// <param name="root">The root visual.</param>
/// <param name="renderLoop">The render loop.</param>
IRenderer Create(IRenderRoot root, IRenderLoop renderLoop);
}
}

248
src/Avalonia.Base/Rendering/ImmediateRenderer.cs

@ -10,102 +10,12 @@ using Avalonia.VisualTree;
namespace Avalonia.Rendering namespace Avalonia.Rendering
{ {
/// <summary> /// <summary>
/// A renderer which renders the state of the visual tree without an intermediate scene graph /// This class is used to render the visual tree into a DrawingContext by doing
/// representation. /// a simple tree traversal.
/// It's currently used mostly for RenderTargetBitmap.Render and VisualBrush
/// </summary> /// </summary>
/// <remarks> internal class ImmediateRenderer : IVisualBrushRenderer//, IRenderer
/// The immediate renderer supports only clip-bound-based hit testing; a control's geometry is
/// not taken into account.
/// </remarks>
public class ImmediateRenderer : RendererBase, IRenderer, IVisualBrushRenderer
{ {
private readonly Visual _root;
private readonly Func<IRenderTarget> _renderTargetFactory;
private readonly PlatformRenderInterfaceContextManager? _renderContext;
private readonly IRenderRoot? _renderRoot;
private bool _updateTransformedBounds = true;
private IRenderTarget? _renderTarget;
/// <summary>
/// Initializes a new instance of the <see cref="ImmediateRenderer"/> class.
/// </summary>
/// <param name="root">The control to render.</param>
/// <param name="renderTargetFactory">The target render factory.</param>
/// <param name="renderContext">The render contex.</param>
public ImmediateRenderer(Visual root, Func<IRenderTarget> renderTargetFactory,
PlatformRenderInterfaceContextManager? renderContext = null)
{
_root = root ?? throw new ArgumentNullException(nameof(root));
_renderTargetFactory = renderTargetFactory;
_renderContext = renderContext;
_renderRoot = root as IRenderRoot;
}
private ImmediateRenderer(Visual root, Func<IRenderTarget> renderTargetFactory, bool updateTransformedBounds)
{
_root = root ?? throw new ArgumentNullException(nameof(root));
_renderTargetFactory = renderTargetFactory;
_renderRoot = root as IRenderRoot;
_updateTransformedBounds = updateTransformedBounds;
}
/// <inheritdoc/>
public bool DrawFps { get; set; }
/// <inheritdoc/>
public bool DrawDirtyRects { get; set; }
/// <inheritdoc/>
public event EventHandler<SceneInvalidatedEventArgs>? SceneInvalidated;
/// <inheritdoc/>
public void Paint(Rect rect)
{
if (_renderTarget == null)
{
_renderTarget = _renderTargetFactory();
}
try
{
using (var context = new DrawingContext(_renderTarget.CreateDrawingContext(this)))
{
context.PlatformImpl.Clear(Colors.Transparent);
using (context.PushTransformContainer())
{
Render(context, _root, _root.Bounds);
}
if (DrawDirtyRects)
{
var color = (uint)new Random().Next(0xffffff) | 0x44000000;
context.FillRectangle(
new SolidColorBrush(color),
rect);
}
if (DrawFps)
{
RenderFps(context, _root.Bounds, null);
}
}
}
catch (RenderTargetCorruptedException ex)
{
Logger.TryGet(LogEventLevel.Information, LogArea.Animations)?.Log(this, "Render target was corrupted. Exception: {0}", ex);
_renderTarget.Dispose();
_renderTarget = null;
}
SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs((IRenderRoot)_root, rect));
}
/// <inheritdoc/>
public void Resized(Size size)
{
}
/// <summary> /// <summary>
/// Renders a visual to a render target. /// Renders a visual to a render target.
/// </summary> /// </summary>
@ -113,11 +23,8 @@ namespace Avalonia.Rendering
/// <param name="target">The render target.</param> /// <param name="target">The render target.</param>
public static void Render(Visual visual, IRenderTarget target) public static void Render(Visual visual, IRenderTarget target)
{ {
using (var renderer = new ImmediateRenderer(visual, () => target, updateTransformedBounds: false)) using var context = new DrawingContext(target.CreateDrawingContext(new ImmediateRenderer()));
using (var context = new DrawingContext(target.CreateDrawingContext(renderer))) Render(context, visual, visual.Bounds);
{
renderer.Render(context, visual, visual.Bounds);
}
} }
/// <summary> /// <summary>
@ -127,77 +34,9 @@ namespace Avalonia.Rendering
/// <param name="context">The drawing context.</param> /// <param name="context">The drawing context.</param>
public static void Render(Visual visual, DrawingContext context) public static void Render(Visual visual, DrawingContext context)
{ {
using (var renderer = new ImmediateRenderer(visual, Render(context, visual, visual.Bounds);
() => throw new InvalidOperationException("This is not supposed to be called"),
updateTransformedBounds: false))
{
renderer.Render(context, visual, visual.Bounds);
}
} }
/// <inheritdoc/>
public void AddDirty(Visual visual)
{
if (!visual.Bounds.IsDefault)
{
var m = visual.TransformToVisual(_root);
if (m.HasValue)
{
var bounds = new Rect(visual.Bounds.Size).TransformToAABB(m.Value);
//use transformedbounds as previous render state of the visual bounds
//so we can invalidate old and new bounds of a control in case it moved/shrinked
if (visual.TransformedBounds.HasValue)
{
var trb = visual.TransformedBounds.Value;
var trBounds = trb.Bounds.TransformToAABB(trb.Transform);
if (trBounds != bounds)
{
_renderRoot?.Invalidate(trBounds);
}
}
_renderRoot?.Invalidate(bounds);
}
}
}
/// <summary>
/// Ends the operation of the renderer.
/// </summary>
public void Dispose()
{
_renderTarget?.Dispose();
}
/// <inheritdoc/>
public IEnumerable<Visual> HitTest(Point p, Visual root, Func<Visual, bool> filter)
{
return HitTest(root, p, filter);
}
public Visual? HitTestFirst(Point p, Visual root, Func<Visual, bool> filter)
{
return HitTest(root, p, filter).FirstOrDefault();
}
/// <inheritdoc/>
public void RecalculateChildren(Visual visual) => AddDirty(visual);
/// <inheritdoc/>
public void Start()
{
}
/// <inheritdoc/>
public void Stop()
{
}
public ValueTask<object?> TryGetRenderInterfaceFeature(Type featureType) =>
new(_renderContext?.Value?.TryGetFeature(featureType));
/// <inheritdoc/> /// <inheritdoc/>
Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
@ -215,18 +54,7 @@ namespace Avalonia.Rendering
internal static void Render(Visual visual, DrawingContext context, bool updateTransformedBounds) internal static void Render(Visual visual, DrawingContext context, bool updateTransformedBounds)
{ {
using var renderer = new ImmediateRenderer(visual, Render(context, visual, visual.Bounds);
() => throw new InvalidOperationException("This is not supposed to be called"),
updateTransformedBounds);
renderer.Render(context, visual, visual.Bounds);
}
private static void ClearTransformedBounds(Visual visual)
{
foreach (var e in visual.GetSelfAndVisualDescendants())
{
visual.SetTransformedBounds(null);
}
} }
private static Rect GetTransformedBounds(Visual visual) private static Rect GetTransformedBounds(Visual visual)
@ -244,45 +72,8 @@ namespace Avalonia.Rendering
} }
} }
private static IEnumerable<Visual> HitTest(
Visual visual,
Point p,
Func<Visual, bool>? filter)
{
_ = visual ?? throw new ArgumentNullException(nameof(visual));
if (filter?.Invoke(visual) != false)
{
bool containsPoint;
if (visual is ICustomSimpleHitTest custom)
{
containsPoint = custom.HitTest(p);
}
else
{
containsPoint = visual.TransformedBounds?.Contains(p) == true;
}
if ((containsPoint || !visual.ClipToBounds) && visual.VisualChildren.Count > 0)
{
foreach (var child in visual.VisualChildren.SortByZIndex())
{
foreach (var result in HitTest(child, p, filter))
{
yield return result;
}
}
}
if (containsPoint) private static void Render(DrawingContext context, Visual visual, Rect clipRect)
{
yield return visual;
}
}
}
private void Render(DrawingContext context, Visual visual, Rect clipRect)
{ {
var opacity = visual.Opacity; var opacity = visual.Opacity;
var clipToBounds = visual.ClipToBounds; var clipToBounds = visual.ClipToBounds;
@ -338,15 +129,7 @@ namespace Avalonia.Rendering
using (context.PushTransformContainer()) using (context.PushTransformContainer())
{ {
visual.Render(context); visual.Render(context);
#pragma warning disable 0618
var transformed =
new TransformedBounds(bounds, new Rect(), context.CurrentContainerTransform);
#pragma warning restore 0618
if (_updateTransformedBounds)
visual.SetTransformedBounds(transformed);
var childrenEnumerable = visual.HasNonUniformZIndexChildren var childrenEnumerable = visual.HasNonUniformZIndexChildren
? visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance) ? visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance)
: (IEnumerable<Visual>)visual.VisualChildren; : (IEnumerable<Visual>)visual.VisualChildren;
@ -362,18 +145,9 @@ namespace Avalonia.Rendering
: clipRect; : clipRect;
Render(context, child, childClipRect); Render(context, child, childClipRect);
} }
else if (_updateTransformedBounds)
{
ClearTransformedBounds(child);
}
} }
} }
} }
if (!visual.IsVisible && _updateTransformedBounds)
{
ClearTransformedBounds(visual);
}
} }
} }
} }

4
src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs

@ -6,9 +6,7 @@ using Avalonia.Reactive;
namespace Avalonia.Rendering; namespace Avalonia.Rendering;
[Unstable] internal class PlatformRenderInterfaceContextManager
// TODO: Make it internal once legacy renderers are removed
public class PlatformRenderInterfaceContextManager
{ {
private readonly IPlatformGraphics? _graphics; private readonly IPlatformGraphics? _graphics;
private IPlatformRenderInterfaceContext? _backend; private IPlatformRenderInterfaceContext? _backend;

48
src/Avalonia.Base/Rendering/RenderLayer.cs

@ -1,48 +0,0 @@
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Utilities;
using Avalonia.VisualTree;
namespace Avalonia.Rendering
{
public class RenderLayer
{
public RenderLayer(
IDrawingContextImpl drawingContext,
Size size,
double scaling,
Visual layerRoot)
{
Bitmap = RefCountable.Create(drawingContext.CreateLayer(size));
Size = size;
Scaling = scaling;
LayerRoot = layerRoot;
IsEmpty = true;
}
public IRef<IDrawingContextLayerImpl> Bitmap { get; private set; }
public bool IsEmpty { get; set; }
public double Scaling { get; private set; }
public Size Size { get; private set; }
public Visual LayerRoot { get; }
public void RecreateBitmap(IDrawingContextImpl drawingContext, Size size, double scaling)
{
if (Size != size || Scaling != scaling)
{
var resized = RefCountable.Create(drawingContext.CreateLayer(size));
using (var context = resized.Item.CreateDrawingContext(null))
{
Bitmap.Dispose();
context.Clear(default);
Bitmap = resized;
Scaling = scaling;
Size = size;
IsEmpty = true;
}
}
}
}
}

69
src/Avalonia.Base/Rendering/RenderLayers.cs

@ -1,69 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
namespace Avalonia.Rendering
{
public class RenderLayers : IEnumerable<RenderLayer>
{
private readonly List<RenderLayer> _inner = new List<RenderLayer>();
private readonly Dictionary<Visual, RenderLayer> _index = new Dictionary<Visual, RenderLayer>();
public int Count => _inner.Count;
public RenderLayer this[Visual layerRoot] => _index[layerRoot];
public void Update(Scene scene, IDrawingContextImpl context)
{
for (var i = scene.Layers.Count - 1; i >= 0; --i)
{
var src = scene.Layers[i];
if (!_index.TryGetValue(src.LayerRoot, out var layer))
{
layer = new RenderLayer(context, scene.Size, scene.Scaling, src.LayerRoot);
_inner.Add(layer);
_index.Add(src.LayerRoot, layer);
}
else
{
layer.RecreateBitmap(context, scene.Size, scene.Scaling);
}
}
for (var i = 0; i < _inner.Count;)
{
var layer = _inner[i];
if (!scene.Layers.Exists(layer.LayerRoot))
{
layer.Bitmap.Dispose();
_inner.RemoveAt(i);
_index.Remove(layer.LayerRoot);
}
else
i++;
}
}
public void Clear()
{
foreach (var layer in _index.Values)
{
layer.Bitmap.Dispose();
}
_index.Clear();
_inner.Clear();
}
public bool TryGetValue(Visual layerRoot, [NotNullWhen(true)] out RenderLayer? value)
{
return _index.TryGetValue(layerRoot, out value);
}
public IEnumerator<RenderLayer> GetEnumerator() => _inner.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

482
src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@ -1,482 +0,0 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Utilities;
using Avalonia.Media.Imaging;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// A drawing context which builds a scene graph.
/// </summary>
internal class DeferredDrawingContextImpl : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport
{
private readonly ISceneBuilder _sceneBuilder;
private VisualNode? _node;
private int _childIndex;
private int _drawOperationindex;
/// <summary>
/// Initializes a new instance of the <see cref="DeferredDrawingContextImpl"/> class.
/// </summary>
/// <param name="sceneBuilder">
/// A scene builder used for constructing child scenes for visual brushes.
/// </param>
/// <param name="layers">The scene layers.</param>
public DeferredDrawingContextImpl(ISceneBuilder sceneBuilder, SceneLayers layers)
{
_sceneBuilder = sceneBuilder;
Layers = layers;
}
/// <inheritdoc/>
public Matrix Transform { get; set; } = Matrix.Identity;
/// <summary>
/// Gets the layers in the scene being built.
/// </summary>
public SceneLayers Layers { get; }
/// <summary>
/// Informs the drawing context of the visual node that is about to be rendered.
/// </summary>
/// <param name="node">The visual node.</param>
/// <returns>
/// An object which when disposed will commit the changes to visual node.
/// </returns>
public UpdateState BeginUpdate(VisualNode node)
{
_ = node ?? throw new ArgumentNullException(nameof(node));
if (_node != null)
{
if (_childIndex < _node.Children.Count)
{
_node.ReplaceChild(_childIndex, node);
}
else
{
_node.AddChild(node);
}
++_childIndex;
}
var state = new UpdateState(this, _node, _childIndex, _drawOperationindex);
_node = node;
_childIndex = _drawOperationindex = 0;
return state;
}
/// <inheritdoc/>
public void Clear(Color color)
{
// Cannot clear a deferred scene.
}
/// <inheritdoc/>
public void Dispose()
{
// Nothing to do here since we allocate no unmanaged resources.
}
/// <summary>
/// Removes any remaining drawing operations from the visual node.
/// </summary>
/// <remarks>
/// Drawing operations are updated in place, overwriting existing drawing operations if
/// they are different. Once drawing has completed for the current visual node, it is
/// possible that there are stale drawing operations at the end of the list. This method
/// trims these stale drawing operations.
/// </remarks>
public void TrimChildren()
{
_node!.TrimChildren(_childIndex);
}
/// <inheritdoc/>
public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry)
{
var next = NextDrawAs<GeometryNode>();
if (next == null || !next.Item.Equals(Transform, brush, pen, geometry))
{
Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush)));
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
{
var next = NextDrawAs<ImageNode>();
if (next == null || !next.Item.Equals(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode))
{
Add(new ImageNode(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode));
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect)
{
// This method is currently only used to composite layers so shouldn't be called here.
throw new NotSupportedException();
}
/// <inheritdoc/>
public void DrawLine(IPen pen, Point p1, Point p2)
{
var next = NextDrawAs<LineNode>();
if (next == null || !next.Item.Equals(Transform, pen, p1, p2))
{
Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush)));
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect,
BoxShadows boxShadows = default)
{
var next = NextDrawAs<RectangleNode>();
if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows))
{
Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush)));
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect)
{
var next = NextDrawAs<ExperimentalAcrylicNode>();
if (next == null || !next.Item.Equals(Transform, material, rect))
{
Add(new ExperimentalAcrylicNode(Transform, material, rect));
}
else
{
++_drawOperationindex;
}
}
public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect)
{
var next = NextDrawAs<EllipseNode>();
if (next == null || !next.Item.Equals(Transform, brush, pen, rect))
{
Add(new EllipseNode(Transform, brush, pen, rect, CreateChildScene(brush)));
}
else
{
++_drawOperationindex;
}
}
public void Custom(ICustomDrawOperation custom)
{
var next = NextDrawAs<CustomDrawOperation>();
if (next == null || !next.Item.Equals(Transform, custom))
Add(new CustomDrawOperation(custom, Transform));
else
++_drawOperationindex;
}
public object? GetFeature(Type t) => null;
/// <inheritdoc/>
public void DrawGlyphRun(IBrush foreground, IRef<IGlyphRunImpl> glyphRun)
{
var next = NextDrawAs<GlyphRunNode>();
if (next == null || !next.Item.Equals(Transform, foreground, glyphRun))
{
Add(new GlyphRunNode(Transform, foreground, glyphRun, CreateChildScene(foreground)));
}
else
{
++_drawOperationindex;
}
}
public IDrawingContextLayerImpl CreateLayer(Size size)
{
throw new NotSupportedException("Creating layers on a deferred drawing context not supported");
}
/// <inheritdoc/>
public void PopClip()
{
var next = NextDrawAs<ClipNode>();
if (next == null || !next.Item.Equals(null))
{
Add(new ClipNode());
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void PopGeometryClip()
{
var next = NextDrawAs<GeometryClipNode>();
if (next == null || !next.Item.Equals(null))
{
Add(new GeometryClipNode());
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void PopBitmapBlendMode()
{
var next = NextDrawAs<BitmapBlendModeNode>();
if (next == null || !next.Item.Equals(null))
{
Add(new BitmapBlendModeNode());
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void PopOpacity()
{
var next = NextDrawAs<OpacityNode>();
if (next == null || !next.Item.Equals(null))
{
Add(new OpacityNode());
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void PopOpacityMask()
{
var next = NextDrawAs<OpacityMaskNode>();
if (next == null || !next.Item.Equals(null, null))
{
Add(new OpacityMaskNode());
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void PushClip(Rect clip)
{
var next = NextDrawAs<ClipNode>();
if (next == null || !next.Item.Equals(Transform, clip))
{
Add(new ClipNode(Transform, clip));
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc />
public void PushClip(RoundedRect clip)
{
var next = NextDrawAs<ClipNode>();
if (next == null || !next.Item.Equals(Transform, clip))
{
Add(new ClipNode(Transform, clip));
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void PushGeometryClip(IGeometryImpl? clip)
{
if (clip is null)
return;
var next = NextDrawAs<GeometryClipNode>();
if (next == null || !next.Item.Equals(Transform, clip))
{
Add(new GeometryClipNode(Transform, clip));
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void PushOpacity(double opacity)
{
var next = NextDrawAs<OpacityNode>();
if (next == null || !next.Item.Equals(opacity))
{
Add(new OpacityNode(opacity));
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void PushOpacityMask(IBrush mask, Rect bounds)
{
var next = NextDrawAs<OpacityMaskNode>();
if (next == null || !next.Item.Equals(mask, bounds))
{
Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask)));
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void PushBitmapBlendMode(BitmapBlendingMode blendingMode)
{
var next = NextDrawAs<BitmapBlendModeNode>();
if (next == null || !next.Item.Equals(blendingMode))
{
Add(new BitmapBlendModeNode(blendingMode));
}
else
{
++_drawOperationindex;
}
}
public readonly struct UpdateState : IDisposable
{
public UpdateState(
DeferredDrawingContextImpl owner,
VisualNode? node,
int childIndex,
int drawOperationIndex)
{
Owner = owner;
Node = node;
ChildIndex = childIndex;
DrawOperationIndex = drawOperationIndex;
}
public void Dispose()
{
Owner._node!.TrimDrawOperations(Owner._drawOperationindex);
var dirty = Owner.Layers.GetOrAdd(Owner._node.LayerRoot!).Dirty;
var drawOperations = Owner._node.DrawOperations;
var drawOperationsCount = drawOperations.Count;
for (var i = 0; i < drawOperationsCount; i++)
{
dirty.Add(drawOperations[i].Item.Bounds);
}
Owner._node = Node;
Owner._childIndex = ChildIndex;
Owner._drawOperationindex = DrawOperationIndex;
}
public DeferredDrawingContextImpl Owner { get; }
public VisualNode? Node { get; }
public int ChildIndex { get; }
public int DrawOperationIndex { get; }
}
private void Add<T>(T node) where T : class, IDrawOperation
{
using (var refCounted = RefCountable.Create(node))
{
Add(refCounted);
}
}
private void Add(IRef<IDrawOperation> node)
{
if (_drawOperationindex < _node!.DrawOperations.Count)
{
_node.ReplaceDrawOperation(_drawOperationindex, node);
}
else
{
_node.AddDrawOperation(node);
}
++_drawOperationindex;
}
private IRef<T>? NextDrawAs<T>() where T : class, IDrawOperation
{
return _drawOperationindex < _node!.DrawOperations.Count ? _node.DrawOperations[_drawOperationindex] as IRef<T> : null;
}
private IDisposable? CreateChildScene(IBrush? brush)
{
var visualBrush = brush as VisualBrush;
if (visualBrush != null)
{
var visual = visualBrush.Visual;
if (visual != null)
{
(visual as IVisualBrushInitialize)?.EnsureInitialized();
var scene = new Scene(visual);
_sceneBuilder.UpdateAll(scene);
return scene;
}
}
return null;
}
}
}

24
src/Avalonia.Base/Rendering/SceneGraph/ISceneBuilder.cs

@ -1,24 +0,0 @@
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// Builds a scene graph from a visual tree.
/// </summary>
public interface ISceneBuilder
{
/// <summary>
/// Builds the initial scene graph for a visual tree.
/// </summary>
/// <param name="scene">The scene to build.</param>
void UpdateAll(Scene scene);
/// <summary>
/// Updates the visual (and potentially its children) in a scene.
/// </summary>
/// <param name="scene">The scene.</param>
/// <param name="visual">The visual to update.</param>
/// <returns>True if changes were made, otherwise false.</returns>
bool Update(Scene scene, Visual visual);
}
}

105
src/Avalonia.Base/Rendering/SceneGraph/IVisualNode.cs

@ -1,105 +0,0 @@
using System;
using System.Collections.Generic;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// Represents a node in the low-level scene graph representing a <see cref="Visual"/>.
/// </summary>
public interface IVisualNode : IDisposable
{
/// <summary>
/// Gets the visual to which the node relates.
/// </summary>
Visual Visual { get; }
/// <summary>
/// Gets the parent scene graph node.
/// </summary>
IVisualNode? Parent { get; }
/// <summary>
/// Gets the transform for the node from global to control coordinates.
/// </summary>
Matrix Transform { get; }
/// <summary>
/// Gets the corner radius of visual. Contents are clipped to this radius.
/// </summary>
CornerRadius ClipToBoundsRadius { get; }
/// <summary>
/// Gets the bounds of the node's geometry in global coordinates.
/// </summary>
Rect Bounds { get; }
/// <summary>
/// Gets the clip bounds for the node in global coordinates.
/// </summary>
Rect ClipBounds { get; }
/// <summary>
/// Gets the layout bounds for the node in global coordinates.
/// </summary>
Rect LayoutBounds { get; }
/// <summary>
/// Whether the node is clipped to <see cref="ClipBounds"/>.
/// </summary>
bool ClipToBounds { get; }
/// <summary>
/// Gets the node's clip geometry, if any.
/// </summary>
IGeometryImpl? GeometryClip { get; set; }
/// <summary>
/// Gets a value indicating whether one of the node's ancestors has a geometry clip.
/// </summary>
bool HasAncestorGeometryClip { get; }
/// <summary>
/// Gets the child scene graph nodes.
/// </summary>
IReadOnlyList<IVisualNode> Children { get; }
/// <summary>
/// Gets the drawing operations for the visual.
/// </summary>
IReadOnlyList<IRef<IDrawOperation>> DrawOperations { get; }
/// <summary>
/// Gets the opacity of the scene graph node.
/// </summary>
double Opacity { get; }
/// <summary>
/// Sets up the drawing context for rendering the node's geometry.
/// </summary>
/// <param name="context">The drawing context.</param>
/// <param name="skipOpacity">Whether to skip pushing the control's opacity.</param>
void BeginRender(IDrawingContextImpl context, bool skipOpacity);
/// <summary>
/// Resets the drawing context after rendering the node's geometry.
/// </summary>
/// <param name="context">The drawing context.</param>
/// <param name="skipOpacity">Whether to skip popping the control's opacity.</param>
void EndRender(IDrawingContextImpl context, bool skipOpacity);
/// <summary>
/// Hit test the geometry in this node.
/// </summary>
/// <param name="p">The point in global coordinates.</param>
/// <returns>True if the point hits the node's geometry; otherwise false.</returns>
/// <remarks>
/// This method does not recurse to child <see cref="IVisualNode"/>s, if you want
/// to hit test children they must be hit tested manually.
/// </remarks>
bool HitTest(Point p);
bool Disposed { get; }
}
}

352
src/Avalonia.Base/Rendering/SceneGraph/Scene.cs

@ -1,352 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Collections.Pooled;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// Represents a scene graph used by the <see cref="DeferredRenderer"/>.
/// </summary>
public class Scene : IDisposable
{
private readonly Dictionary<Visual, IVisualNode> _index;
private readonly TaskCompletionSource<bool> _rendered = new TaskCompletionSource<bool>();
/// <summary>
/// Initializes a new instance of the <see cref="Scene"/> class.
/// </summary>
/// <param name="rootVisual">The root visual to draw.</param>
public Scene(Visual rootVisual)
: this(
new VisualNode(rootVisual, null),
new Dictionary<Visual, IVisualNode>(),
new SceneLayers(rootVisual),
0)
{
_index.Add(rootVisual, Root);
}
private Scene(VisualNode root, Dictionary<Visual, IVisualNode> index, SceneLayers layers, int generation)
{
_ = root ?? throw new ArgumentNullException(nameof(root));
var renderRoot = root.Visual as IRenderRoot;
_index = index;
Root = root;
Layers = layers;
Generation = generation;
root.LayerRoot = root.Visual;
}
public Task Rendered => _rendered.Task;
/// <summary>
/// Gets a value identifying the scene's generation. This is incremented each time the scene is cloned.
/// </summary>
public int Generation { get; }
/// <summary>
/// Gets the layers for the scene.
/// </summary>
public SceneLayers Layers { get; }
/// <summary>
/// Gets the root node of the scene graph.
/// </summary>
public IVisualNode Root { get; }
/// <summary>
/// Gets or sets the size of the scene in device independent pixels.
/// </summary>
public Size Size { get; set; }
/// <summary>
/// Gets or sets the scene scaling.
/// </summary>
public double Scaling { get; set; } = 1;
/// <summary>
/// Adds a node to the scene index.
/// </summary>
/// <param name="node">The node.</param>
public void Add(IVisualNode node)
{
_ = node ?? throw new ArgumentNullException(nameof(node));
_index.Add(node.Visual, node);
}
/// <summary>
/// Clones the scene.
/// </summary>
/// <returns>The cloned scene.</returns>
public Scene CloneScene()
{
var index = new Dictionary<Visual, IVisualNode>(_index.Count);
var root = Clone((VisualNode)Root, null, index);
var result = new Scene(root, index, Layers.Clone(), Generation + 1)
{
Size = Size,
Scaling = Scaling,
};
return result;
}
public void Dispose()
{
_rendered.TrySetResult(false);
foreach (var node in _index.Values)
{
node.Dispose();
}
}
/// <summary>
/// Tries to find a node in the scene graph representing the specified visual.
/// </summary>
/// <param name="visual">The visual.</param>
/// <returns>
/// The node representing the visual or null if it could not be found.
/// </returns>
public IVisualNode? FindNode(Visual visual)
{
_index.TryGetValue(visual, out var node);
return node;
}
/// <summary>
/// Gets the visuals at a point in the scene.
/// </summary>
/// <param name="p">The point.</param>
/// <param name="root">The root of the subtree to search.</param>
/// <param name="filter">A filter. May be null.</param>
/// <returns>The visuals at the specified point.</returns>
public IEnumerable<Visual> HitTest(Point p, Visual root, Func<Visual, bool>? filter)
{
var node = FindNode(root);
return (node != null) ? new HitTestEnumerable(node, filter, p, Root) : Enumerable.Empty<Visual>();
}
/// <summary>
/// Gets the visual at a point in the scene.
/// </summary>
/// <param name="p">The point.</param>
/// <param name="root">The root of the subtree to search.</param>
/// <param name="filter">A filter. May be null.</param>
/// <returns>The visual at the specified point.</returns>
public Visual? HitTestFirst(Point p, Visual root, Func<Visual, bool>? filter)
{
var node = FindNode(root);
return (node != null) ? HitTestFirst(node, p, filter) : null;
}
/// <summary>
/// Removes a node from the scene index.
/// </summary>
/// <param name="node">The node.</param>
public void Remove(IVisualNode node)
{
_ = node ?? throw new ArgumentNullException(nameof(node));
_index.Remove(node.Visual);
node.Dispose();
}
private VisualNode Clone(VisualNode source, IVisualNode? parent, Dictionary<Visual, IVisualNode> index)
{
var result = source.Clone(parent);
index.Add(result.Visual, result);
var children = source.Children;
var childrenCount = children.Count;
if (childrenCount > 0)
{
result.TryPreallocateChildren(childrenCount);
for (var i = 0; i < childrenCount; i++)
{
var child = children[i];
result.AddChild(Clone((VisualNode)child, result, index));
}
}
return result;
}
private Visual HitTestFirst(IVisualNode root, Point p, Func<Visual, bool>? filter)
{
using var enumerator = new HitTestEnumerator(root, filter, p, Root);
enumerator.MoveNext();
return enumerator.Current;
}
private class HitTestEnumerable : IEnumerable<Visual>
{
private readonly IVisualNode _root;
private readonly Func<Visual, bool>? _filter;
private readonly IVisualNode _sceneRoot;
private readonly Point _point;
public HitTestEnumerable(IVisualNode root, Func<Visual, bool>? filter, Point point, IVisualNode sceneRoot)
{
_root = root;
_filter = filter;
_point = point;
_sceneRoot = sceneRoot;
}
public IEnumerator<Visual> GetEnumerator()
{
return new HitTestEnumerator(_root, _filter, _point, _sceneRoot);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
private struct HitTestEnumerator : IEnumerator<Visual>
{
private readonly PooledStack<Entry> _nodeStack;
private readonly Func<Visual, bool>? _filter;
private readonly IVisualNode _sceneRoot;
private Visual? _current;
private readonly Point _point;
public HitTestEnumerator(IVisualNode root, Func<Visual, bool>? filter, Point point, IVisualNode sceneRoot)
{
_nodeStack = new PooledStack<Entry>();
_nodeStack.Push(new Entry(root, false, null, true));
_filter = filter;
_point = point;
_sceneRoot = sceneRoot;
_current = null;
}
public bool MoveNext()
{
while (_nodeStack.Count > 0)
{
(var wasVisited, var isRoot, IVisualNode node, Rect? clip) = _nodeStack.Pop();
if (wasVisited && isRoot)
{
break;
}
var children = node.Children;
int childCount = children.Count;
if (childCount == 0 || wasVisited)
{
if ((wasVisited || FilterAndClip(node, ref clip)) &&
(node.Visual is ICustomHitTest custom ? custom.HitTest(_point) : node.HitTest(_point)))
{
_current = node.Visual;
return true;
}
}
else if (FilterAndClip(node, ref clip))
{
_nodeStack.Push(new Entry(node, true, null));
for (var i = 0; i < childCount; i++)
{
_nodeStack.Push(new Entry(children[i], false, clip));
}
}
}
return false;
}
public void Reset()
{
throw new NotSupportedException();
}
public Visual Current => _current!;
object IEnumerator.Current => Current;
public void Dispose()
{
_nodeStack.Dispose();
}
private bool FilterAndClip(IVisualNode node, ref Rect? clip)
{
if (_filter?.Invoke(node.Visual) != false && node.Visual.IsAttachedToVisualTree)
{
var clipped = false;
if (node.ClipToBounds)
{
clip = clip == null ? node.ClipBounds : clip.Value.Intersect(node.ClipBounds);
clipped = !clip.Value.ContainsExclusive(_point);
}
if (node.GeometryClip != null)
{
var controlPoint = _sceneRoot.Visual.TranslatePoint(_point, node.Visual);
clipped = !node.GeometryClip.FillContains(controlPoint!.Value);
}
if (!clipped && node.Visual is ICustomHitTest custom)
{
clipped = !custom.HitTest(_point);
}
return !clipped;
}
return false;
}
private readonly struct Entry
{
public readonly bool WasVisited;
public readonly bool IsRoot;
public readonly IVisualNode Node;
public readonly Rect? Clip;
public Entry(IVisualNode node, bool wasVisited, Rect? clip, bool isRoot = false)
{
Node = node;
WasVisited = wasVisited;
IsRoot = isRoot;
Clip = clip;
}
public void Deconstruct(out bool wasVisited, out bool isRoot, out IVisualNode node, out Rect? clip)
{
wasVisited = WasVisited;
isRoot = IsRoot;
node = Node;
clip = Clip;
}
}
}
public void MarkAsRendered() => _rendered.TrySetResult(true);
public List<Action>? RenderThreadJobs { get; set; }
}
}

485
src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs

@ -1,485 +0,0 @@
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// Builds a scene graph from a visual tree.
/// </summary>
public class SceneBuilder : ISceneBuilder
{
/// <inheritdoc/>
public void UpdateAll(Scene scene)
{
_ = scene ?? throw new ArgumentNullException(nameof(scene));
Dispatcher.UIThread.VerifyAccess();
UpdateSize(scene);
scene.Layers.GetOrAdd(scene.Root.Visual);
using (var impl = new DeferredDrawingContextImpl(this, scene.Layers))
using (var context = new DrawingContext(impl))
{
var clip = new Rect(scene.Root.Visual.Bounds.Size);
Update(context, scene, (VisualNode)scene.Root, clip, true);
}
}
/// <inheritdoc/>
public bool Update(Scene scene, Visual visual)
{
_ = scene ?? throw new ArgumentNullException(nameof(scene));
_ = visual ?? throw new ArgumentNullException(nameof(visual));
Dispatcher.UIThread.VerifyAccess();
if (!scene.Root.Visual.IsVisible)
{
throw new AvaloniaInternalException("Cannot update the scene for an invisible root visual.");
}
var node = (VisualNode?)scene.FindNode(visual);
if (visual == scene.Root.Visual)
{
UpdateSize(scene);
}
if (visual.VisualRoot == scene.Root.Visual)
{
if (node?.Parent != null &&
visual.VisualParent != null &&
node.Parent.Visual != visual.VisualParent)
{
// The control has changed parents. Remove the node and recurse into the new parent node.
((VisualNode)node.Parent).RemoveChild(node);
Deindex(scene, node);
node = (VisualNode?)scene.FindNode(visual.VisualParent);
}
if (visual.IsVisible)
{
// If the node isn't yet part of the scene, find the nearest ancestor that is.
node = node ?? FindExistingAncestor(scene, visual);
// We don't need to do anything if this part of the tree has already been fully
// updated.
if (node != null && !node.SubTreeUpdated)
{
// If the control we've been asked to update isn't part of the scene then
// we're carrying out an add operation, so recurse and add all the
// descendents too.
var recurse = node.Visual != visual;
using (var impl = new DeferredDrawingContextImpl(this, scene.Layers))
using (var context = new DrawingContext(impl))
{
var clip = new Rect(scene.Root.Visual.Bounds.Size);
if (node.Parent != null)
{
context.PushPostTransform(node.Parent.Transform);
clip = node.Parent.ClipBounds;
}
using (context.PushTransformContainer())
{
Update(context, scene, node, clip, recurse);
}
}
return true;
}
}
else
{
if (node != null)
{
// The control has been hidden so remove it from its parent and deindex the
// node and its descendents.
((VisualNode?)node.Parent)?.RemoveChild(node);
Deindex(scene, node);
return true;
}
}
}
else if (node != null)
{
// The control has been removed so remove it from its parent and deindex the
// node and its descendents.
var trim = FindFirstDeadAncestor(scene, node);
((VisualNode)trim.Parent!).RemoveChild(trim);
Deindex(scene, trim);
return true;
}
return false;
}
private static VisualNode? FindExistingAncestor(Scene scene, Visual visual)
{
var node = scene.FindNode(visual);
while (node == null && visual.IsVisible)
{
var parent = visual.VisualParent;
if (parent is null)
return null;
visual = parent;
node = scene.FindNode(visual);
}
return visual.IsVisible ? (VisualNode?)node : null;
}
private static VisualNode FindFirstDeadAncestor(Scene scene, IVisualNode node)
{
var parent = node.Parent;
while (parent!.Visual.VisualRoot == null)
{
node = parent;
parent = node.Parent;
}
return (VisualNode)node;
}
private static object GetOrCreateChildNode(Scene scene, Visual child, VisualNode parent)
{
var result = (VisualNode?)scene.FindNode(child);
if (result != null && result.Parent != parent)
{
Deindex(scene, result);
((VisualNode?)result.Parent)?.RemoveChild(result);
result = null;
}
return result ?? CreateNode(scene, child, parent);
}
private static void Update(DrawingContext context, Scene scene, VisualNode node, Rect clip, bool forceRecurse)
{
var visual = node.Visual;
var opacity = visual.Opacity;
var clipToBounds = visual.ClipToBounds;
#pragma warning disable CS0618 // Type or member is obsolete
var clipToBoundsRadius = visual is IVisualWithRoundRectClip roundRectClip ?
roundRectClip.ClipToBoundsRadius :
default;
#pragma warning restore CS0618 // Type or member is obsolete
var bounds = new Rect(visual.Bounds.Size);
var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl;
contextImpl.Layers.Find(node.LayerRoot!)?.Dirty.Add(node.Bounds);
if (visual.IsVisible)
{
var m = node != scene.Root ?
Matrix.CreateTranslation(visual.Bounds.Position) :
Matrix.Identity;
var renderTransform = Matrix.Identity;
// this should be calculated BEFORE renderTransform
if (visual.HasMirrorTransform)
{
var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0);
renderTransform *= mirrorMatrix;
}
if (visual.RenderTransform != null)
{
var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height));
var offset = Matrix.CreateTranslation(origin);
var finalTransform = (-offset) * visual.RenderTransform.Value * (offset);
renderTransform *= finalTransform;
}
m = renderTransform * m;
using (contextImpl.BeginUpdate(node))
using (context.PushPostTransform(m))
using (context.PushTransformContainer())
{
var globalBounds = bounds.TransformToAABB(contextImpl.Transform);
var clipBounds = clipToBounds ?
globalBounds.Intersect(clip) :
clip;
forceRecurse = forceRecurse ||
node.ClipBounds != clipBounds ||
node.Opacity != opacity ||
node.Transform != contextImpl.Transform;
node.Transform = contextImpl.Transform;
node.ClipBounds = clipBounds;
node.ClipToBounds = clipToBounds;
node.LayoutBounds = globalBounds;
node.ClipToBoundsRadius = clipToBoundsRadius;
node.GeometryClip = visual.Clip?.PlatformImpl;
node.Opacity = opacity;
// TODO: Check equality between node.OpacityMask and visual.OpacityMask before assigning.
node.OpacityMask = visual.OpacityMask?.ToImmutable();
if (ShouldStartLayer(visual))
{
if (node.LayerRoot != visual)
{
MakeLayer(scene, node);
}
else
{
UpdateLayer(node, scene.Layers[node.LayerRoot]);
}
}
else if (node.LayerRoot == node.Visual && node.Parent != null)
{
ClearLayer(scene, node);
}
if (node.ClipToBounds)
{
clip = clip.Intersect(node.ClipBounds);
}
try
{
visual.Render(context);
}
catch { }
var transformed = new TransformedBounds(new Rect(visual.Bounds.Size), clip, node.Transform);
visual.SetTransformedBounds(transformed);
if (forceRecurse)
{
var visualChildren = (IList<Visual>) visual.VisualChildren;
node.TryPreallocateChildren(visualChildren.Count);
if (visualChildren.Count == 1)
{
var childNode = GetOrCreateChildNode(scene, visualChildren[0], node);
Update(context, scene, (VisualNode)childNode, clip, forceRecurse);
}
else if (visualChildren.Count > 1)
{
var count = visualChildren.Count;
if (visual.HasNonUniformZIndexChildren)
{
var sortedChildren = new (Visual visual, int index)[count];
for (var i = 0; i < count; i++)
{
sortedChildren[i] = (visualChildren[i], i);
}
// Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements.
Array.Sort(sortedChildren, (lhs, rhs) =>
{
var result = ZIndexComparer.Instance.Compare(lhs.visual, rhs.visual);
return result == 0 ? lhs.index.CompareTo(rhs.index) : result;
});
foreach (var child in sortedChildren)
{
var childNode = GetOrCreateChildNode(scene, child.Item1, node);
Update(context, scene, (VisualNode)childNode, clip, forceRecurse);
}
}
else
foreach (var child in visualChildren)
{
var childNode = GetOrCreateChildNode(scene, child, node);
Update(context, scene, (VisualNode)childNode, clip, forceRecurse);
}
}
node.SubTreeUpdated = true;
contextImpl.TrimChildren();
}
}
}
else
{
contextImpl.BeginUpdate(node).Dispose();
}
}
private static void UpdateSize(Scene scene)
{
var renderRoot = scene.Root.Visual as IRenderRoot;
var newSize = renderRoot?.ClientSize ?? scene.Root.Visual.Bounds.Size;
scene.Scaling = renderRoot?.RenderScaling ?? 1;
if (scene.Size != newSize)
{
var oldSize = scene.Size;
scene.Size = newSize;
Rect horizontalDirtyRect = default;
Rect verticalDirtyRect = default;
if (newSize.Width > oldSize.Width)
{
horizontalDirtyRect = new Rect(oldSize.Width, 0, newSize.Width - oldSize.Width, oldSize.Height);
}
if (newSize.Height > oldSize.Height)
{
verticalDirtyRect = new Rect(0, oldSize.Height, newSize.Width, newSize.Height - oldSize.Height);
}
foreach (var layer in scene.Layers)
{
layer.Dirty.Add(horizontalDirtyRect);
layer.Dirty.Add(verticalDirtyRect);
}
}
}
private static VisualNode CreateNode(Scene scene, Visual visual, VisualNode parent)
{
var node = new VisualNode(visual, parent);
node.LayerRoot = parent.LayerRoot;
scene.Add(node);
return node;
}
private static void Deindex(Scene scene, VisualNode node)
{
var nodeChildren = node.Children;
var nodeChildrenCount = nodeChildren.Count;
for (var i = 0; i < nodeChildrenCount; i++)
{
if (nodeChildren[i] is VisualNode visual)
{
Deindex(scene, visual);
}
}
scene.Remove(node);
node.SubTreeUpdated = true;
scene.Layers[node.LayerRoot!].Dirty.Add(node.Bounds);
node.Visual.SetTransformedBounds(null);
if (node.LayerRoot == node.Visual && node.Visual != scene.Root.Visual)
{
scene.Layers.Remove(node.LayerRoot);
}
}
private static void ClearLayer(Scene scene, VisualNode node)
{
var parent = (VisualNode)node.Parent!;
var oldLayerRoot = node.LayerRoot;
var newLayerRoot = parent.LayerRoot!;
var existingDirtyRects = scene.Layers[node.LayerRoot!].Dirty;
var newDirtyRects = scene.Layers[newLayerRoot].Dirty;
existingDirtyRects.Coalesce();
foreach (var r in existingDirtyRects)
{
newDirtyRects.Add(r);
}
var oldLayer = scene.Layers[oldLayerRoot!];
PropagateLayer(node, scene.Layers[newLayerRoot], oldLayer);
scene.Layers.Remove(oldLayer);
}
private static void MakeLayer(Scene scene, VisualNode node)
{
var oldLayerRoot = node.LayerRoot!;
var layer = scene.Layers.Add(node.Visual);
var oldLayer = scene.Layers[oldLayerRoot!];
UpdateLayer(node, layer);
PropagateLayer(node, layer, scene.Layers[oldLayerRoot]);
}
private static void UpdateLayer(VisualNode node, SceneLayer layer)
{
layer.Opacity = node.Visual.Opacity;
if (node.Visual.OpacityMask != null)
{
layer.OpacityMask = node.Visual.OpacityMask?.ToImmutable();
layer.OpacityMaskRect = node.ClipBounds;
}
else
{
layer.OpacityMask = null;
layer.OpacityMaskRect = default;
}
layer.GeometryClip = node.HasAncestorGeometryClip ?
CreateLayerGeometryClip(node) :
null;
}
private static void PropagateLayer(VisualNode node, SceneLayer layer, SceneLayer oldLayer)
{
node.LayerRoot = layer.LayerRoot;
layer.Dirty.Add(node.Bounds);
oldLayer.Dirty.Add(node.Bounds);
foreach (VisualNode child in node.Children)
{
// If the child is not the start of a new layer, recurse.
if (child.LayerRoot != child.Visual)
{
PropagateLayer(child, layer, oldLayer);
}
}
}
// HACK: Disabled layers because they're broken in current renderer. See #2244.
private static bool ShouldStartLayer(Visual visual) => false;
private static IGeometryImpl? CreateLayerGeometryClip(VisualNode node)
{
IGeometryImpl? result = null;
VisualNode? n = node;
for (;;)
{
n = (VisualNode?)n!.Parent;
if (n == null || (n.GeometryClip == null && !n.HasAncestorGeometryClip))
{
break;
}
if (n?.GeometryClip != null)
{
var transformed = n.GeometryClip.WithTransform(n.Transform);
result = result == null ? transformed : result.Intersect(transformed);
}
}
return result;
}
}
}

73
src/Avalonia.Base/Rendering/SceneGraph/SceneLayer.cs

@ -1,73 +0,0 @@
using Avalonia.Media;
using Avalonia.Platform;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// Represents a layer in a <see cref="Scene"/>.
/// </summary>
public class SceneLayer
{
/// <summary>
/// Initializes a new instance of the <see cref="SceneLayer"/> class.
/// </summary>
/// <param name="layerRoot">The visual at the root of the layer.</param>
/// <param name="distanceFromRoot">The distance from the scene root.</param>
public SceneLayer(Visual layerRoot, int distanceFromRoot)
{
LayerRoot = layerRoot;
Dirty = new DirtyRects();
DistanceFromRoot = distanceFromRoot;
}
/// <summary>
/// Clones the layer.
/// </summary>
/// <returns>The cloned layer.</returns>
public SceneLayer Clone()
{
return new SceneLayer(LayerRoot, DistanceFromRoot)
{
Opacity = Opacity,
OpacityMask = OpacityMask,
OpacityMaskRect = OpacityMaskRect,
GeometryClip = GeometryClip,
};
}
/// <summary>
/// Gets the visual at the root of the layer.
/// </summary>
public Visual LayerRoot { get; }
/// <summary>
/// Gets the distance of the layer root from the root of the scene.
/// </summary>
public int DistanceFromRoot { get; }
/// <summary>
/// Gets or sets the opacity of the layer.
/// </summary>
public double Opacity { get; set; } = 1;
/// <summary>
/// Gets or sets the opacity mask for the layer.
/// </summary>
public IBrush? OpacityMask { get; set; }
/// <summary>
/// Gets or sets the target rectangle for the layer opacity mask.
/// </summary>
public Rect OpacityMaskRect { get; set; }
/// <summary>
/// Gets the layer's geometry clip.
/// </summary>
public IGeometryImpl? GeometryClip { get; set; }
/// <summary>
/// Gets the dirty rectangles for the layer.
/// </summary>
internal DirtyRects Dirty { get; }
}
}

206
src/Avalonia.Base/Rendering/SceneGraph/SceneLayers.cs

@ -1,206 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// Holds a collection of layers for a <see cref="Scene"/>.
/// </summary>
public class SceneLayers : IEnumerable<SceneLayer>
{
private readonly Visual _root;
private readonly List<SceneLayer> _inner;
private readonly Dictionary<Visual, SceneLayer> _index;
/// <summary>
/// Initializes a new instance of the <see cref="SceneLayers"/> class.
/// </summary>
/// <param name="root">The scene's root visual.</param>
public SceneLayers(Visual root) : this(root, 0)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SceneLayers"/> class.
/// </summary>
/// <param name="root">The scene's root visual.</param>
/// <param name="capacity">Initial layer capacity.</param>
public SceneLayers(Visual root, int capacity)
{
_root = root;
_inner = new List<SceneLayer>(capacity);
_index = new Dictionary<Visual, SceneLayer>(capacity);
}
/// <summary>
/// Gets the number of layers in the scene.
/// </summary>
public int Count => _inner.Count;
/// <summary>
/// Gets a value indicating whether any of the layers have a dirty region.
/// </summary>
public bool HasDirty
{
get
{
foreach (var layer in _inner)
{
if (!layer.Dirty.IsEmpty)
{
return true;
}
}
return false;
}
}
/// <summary>
/// Gets a layer by index.
/// </summary>
/// <param name="index">The index of the layer.</param>
/// <returns>The layer.</returns>
public SceneLayer this[int index] => _inner[index];
/// <summary>
/// Gets a layer by its root visual.
/// </summary>
/// <param name="visual">The layer's root visual.</param>
/// <returns>The layer.</returns>
public SceneLayer this[Visual visual] => _index[visual];
/// <summary>
/// Adds a layer to the scene.
/// </summary>
/// <param name="layerRoot">The root visual of the layer.</param>
/// <returns>The created layer.</returns>
public SceneLayer Add(Visual layerRoot)
{
_ = layerRoot ?? throw new ArgumentNullException(nameof(layerRoot));
var distance = layerRoot.CalculateDistanceFromAncestor(_root);
var layer = new SceneLayer(layerRoot, distance);
var insert = FindInsertIndex(layer);
_index.Add(layerRoot, layer);
_inner.Insert(insert, layer);
return layer;
}
/// <summary>
/// Makes a deep clone of the layers.
/// </summary>
/// <returns>The cloned layers.</returns>
public SceneLayers Clone()
{
var result = new SceneLayers(_root, Count);
foreach (var src in _inner)
{
var dest = src.Clone();
result._index.Add(dest.LayerRoot, dest);
result._inner.Add(dest);
}
return result;
}
/// <summary>
/// Tests whether a layer exists with the specified root visual.
/// </summary>
/// <param name="layerRoot">The root visual.</param>
/// <returns>
/// True if a layer exists with the specified root visual, otherwise false.
/// </returns>
public bool Exists(Visual layerRoot)
{
_ = layerRoot ?? throw new ArgumentNullException(nameof(layerRoot));
return _index.ContainsKey(layerRoot);
}
/// <summary>
/// Tries to find a layer with the specified root visual.
/// </summary>
/// <param name="layerRoot">The root visual.</param>
/// <returns>The layer if found, otherwise null.</returns>
public SceneLayer? Find(Visual layerRoot)
{
_index.TryGetValue(layerRoot, out var result);
return result;
}
/// <summary>
/// Gets an existing layer or creates a new one if no existing layer is found.
/// </summary>
/// <param name="layerRoot">The root visual.</param>
/// <returns>The layer.</returns>
public SceneLayer GetOrAdd(Visual layerRoot)
{
_ = layerRoot ?? throw new ArgumentNullException(nameof(layerRoot));
if (!_index.TryGetValue(layerRoot, out var result))
{
result = Add(layerRoot);
}
return result;
}
/// <summary>
/// Removes a layer from the scene.
/// </summary>
/// <param name="layerRoot">The root visual.</param>
/// <returns>True if a matching layer was removed, otherwise false.</returns>
public bool Remove(Visual layerRoot)
{
_ = layerRoot ?? throw new ArgumentNullException(nameof(layerRoot));
if (_index.TryGetValue(layerRoot, out var layer))
{
Remove(layer);
}
return layer != null;
}
/// <summary>
/// Removes a layer from the scene.
/// </summary>
/// <param name="layer">The layer.</param>
/// <returns>True if the layer was part of the scene, otherwise false.</returns>
public bool Remove(SceneLayer layer)
{
_ = layer ?? throw new ArgumentNullException(nameof(layer));
_index.Remove(layer.LayerRoot);
return _inner.Remove(layer);
}
/// <inheritdoc/>
public IEnumerator<SceneLayer> GetEnumerator() => _inner.GetEnumerator();
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
private int FindInsertIndex(SceneLayer insert)
{
var index = 0;
foreach (var layer in _inner)
{
if (layer.DistanceFromRoot > insert.DistanceFromRoot)
{
break;
}
++index;
}
return index;
}
}
}

448
src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs

@ -1,448 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Reactive;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Utilities;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// A node in the low-level scene graph representing an <see cref="Visual"/>.
/// </summary>
internal class VisualNode : IVisualNode
{
private static readonly IReadOnlyList<IVisualNode> EmptyChildren = Array.Empty<IVisualNode>();
private static readonly IReadOnlyList<IRef<IDrawOperation>> EmptyDrawOperations = Array.Empty<IRef<IDrawOperation>>();
private Rect? _bounds;
private double _opacity;
private List<IVisualNode>? _children;
private List<IRef<IDrawOperation>>? _drawOperations;
private IRef<IDisposable>? _drawOperationsRefCounter;
private bool _drawOperationsCloned;
private Matrix transformRestore;
/// <summary>
/// Initializes a new instance of the <see cref="VisualNode"/> class.
/// </summary>
/// <param name="visual">The visual that this node represents.</param>
/// <param name="parent">The parent scene graph node, if any.</param>
public VisualNode(Visual visual, IVisualNode? parent)
{
Visual = visual ?? throw new ArgumentNullException(nameof(visual));
Parent = parent;
HasAncestorGeometryClip = parent != null &&
(parent.HasAncestorGeometryClip || parent.GeometryClip != null);
}
/// <inheritdoc/>
public Visual Visual { get; }
/// <inheritdoc/>
public IVisualNode? Parent { get; }
/// <inheritdoc/>
public CornerRadius ClipToBoundsRadius { get; set; }
/// <inheritdoc/>
public Matrix Transform { get; set; }
/// <inheritdoc/>
public Rect Bounds => _bounds ?? CalculateBounds();
/// <inheritdoc/>
public Rect ClipBounds { get; set; }
/// <inheritdoc/>
public Rect LayoutBounds { get; set; }
/// <inheritdoc/>
public bool ClipToBounds { get; set; }
/// <inheritdoc/>
public IGeometryImpl? GeometryClip { get; set; }
/// <inheritdoc/>
public bool HasAncestorGeometryClip { get; }
/// <inheritdoc/>
public double Opacity
{
get { return _opacity; }
set
{
if (_opacity != value)
{
_opacity = value;
OpacityChanged = true;
}
}
}
/// <summary>
/// Gets or sets the opacity mask for the scene graph node.
/// </summary>
public IBrush? OpacityMask { get; set; }
/// <summary>
/// Gets a value indicating whether this node in the scene graph has already
/// been updated in the current update pass.
/// </summary>
public bool SubTreeUpdated { get; set; }
/// <summary>
/// Gets a value indicating whether the <see cref="Opacity"/> property has changed.
/// </summary>
public bool OpacityChanged { get; private set; }
public Visual? LayerRoot { get; set; }
/// <inheritdoc/>
public IReadOnlyList<IVisualNode> Children => _children ?? EmptyChildren;
/// <inheritdoc/>
public IReadOnlyList<IRef<IDrawOperation>> DrawOperations => _drawOperations ?? EmptyDrawOperations;
/// <summary>
/// Adds a child to the <see cref="Children"/> collection.
/// </summary>
/// <param name="child">The child to add.</param>
public void AddChild(IVisualNode child)
{
if (child.Disposed)
{
throw new ObjectDisposedException("Visual node for {node.Visual}");
}
if (child.Parent != this)
{
throw new AvaloniaInternalException("VisualNode added to wrong parent.");
}
EnsureChildrenCreated();
_children.Add(child);
}
/// <summary>
/// Adds an operation to the <see cref="DrawOperations"/> collection.
/// </summary>
/// <param name="operation">The operation to add.</param>
public void AddDrawOperation(IRef<IDrawOperation> operation)
{
EnsureDrawOperationsCreated();
_drawOperations.Add(operation.Clone());
}
/// <summary>
/// Removes a child from the <see cref="Children"/> collection.
/// </summary>
/// <param name="child">The child to remove.</param>
public void RemoveChild(IVisualNode child)
{
EnsureChildrenCreated();
_children.Remove(child);
}
/// <summary>
/// Replaces a child in the <see cref="Children"/> collection.
/// </summary>
/// <param name="index">The child to be replaced.</param>
/// <param name="node">The child to add.</param>
public void ReplaceChild(int index, IVisualNode node)
{
if (node.Disposed)
{
throw new ObjectDisposedException("Visual node for {node.Visual}");
}
if (node.Parent != this)
{
throw new AvaloniaInternalException("VisualNode added to wrong parent.");
}
EnsureChildrenCreated();
_children[index] = node;
}
/// <summary>
/// Replaces an item in the <see cref="DrawOperations"/> collection.
/// </summary>
/// <param name="index">The operation to be replaced.</param>
/// <param name="operation">The operation to add.</param>
public void ReplaceDrawOperation(int index, IRef<IDrawOperation> operation)
{
EnsureDrawOperationsCreated();
var old = _drawOperations[index];
_drawOperations[index] = operation.Clone();
old.Dispose();
}
/// <summary>
/// Sorts the <see cref="Children"/> collection according to the order of the visual's
/// children and their z-index.
/// </summary>
/// <param name="scene">The scene that the node is a part of.</param>
public void SortChildren(Scene scene)
{
if (_children == null || _children.Count <= 1)
{
return;
}
var keys = new List<long>(Visual.VisualChildren.Count);
for (var i = 0; i < Visual.VisualChildren.Count; ++i)
{
var child = Visual.VisualChildren[i];
var zIndex = child.ZIndex;
keys.Add(((long)zIndex << 32) + i);
}
keys.Sort();
_children.Clear();
foreach (var i in keys)
{
var child = Visual.VisualChildren[(int)(i & 0xffffffff)];
var node = scene.FindNode(child);
if (node != null)
{
_children.Add(node);
}
}
}
/// <summary>
/// Removes items in the <see cref="Children"/> collection from the specified index
/// to the end.
/// </summary>
/// <param name="first">The index of the first child to be removed.</param>
public void TrimChildren(int first)
{
if (first < _children?.Count)
{
EnsureChildrenCreated();
for (int i = first; i < _children.Count; i++)
{
_children[i].Dispose();
}
_children.RemoveRange(first, _children.Count - first);
}
}
/// <summary>
/// Removes items in the <see cref="DrawOperations"/> collection from the specified index
/// to the end.
/// </summary>
/// <param name="first">The index of the first operation to be removed.</param>
public void TrimDrawOperations(int first)
{
if (first < _drawOperations?.Count)
{
EnsureDrawOperationsCreated();
for (int i = first; i < _drawOperations.Count; i++)
{
_drawOperations[i].Dispose();
}
_drawOperations.RemoveRange(first, _drawOperations.Count - first);
}
}
/// <summary>
/// Makes a copy of the node
/// </summary>
/// <param name="parent">The new parent node.</param>
/// <returns>A cloned node.</returns>
public VisualNode Clone(IVisualNode? parent)
{
return new VisualNode(Visual, parent)
{
Transform = Transform,
ClipBounds = ClipBounds,
ClipToBoundsRadius = ClipToBoundsRadius,
ClipToBounds = ClipToBounds,
LayoutBounds = LayoutBounds,
GeometryClip = GeometryClip,
_opacity = Opacity,
OpacityMask = OpacityMask,
_drawOperations = _drawOperations,
_drawOperationsRefCounter = _drawOperationsRefCounter?.Clone(),
_drawOperationsCloned = true,
LayerRoot= LayerRoot,
};
}
/// <inheritdoc/>
public bool HitTest(Point p)
{
var drawOperations = DrawOperations;
var drawOperationsCount = drawOperations.Count;
for (var i = 0; i < drawOperationsCount; i++)
{
var operation = drawOperations[i];
if (operation?.Item?.HitTest(p) == true)
{
return true;
}
}
return false;
}
/// <inheritdoc/>
public void BeginRender(IDrawingContextImpl context, bool skipOpacity)
{
transformRestore = context.Transform;
if (ClipToBounds)
{
context.Transform = Matrix.Identity;
if (ClipToBoundsRadius.IsDefault)
context.PushClip(ClipBounds);
else
context.PushClip(new RoundedRect(ClipBounds, ClipToBoundsRadius));
}
context.Transform = Transform;
if (Opacity != 1 && !skipOpacity)
{
context.PushOpacity(Opacity);
}
if (GeometryClip != null)
{
context.PushGeometryClip(GeometryClip);
}
if (OpacityMask != null)
{
context.PushOpacityMask(OpacityMask, LayoutBounds);
}
}
/// <inheritdoc/>
public void EndRender(IDrawingContextImpl context, bool skipOpacity)
{
if (OpacityMask != null)
{
context.PopOpacityMask();
}
if (GeometryClip != null)
{
context.PopGeometryClip();
}
if (Opacity != 1 && !skipOpacity)
{
context.PopOpacity();
}
if (ClipToBounds)
{
context.Transform = Matrix.Identity;
context.PopClip();
}
context.Transform = transformRestore;
}
internal void TryPreallocateChildren(int count)
{
if (count == 0)
{
return;
}
EnsureChildrenCreated(count);
}
private Rect CalculateBounds()
{
var result = new Rect();
if (_drawOperations != null)
{
foreach (var operation in _drawOperations)
{
result = result.Union(operation.Item.Bounds);
}
}
_bounds = result;
return result;
}
[MemberNotNull(nameof(_children))]
private void EnsureChildrenCreated(int capacity = 0)
{
if (_children == null)
{
_children = new List<IVisualNode>(capacity);
}
}
/// <summary>
/// Ensures that this node draw operations have been created and are mutable (in case we are using cloned operations).
/// </summary>
[MemberNotNull(nameof(_drawOperations))]
private void EnsureDrawOperationsCreated()
{
if (_drawOperations == null)
{
_drawOperations = new List<IRef<IDrawOperation>>();
_drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations));
_drawOperationsCloned = false;
}
else if (_drawOperationsCloned)
{
var oldDrawOperations = _drawOperations;
_drawOperations = new List<IRef<IDrawOperation>>(oldDrawOperations.Count);
foreach (var drawOperation in oldDrawOperations)
{
_drawOperations.Add(drawOperation.Clone());
}
_drawOperationsRefCounter?.Dispose();
_drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations));
_drawOperationsCloned = false;
}
}
/// <summary>
/// Creates disposable that will dispose all items in passed draw operations after being disposed.
/// It is crucial that we don't capture current <see cref="VisualNode"/> instance
/// as draw operations can be cloned and may persist across subsequent scenes.
/// </summary>
/// <param name="drawOperations">Draw operations that need to be disposed.</param>
/// <returns>Disposable for given draw operations.</returns>
private static IDisposable CreateDisposeDrawOperations(List<IRef<IDrawOperation>> drawOperations)
{
return Disposable.Create(drawOperations, operations =>
{
foreach (var operation in operations)
{
operation.Dispose();
}
});
}
public bool Disposed { get; private set; }
public void Dispose()
{
_drawOperationsRefCounter?.Dispose();
Disposed = true;
}
}
}

2
src/Avalonia.Base/Rendering/SceneInvalidatedEventArgs.cs

@ -12,7 +12,7 @@ namespace Avalonia.Rendering
/// </summary> /// </summary>
/// <param name="root">The render root that has been updated.</param> /// <param name="root">The render root that has been updated.</param>
/// <param name="dirtyRect">The updated area.</param> /// <param name="dirtyRect">The updated area.</param>
internal SceneInvalidatedEventArgs( public SceneInvalidatedEventArgs(
IRenderRoot root, IRenderRoot root,
Rect dirtyRect) Rect dirtyRect)
{ {

18
src/Avalonia.Base/Visual.cs

@ -36,12 +36,7 @@ namespace Avalonia
/// </summary> /// </summary>
public static readonly DirectProperty<Visual, Rect> BoundsProperty = public static readonly DirectProperty<Visual, Rect> BoundsProperty =
AvaloniaProperty.RegisterDirect<Visual, Rect>(nameof(Bounds), o => o.Bounds); AvaloniaProperty.RegisterDirect<Visual, Rect>(nameof(Bounds), o => o.Bounds);
public static readonly DirectProperty<Visual, TransformedBounds?> TransformedBoundsProperty =
AvaloniaProperty.RegisterDirect<Visual, TransformedBounds?>(
nameof(TransformedBounds),
o => o.TransformedBounds);
/// <summary> /// <summary>
/// Defines the <see cref="ClipToBounds"/> property. /// Defines the <see cref="ClipToBounds"/> property.
/// </summary> /// </summary>
@ -116,7 +111,6 @@ namespace Avalonia
(s, h) => s.Invalidated -= h); (s, h) => s.Invalidated -= h);
private Rect _bounds; private Rect _bounds;
private TransformedBounds? _transformedBounds;
private IRenderRoot? _visualRoot; private IRenderRoot? _visualRoot;
private Visual? _visualParent; private Visual? _visualParent;
private bool _hasMirrorTransform; private bool _hasMirrorTransform;
@ -172,11 +166,6 @@ namespace Avalonia
protected set { SetAndRaise(BoundsProperty, ref _bounds, value); } protected set { SetAndRaise(BoundsProperty, ref _bounds, value); }
} }
/// <summary>
/// Gets the bounds of the control relative to the window, accounting for rendering transforms.
/// </summary>
public TransformedBounds? TransformedBounds => _transformedBounds;
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the control should be clipped to its bounds. /// Gets or sets a value indicating whether the control should be clipped to its bounds.
/// </summary> /// </summary>
@ -523,11 +512,6 @@ namespace Avalonia
return CompositionVisual; return CompositionVisual;
} }
internal void SetTransformedBounds(TransformedBounds? value)
{
SetAndRaise(TransformedBoundsProperty, ref _transformedBounds, value);
}
/// <summary> /// <summary>
/// Calls the <see cref="OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs)"/> method /// Calls the <see cref="OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs)"/> method
/// for this control and all of its visual descendants. /// for this control and all of its visual descendants.

3
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorPicker.xaml

@ -245,11 +245,12 @@
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</Border> </Border>
<!-- Color updated in code-behind --> <!-- Color updated in code-behind -->
<!-- Max length must include an optional '#' prefix (#FFFFFFFF) -->
<TextBox x:Name="PART_HexTextBox" <TextBox x:Name="PART_HexTextBox"
Grid.Column="1" Grid.Column="1"
AutomationProperties.Name="Hexadecimal Color" AutomationProperties.Name="Hexadecimal Color"
Height="32" Height="32"
MaxLength="8" MaxLength="9"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
CornerRadius="0,4,4,0" /> CornerRadius="0,4,4,0" />
</Grid> </Grid>

3
src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml

@ -494,11 +494,12 @@
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</Border> </Border>
<!-- Color updated in code-behind --> <!-- Color updated in code-behind -->
<!-- Max length must include an optional '#' prefix (#FFFFFFFF) -->
<TextBox x:Name="PART_HexTextBox" <TextBox x:Name="PART_HexTextBox"
Grid.Column="1" Grid.Column="1"
AutomationProperties.Name="Hexadecimal Color" AutomationProperties.Name="Hexadecimal Color"
Height="32" Height="32"
MaxLength="8" MaxLength="9"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
CornerRadius="0,4,4,0" /> CornerRadius="0,4,4,0" />
</Grid> </Grid>

3
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorPicker.xaml

@ -245,11 +245,12 @@
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</Border> </Border>
<!-- Color updated in code-behind --> <!-- Color updated in code-behind -->
<!-- Max length must include an optional '#' prefix (#FFFFFFFF) -->
<TextBox x:Name="PART_HexTextBox" <TextBox x:Name="PART_HexTextBox"
Grid.Column="1" Grid.Column="1"
AutomationProperties.Name="Hexadecimal Color" AutomationProperties.Name="Hexadecimal Color"
Height="32" Height="32"
MaxLength="8" MaxLength="9"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
CornerRadius="0,0,0,0" /> CornerRadius="0,0,0,0" />
</Grid> </Grid>

3
src/Avalonia.Controls.ColorPicker/Themes/Simple/ColorView.xaml

@ -456,11 +456,12 @@
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</Border> </Border>
<!-- Color updated in code-behind --> <!-- Color updated in code-behind -->
<!-- Max length must include an optional '#' prefix (#FFFFFFFF) -->
<TextBox x:Name="PART_HexTextBox" <TextBox x:Name="PART_HexTextBox"
Grid.Column="1" Grid.Column="1"
AutomationProperties.Name="Hexadecimal Color" AutomationProperties.Name="Hexadecimal Color"
Height="32" Height="32"
MaxLength="8" MaxLength="9"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
CornerRadius="0,0,0,0" /> CornerRadius="0,0,0,0" />
</Grid> </Grid>

2
src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs

@ -7,6 +7,7 @@ using Avalonia.Data;
using System; using System;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
using Avalonia.Markup.Xaml.MarkupExtensions; using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Metadata;
using Avalonia.Reactive; using Avalonia.Reactive;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -24,6 +25,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
//TODO Binding //TODO Binding
[AssignBinding] [AssignBinding]
[InheritDataTypeFromItems(nameof(DataGrid.Items), AncestorType = typeof(DataGrid))]
public virtual IBinding Binding public virtual IBinding Binding
{ {
get get

2
src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs

@ -24,6 +24,7 @@ namespace Avalonia.Controls
(o, v) => o.CellTemplate = v); (o, v) => o.CellTemplate = v);
[Content] [Content]
[InheritDataTypeFromItems(nameof(DataGrid.Items), AncestorType = typeof(DataGrid))]
public IDataTemplate CellTemplate public IDataTemplate CellTemplate
{ {
get { return _cellTemplate; } get { return _cellTemplate; }
@ -50,6 +51,7 @@ namespace Avalonia.Controls
/// <remarks> /// <remarks>
/// If this property is <see langword="null"/> the column is read-only. /// If this property is <see langword="null"/> the column is read-only.
/// </remarks> /// </remarks>
[InheritDataTypeFromItems(nameof(DataGrid.Items), AncestorType = typeof(DataGrid))]
public IDataTemplate CellEditingTemplate public IDataTemplate CellEditingTemplate
{ {
get => _cellEditingCellTemplate; get => _cellEditingCellTemplate;

4
src/Avalonia.Controls/Button.cs

@ -394,10 +394,10 @@ namespace Avalonia.Controls
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{ {
IsPressed = true; IsPressed = true;
e.Handled = true;
if (ClickMode == ClickMode.Press) if (ClickMode == ClickMode.Press)
{ {
e.Handled = true;
OnClick(); OnClick();
} }
} }
@ -411,11 +411,11 @@ namespace Avalonia.Controls
if (IsPressed && e.InitialPressMouseButton == MouseButton.Left) if (IsPressed && e.InitialPressMouseButton == MouseButton.Left)
{ {
IsPressed = false; IsPressed = false;
e.Handled = true;
if (ClickMode == ClickMode.Release && if (ClickMode == ClickMode.Release &&
this.GetVisualsAt(e.GetPosition(this)).Any(c => this == c || this.IsVisualAncestorOf(c))) this.GetVisualsAt(e.GetPosition(this)).Any(c => this == c || this.IsVisualAncestorOf(c)))
{ {
e.Handled = true;
OnClick(); OnClick();
} }
} }

20
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs

@ -1,10 +1,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
namespace Avalonia.Controls.Embedding.Offscreen namespace Avalonia.Controls.Embedding.Offscreen
{ {
@ -13,7 +16,7 @@ namespace Avalonia.Controls.Embedding.Offscreen
{ {
private double _scaling = 1; private double _scaling = 1;
private Size _clientSize; private Size _clientSize;
private PlatformRenderInterfaceContextManager _renderInterface = new(null); private ManualRenderTimer _manualRenderTimer = new();
public IInputRoot? InputRoot { get; private set; } public IInputRoot? InputRoot { get; private set; }
public bool IsDisposed { get; private set; } public bool IsDisposed { get; private set; }
@ -23,10 +26,19 @@ namespace Avalonia.Controls.Embedding.Offscreen
IsDisposed = true; IsDisposed = true;
} }
class ManualRenderTimer : IRenderTimer
{
static Stopwatch St = Stopwatch.StartNew();
public event Action<TimeSpan>? Tick;
public bool RunsInBackground => false;
public void TriggerTick() => Tick?.Invoke(St.Elapsed);
}
public IRenderer CreateRenderer(IRenderRoot root) => public IRenderer CreateRenderer(IRenderRoot root) =>
new ImmediateRenderer((Visual)root, () => _renderInterface.CreateRenderTarget(Surfaces), _renderInterface); new CompositingRenderer(root, new Compositor(new RenderLoop(_manualRenderTimer, Dispatcher.UIThread), null),
() => Surfaces);
public abstract void Invalidate(Rect rect);
public abstract IEnumerable<object> Surfaces { get; } public abstract IEnumerable<object> Surfaces { get; }
public Size ClientSize public Size ClientSize
@ -82,5 +94,7 @@ namespace Avalonia.Controls.Embedding.Offscreen
public WindowTransparencyLevel TransparencyLevel { get; private set; } public WindowTransparencyLevel TransparencyLevel { get; private set; }
public IPopupImpl? CreatePopup() => null; public IPopupImpl? CreatePopup() => null;
public virtual object? TryGetFeature(Type featureType) => null;
} }
} }

130
src/Avalonia.Controls/Expander.cs

@ -59,12 +59,11 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Defines the <see cref="IsExpanded"/> property. /// Defines the <see cref="IsExpanded"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<Expander, bool> IsExpandedProperty = public static readonly StyledProperty<bool> IsExpandedProperty =
AvaloniaProperty.RegisterDirect<Expander, bool>( AvaloniaProperty.Register<Expander, bool>(
nameof(IsExpanded), nameof(IsExpanded),
o => o.IsExpanded, defaultBindingMode: BindingMode.TwoWay,
(o, v) => o.IsExpanded = v, coerce: CoerceIsExpanded);
defaultBindingMode: Data.BindingMode.TwoWay);
/// <summary> /// <summary>
/// Defines the <see cref="Collapsed"/> event. /// Defines the <see cref="Collapsed"/> event.
@ -77,8 +76,8 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Defines the <see cref="Collapsing"/> event. /// Defines the <see cref="Collapsing"/> event.
/// </summary> /// </summary>
public static readonly RoutedEvent<RoutedEventArgs> CollapsingEvent = public static readonly RoutedEvent<CancelRoutedEventArgs> CollapsingEvent =
RoutedEvent.Register<Expander, RoutedEventArgs>( RoutedEvent.Register<Expander, CancelRoutedEventArgs>(
nameof(Collapsing), nameof(Collapsing),
RoutingStrategies.Bubble); RoutingStrategies.Bubble);
@ -93,13 +92,12 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Defines the <see cref="Expanding"/> event. /// Defines the <see cref="Expanding"/> event.
/// </summary> /// </summary>
public static readonly RoutedEvent<RoutedEventArgs> ExpandingEvent = public static readonly RoutedEvent<CancelRoutedEventArgs> ExpandingEvent =
RoutedEvent.Register<Expander, RoutedEventArgs>( RoutedEvent.Register<Expander, CancelRoutedEventArgs>(
nameof(Expanding), nameof(Expanding),
RoutingStrategies.Bubble); RoutingStrategies.Bubble);
private bool _ignorePropertyChanged = false; private bool _ignorePropertyChanged = false;
private bool _isExpanded;
private CancellationTokenSource? _lastTransitionCts; private CancellationTokenSource? _lastTransitionCts;
/// <summary> /// <summary>
@ -134,50 +132,8 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public bool IsExpanded public bool IsExpanded
{ {
get => _isExpanded; get => GetValue(IsExpandedProperty);
set set => SetValue(IsExpandedProperty, value);
{
// It is important here that IsExpanded is a direct property so events can be invoked
// BEFORE the property system gets notified of updated values. This is because events
// may be canceled by external code.
if (_isExpanded != value)
{
RoutedEventArgs eventArgs;
if (value)
{
eventArgs = new RoutedEventArgs(ExpandingEvent, this);
OnExpanding(eventArgs);
}
else
{
eventArgs = new RoutedEventArgs(CollapsingEvent, this);
OnCollapsing(eventArgs);
}
if (eventArgs.Handled)
{
// If the event was externally handled (canceled) we must still notify the value has changed.
// This property changed notification will update any external code observing this property that itself may have set the new value.
// We are essentially reverted any external state change along with ignoring the IsExpanded property set.
// Remember IsExpanded is usually controlled by a ToggleButton in the control theme.
_ignorePropertyChanged = true;
RaisePropertyChanged(
IsExpandedProperty,
oldValue: value,
newValue: _isExpanded,
BindingPriority.LocalValue,
isEffectiveValue: true);
_ignorePropertyChanged = false;
}
else
{
SetAndRaise(IsExpandedProperty, ref _isExpanded, value);
}
}
}
} }
/// <summary> /// <summary>
@ -193,10 +149,10 @@ namespace Avalonia.Controls
/// Occurs as the content area is closing. /// Occurs as the content area is closing.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// The event args <see cref="RoutedEventArgs.Handled"/> property may be set to true to cancel the event /// The event args <see cref="CancelRoutedEventArgs.Cancel"/> property may be set to true to cancel the event
/// and keep the control open (expanded). /// and keep the control open (expanded).
/// </remarks> /// </remarks>
public event EventHandler<RoutedEventArgs>? Collapsing public event EventHandler<CancelRoutedEventArgs>? Collapsing
{ {
add => AddHandler(CollapsingEvent, value); add => AddHandler(CollapsingEvent, value);
remove => RemoveHandler(CollapsingEvent, value); remove => RemoveHandler(CollapsingEvent, value);
@ -215,10 +171,10 @@ namespace Avalonia.Controls
/// Occurs as the content area is opening. /// Occurs as the content area is opening.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// The event args <see cref="RoutedEventArgs.Handled"/> property may be set to true to cancel the event /// The event args <see cref="CancelRoutedEventArgs.Cancel"/> property may be set to true to cancel the event
/// and keep the control closed (collapsed). /// and keep the control closed (collapsed).
/// </remarks> /// </remarks>
public event EventHandler<RoutedEventArgs>? Expanding public event EventHandler<CancelRoutedEventArgs>? Expanding
{ {
add => AddHandler(ExpandingEvent, value); add => AddHandler(ExpandingEvent, value);
remove => RemoveHandler(ExpandingEvent, value); remove => RemoveHandler(ExpandingEvent, value);
@ -332,5 +288,63 @@ namespace Avalonia.Controls
PseudoClasses.Set(":expanded", IsExpanded); PseudoClasses.Set(":expanded", IsExpanded);
} }
/// <summary>
/// Called when the <see cref="IsExpanded"/> property has to be coerced.
/// </summary>
/// <param name="value">The value to coerce.</param>
protected virtual bool OnCoerceIsExpanded(bool value)
{
CancelRoutedEventArgs eventArgs;
if (value)
{
eventArgs = new CancelRoutedEventArgs(ExpandingEvent, this);
OnExpanding(eventArgs);
}
else
{
eventArgs = new CancelRoutedEventArgs(CollapsingEvent, this);
OnCollapsing(eventArgs);
}
if (eventArgs.Cancel)
{
// If the event was externally canceled we must still notify the value has changed.
// This property changed notification will update any external code observing this property that itself may have set the new value.
// We are essentially reverted any external state change along with ignoring the IsExpanded property set.
// Remember IsExpanded is usually controlled by a ToggleButton in the control theme and is also used for animations.
_ignorePropertyChanged = true;
RaisePropertyChanged(
IsExpandedProperty,
oldValue: value,
newValue: !value,
BindingPriority.LocalValue,
isEffectiveValue: true);
_ignorePropertyChanged = false;
return !value;
}
return value;
}
/// <summary>
/// Coerces/validates the <see cref="IsExpanded"/> property value.
/// </summary>
/// <param name="instance">The <see cref="Expander"/> instance.</param>
/// <param name="value">The value to coerce.</param>
/// <returns>The coerced/validated value.</returns>
private static bool CoerceIsExpanded(AvaloniaObject instance, bool value)
{
if (instance is Expander expander)
{
return expander.OnCoerceIsExpanded(value);
}
return value;
}
} }
} }

19
src/Avalonia.Controls/Flyouts/MenuFlyout.cs

@ -3,6 +3,7 @@ using Avalonia.Collections;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Styling;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -27,6 +28,12 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect<MenuFlyout, IDataTemplate?>(nameof(ItemTemplate), AvaloniaProperty.RegisterDirect<MenuFlyout, IDataTemplate?>(nameof(ItemTemplate),
x => x.ItemTemplate, (x, v) => x.ItemTemplate = v); x => x.ItemTemplate, (x, v) => x.ItemTemplate = v);
/// <summary>
/// Defines the <see cref="ItemContainerTheme"/> property.
/// </summary>
public static readonly StyledProperty<ControlTheme?> ItemContainerThemeProperty =
ItemsControl.ItemContainerThemeProperty.AddOwner<MenuFlyout>();
public Classes FlyoutPresenterClasses => _classes ??= new Classes(); public Classes FlyoutPresenterClasses => _classes ??= new Classes();
/// <summary> /// <summary>
@ -48,6 +55,15 @@ namespace Avalonia.Controls
set => SetAndRaise(ItemTemplateProperty, ref _itemTemplate, value); set => SetAndRaise(ItemTemplateProperty, ref _itemTemplate, value);
} }
/// <summary>
/// Gets or sets the <see cref="ControlTheme"/> that is applied to the container element generated for each item.
/// </summary>
public ControlTheme? ItemContainerTheme
{
get { return GetValue(ItemContainerThemeProperty); }
set { SetValue(ItemContainerThemeProperty, value); }
}
private Classes? _classes; private Classes? _classes;
private IEnumerable? _items; private IEnumerable? _items;
private IDataTemplate? _itemTemplate; private IDataTemplate? _itemTemplate;
@ -57,7 +73,8 @@ namespace Avalonia.Controls
return new MenuFlyoutPresenter return new MenuFlyoutPresenter
{ {
[!ItemsControl.ItemsProperty] = this[!ItemsProperty], [!ItemsControl.ItemsProperty] = this[!ItemsProperty],
[!ItemsControl.ItemTemplateProperty] = this[!ItemTemplateProperty] [!ItemsControl.ItemTemplateProperty] = this[!ItemTemplateProperty],
[!ItemsControl.ItemContainerThemeProperty] = this[!ItemContainerThemeProperty],
}; };
} }

1
src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs

@ -1,5 +1,4 @@
using System; using System;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;

1
src/Avalonia.Controls/ItemsControl.cs

@ -168,6 +168,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// Gets or sets the data template used to display the items in the control. /// Gets or sets the data template used to display the items in the control.
/// </summary> /// </summary>
[InheritDataTypeFromItems(nameof(Items))]
public IDataTemplate? ItemTemplate public IDataTemplate? ItemTemplate
{ {
get { return GetValue(ItemTemplateProperty); } get { return GetValue(ItemTemplateProperty); }

2
src/Avalonia.Controls/NativeControlHost.cs

@ -58,7 +58,7 @@ namespace Avalonia.Controls
private void UpdateHost() private void UpdateHost()
{ {
_queuedForMoveResize = false; _queuedForMoveResize = false;
_currentHost = (_currentRoot?.PlatformImpl as ITopLevelImplWithNativeControlHost)?.NativeControlHost; _currentHost = _currentRoot?.PlatformImpl?.TryGetFeature<INativeControlHostImpl>();
if (_currentHost != null) if (_currentHost != null)
{ {

3
src/Avalonia.Controls/NativeMenu.Export.cs

@ -1,6 +1,7 @@
using System; using System;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Platform;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -21,7 +22,7 @@ namespace Avalonia.Controls
public NativeMenuInfo(TopLevel target) public NativeMenuInfo(TopLevel target)
{ {
Exporter = (target.PlatformImpl as ITopLevelImplWithNativeMenuExporter)?.NativeMenuExporter; Exporter = target.PlatformImpl?.TryGetFeature<ITopLevelNativeMenuExporter>();
if (Exporter != null) if (Exporter != null)
{ {
Exporter.OnIsNativeMenuExportedChanged += delegate Exporter.OnIsNativeMenuExportedChanged += delegate

4
src/Avalonia.Controls/Notifications/WindowNotificationManager.cs

@ -15,7 +15,7 @@ namespace Avalonia.Controls.Notifications
/// </summary> /// </summary>
[TemplatePart("PART_Items", typeof(Panel))] [TemplatePart("PART_Items", typeof(Panel))]
[PseudoClasses(":topleft", ":topright", ":bottomleft", ":bottomright")] [PseudoClasses(":topleft", ":topright", ":bottomleft", ":bottomright")]
public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager, ICustomSimpleHitTest public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager
{ {
private IList? _items; private IList? _items;
@ -160,7 +160,5 @@ namespace Avalonia.Controls.Notifications
PseudoClasses.Set(":bottomleft", position == NotificationPosition.BottomLeft); PseudoClasses.Set(":bottomleft", position == NotificationPosition.BottomLeft);
PseudoClasses.Set(":bottomright", position == NotificationPosition.BottomRight); PseudoClasses.Set(":bottomright", position == NotificationPosition.BottomRight);
} }
public bool HitTest(Point point) => VisualChildren.HitTestCustom(point);
} }
} }

14
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@ -350,13 +350,19 @@ namespace Avalonia.Controls.Platform
{ {
// HACK: #8179 needs to be addressed to correctly implement it in the PointerPressed method. // HACK: #8179 needs to be addressed to correctly implement it in the PointerPressed method.
var item = GetMenuItem(e.Source as Control) as MenuItem; var item = GetMenuItem(e.Source as Control) as MenuItem;
if (item?.TransformedBounds == null)
{ if (item == null)
return; return;
}
var serverTransform = item?.CompositionVisual?.TryGetServerGlobalTransform();
if (serverTransform == null)
return;
var point = e.GetCurrentPoint(null); var point = e.GetCurrentPoint(null);
var transformedPoint = point.Position.Transform(serverTransform.Value);
if (point.Properties.IsLeftButtonPressed && item.TransformedBounds.Value.Contains(point.Position) == false) if (point.Properties.IsLeftButtonPressed &&
new Rect(item!.Bounds.Size).Contains(transformedPoint) == false)
{ {
e.Pointer.Capture(null); e.Pointer.Capture(null);
} }

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

Loading…
Cancel
Save