Browse Source

Merge branch 'master' into themeresource

pull/8166/head
Max Katz 3 years ago
committed by GitHub
parent
commit
9ad3386547
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      build/Base.props
  2. 3
      build/System.Memory.props
  3. 3
      samples/ControlCatalog.NetCore/Program.cs
  4. 22
      samples/ControlCatalog/Pages/DataGridPage.xaml
  5. 13
      samples/ControlCatalog/Pages/DataGridPage.xaml.cs
  6. 2
      samples/ControlCatalog/Pages/ScrollSnapPage.xaml
  7. 5
      samples/GpuInterop/MainWindow.axaml.cs
  8. 4
      samples/RenderDemo/App.xaml.cs
  9. 14
      samples/RenderDemo/MainWindow.xaml
  10. 23
      samples/RenderDemo/MainWindow.xaml.cs
  11. 45
      samples/RenderDemo/ViewModels/MainWindowViewModel.cs
  12. 17
      src/Android/Avalonia.Android/AndroidPlatform.cs
  13. 4
      src/Android/Avalonia.Android/Platform/AndroidSystemNavigationManager.cs
  14. 57
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  15. 4
      src/Avalonia.Base/Avalonia.Base.csproj
  16. 12
      src/Avalonia.Base/Input/PointerEventArgs.cs
  17. 8
      src/Avalonia.Base/Input/TextInput/TransformTrackingHelper.cs
  18. 20
      src/Avalonia.Base/Layout/LayoutManager.cs
  19. 77
      src/Avalonia.Base/Media/Imaging/Bitmap.cs
  20. 51
      src/Avalonia.Base/Media/Imaging/BitmapMemory.cs
  21. 280
      src/Avalonia.Base/Media/Imaging/PixelFormatReaders.cs
  22. 77
      src/Avalonia.Base/Media/Imaging/WriteableBitmap.cs
  23. 34
      src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs
  24. 2
      src/Avalonia.Base/PixelRect.cs
  25. 1
      src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs
  26. 2
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  27. 7
      src/Avalonia.Base/Platform/IReadableBitmapImpl.cs
  28. 3
      src/Avalonia.Base/Platform/IWriteableBitmapImpl.cs
  29. 71
      src/Avalonia.Base/Platform/PixelFormat.cs
  30. 8
      src/Avalonia.Base/Platform/SystemNavigationManagerImpl.cs
  31. 6
      src/Avalonia.Base/Point.cs
  32. 27
      src/Avalonia.Base/Rect.cs
  33. 46
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  34. 3
      src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
  35. 3
      src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs
  36. 9
      src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
  37. 3
      src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs
  38. 8
      src/Avalonia.Base/Rendering/Composition/Compositor.cs
  39. 78
      src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs
  40. 58
      src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs
  41. 176
      src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs
  42. 134
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  43. 780
      src/Avalonia.Base/Rendering/DeferredRenderer.cs
  44. 16
      src/Avalonia.Base/Rendering/ICustomHitTest.cs
  45. 33
      src/Avalonia.Base/Rendering/ICustomSimpleHitTest.cs
  46. 6
      src/Avalonia.Base/Rendering/IRenderRoot.cs
  47. 11
      src/Avalonia.Base/Rendering/IRenderer.cs
  48. 16
      src/Avalonia.Base/Rendering/IRendererFactory.cs
  49. 248
      src/Avalonia.Base/Rendering/ImmediateRenderer.cs
  50. 11
      src/Avalonia.Base/Rendering/LayoutPassTiming.cs
  51. 4
      src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs
  52. 48
      src/Avalonia.Base/Rendering/RenderLayer.cs
  53. 69
      src/Avalonia.Base/Rendering/RenderLayers.cs
  54. 35
      src/Avalonia.Base/Rendering/RendererDebugOverlays.cs
  55. 57
      src/Avalonia.Base/Rendering/RendererDiagnostics.cs
  56. 482
      src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  57. 24
      src/Avalonia.Base/Rendering/SceneGraph/ISceneBuilder.cs
  58. 105
      src/Avalonia.Base/Rendering/SceneGraph/IVisualNode.cs
  59. 352
      src/Avalonia.Base/Rendering/SceneGraph/Scene.cs
  60. 485
      src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs
  61. 73
      src/Avalonia.Base/Rendering/SceneGraph/SceneLayer.cs
  62. 206
      src/Avalonia.Base/Rendering/SceneGraph/SceneLayers.cs
  63. 448
      src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs
  64. 19
      src/Avalonia.Base/Utilities/StopwatchHelper.cs
  65. 18
      src/Avalonia.Base/Visual.cs
  66. 4
      src/Avalonia.Base/composition-schema.xml
  67. 2
      src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs
  68. 2
      src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs
  69. 4
      src/Avalonia.Controls/Button.cs
  70. 20
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  71. 1
      src/Avalonia.Controls/ItemsControl.cs
  72. 2
      src/Avalonia.Controls/NativeControlHost.cs
  73. 3
      src/Avalonia.Controls/NativeMenu.Export.cs
  74. 4
      src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
  75. 14
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  76. 6
      src/Avalonia.Controls/Platform/INativeControlHostImpl.cs
  77. 7
      src/Avalonia.Controls/Platform/ITopLevelImpl.cs
  78. 11
      src/Avalonia.Controls/Platform/ITopLevelImplWithStorageProvider.cs
  79. 13
      src/Avalonia.Controls/Platform/ITopLevelImplWithTextInputMethod.cs
  80. 6
      src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs
  81. 48
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  82. 10
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  83. 4
      src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs
  84. 4
      src/Avalonia.Controls/Primitives/OverlayLayer.cs
  85. 16
      src/Avalonia.Controls/Primitives/Popup.cs
  86. 3
      src/Avalonia.Controls/Remote/RemoteWidget.cs
  87. 34
      src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs
  88. 2
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  89. 79
      src/Avalonia.Controls/TopLevel.cs
  90. 14
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  91. 15
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  92. 14
      src/Avalonia.Diagnostics/Diagnostics/Conventions.cs
  93. 109
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  94. 4
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs
  95. 32
      src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml.cs
  96. 24
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
  97. 8
      src/Avalonia.Diagnostics/Diagnostics/VisualExtensions.cs
  98. 1
      src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs
  99. 4
      src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  100. 3
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

3
build/Base.props

@ -1,5 +1,6 @@
<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="Microsoft.Bcl.AsyncInterfaces" Version="6.0.0" />
<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">
<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" />
</ItemGroup>
</Project>

3
samples/ControlCatalog.NetCore/Program.cs

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

22
samples/ControlCatalog/Pages/DataGridPage.xaml

@ -1,7 +1,9 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:local="using:ControlCatalog.Models"
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>
<local:GDPValueConverter x:Key="GDPConverter" />
<DataTemplate x:Key="Demo.DataTemplates.CountryHeader" x:DataType="local:Country">
@ -33,7 +35,7 @@
<DataGrid.Columns>
<!-- Using HeaderTemplate -->
<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="Area" Binding="{Binding Area}" Width="3*" x:DataType="local:Country" />
<DataGridTextColumn Header="GDP" Binding="{Binding GDP}" Width="3*"
@ -90,19 +92,21 @@
</TabItem>
<TabItem x:Name="EditableTab" Header="Editable">
<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>
<DataGridTextColumn Header="First Name" Binding="{Binding FirstName}" 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}" x:DataType="local:Person" />
<DataGridCheckBoxColumn Header="Is Banned" Binding="{Binding IsBanned}" Width="*" IsThreeState="{Binding #IsThreeStateCheckBox.IsChecked, Mode=OneWay}" x:DataType="local:Person" />
<DataGridTemplateColumn Header="Age" >
<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}" />
<DataGridCheckBoxColumn Header="Is Banned" Binding="{Binding IsBanned}" Width="*" IsThreeState="{Binding #IsThreeStateCheckBox.IsChecked, Mode=OneWay}" />
<DataGridTemplateColumn Header="Age">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="local:Person">
<DataTemplate>
<TextBlock Text="{Binding Age, StringFormat='{}{0} years'}" VerticalAlignment="Center" HorizontalAlignment="Center" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate DataType="local:Person">
<DataTemplate>
<NumericUpDown Value="{Binding Age}" FormatString="N0" HorizontalAlignment="Stretch" Minimum="0" Maximum="120" TemplateApplied="NumericUpDown_OnTemplateApplied" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>

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

@ -1,5 +1,6 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls;
@ -48,20 +49,22 @@ namespace ControlCatalog.Pages
var dg3 = this.Get<DataGrid>("dataGridEdit");
dg3.IsReadOnly = false;
var items = new List<Person>
var list = new ObservableCollection<Person>
{
new Person { FirstName = "John", LastName = "Doe" , Age = 30},
new Person { FirstName = "Elizabeth", LastName = "Thomas", IsBanned = true , Age = 40 },
new Person { FirstName = "Zack", LastName = "Ward" , Age = 50 }
};
var collectionView3 = new DataGridCollectionView(items);
dg3.Items = collectionView3;
DataGrid3Source = list;
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)
{
e.Row.Header = e.Row.GetIndex() + 1;

2
samples/ControlCatalog/Pages/ScrollSnapPage.xaml

@ -8,7 +8,7 @@
x:DataType="pages:ScrollSnapPageViewModel">
<StackPanel Orientation="Vertical" Spacing="4">
<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">
<StackPanel Orientation="Horizontal"

5
samples/GpuInterop/MainWindow.axaml.cs

@ -1,6 +1,7 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Rendering;
namespace GpuInterop
{
@ -8,9 +9,9 @@ namespace GpuInterop
{
public MainWindow()
{
this.InitializeComponent();
InitializeComponent();
this.AttachDevTools();
this.Renderer.DrawFps = true;
Renderer.Diagnostics.DebugOverlays = RendererDebugOverlays.Fps;
}
private void InitializeComponent()

4
samples/RenderDemo/App.xaml.cs

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

14
samples/RenderDemo/MainWindow.xaml

@ -26,6 +26,20 @@
IsHitTestVisible="False" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Command="{Binding ToggleDrawLayoutTimeGraph}" Header="Draw layout time graph">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding DrawLayoutTimeGraph}"
IsHitTestVisible="False" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Command="{Binding ToggleDrawRenderTimeGraph}" Header="Draw render time graph">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding DrawRenderTimeGraph}"
IsHitTestVisible="False" />
</MenuItem.Icon>
</MenuItem>
</MenuItem>
<MenuItem Header="Tests">
<MenuItem Command="{Binding ResizeWindow}" Header="Resize window" />

23
samples/RenderDemo/MainWindow.xaml.cs

@ -1,7 +1,9 @@
using System;
using System.Linq.Expressions;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Rendering;
using RenderDemo.ViewModels;
using MiniMvvm;
@ -11,13 +13,26 @@ namespace RenderDemo
{
public MainWindow()
{
this.InitializeComponent();
InitializeComponent();
this.AttachDevTools();
var vm = new MainWindowViewModel();
vm.WhenAnyValue(x => x.DrawDirtyRects).Subscribe(x => Renderer.DrawDirtyRects = x);
vm.WhenAnyValue(x => x.DrawFps).Subscribe(x => Renderer.DrawFps = x);
this.DataContext = vm;
void BindOverlay(Expression<Func<MainWindowViewModel, bool>> expr, RendererDebugOverlays overlay)
=> vm.WhenAnyValue(expr).Subscribe(x =>
{
var diagnostics = Renderer.Diagnostics;
diagnostics.DebugOverlays = x ?
diagnostics.DebugOverlays | overlay :
diagnostics.DebugOverlays & ~overlay;
});
BindOverlay(x => x.DrawDirtyRects, RendererDebugOverlays.DirtyRects);
BindOverlay(x => x.DrawFps, RendererDebugOverlays.Fps);
BindOverlay(x => x.DrawLayoutTimeGraph, RendererDebugOverlays.LayoutTimeGraph);
BindOverlay(x => x.DrawRenderTimeGraph, RendererDebugOverlays.RenderTimeGraph);
DataContext = vm;
}
private void InitializeComponent()

45
samples/RenderDemo/ViewModels/MainWindowViewModel.cs

@ -1,49 +1,66 @@
using System.Reactive;
using System.Threading.Tasks;
using System.Threading.Tasks;
using MiniMvvm;
namespace RenderDemo.ViewModels
{
public class MainWindowViewModel : ViewModelBase
{
private bool drawDirtyRects = false;
private bool drawFps = true;
private double width = 800;
private double height = 600;
private bool _drawDirtyRects;
private bool _drawFps = true;
private bool _drawLayoutTimeGraph;
private bool _drawRenderTimeGraph;
private double _width = 800;
private double _height = 600;
public MainWindowViewModel()
{
ToggleDrawDirtyRects = MiniCommand.Create(() => DrawDirtyRects = !DrawDirtyRects);
ToggleDrawFps = MiniCommand.Create(() => DrawFps = !DrawFps);
ToggleDrawLayoutTimeGraph = MiniCommand.Create(() => DrawLayoutTimeGraph = !DrawLayoutTimeGraph);
ToggleDrawRenderTimeGraph = MiniCommand.Create(() => DrawRenderTimeGraph = !DrawRenderTimeGraph);
ResizeWindow = MiniCommand.CreateFromTask(ResizeWindowAsync);
}
public bool DrawDirtyRects
{
get => drawDirtyRects;
set => this.RaiseAndSetIfChanged(ref drawDirtyRects, value);
get => _drawDirtyRects;
set => RaiseAndSetIfChanged(ref _drawDirtyRects, value);
}
public bool DrawFps
{
get => drawFps;
set => this.RaiseAndSetIfChanged(ref drawFps, value);
get => _drawFps;
set => RaiseAndSetIfChanged(ref _drawFps, value);
}
public bool DrawLayoutTimeGraph
{
get => _drawLayoutTimeGraph;
set => RaiseAndSetIfChanged(ref _drawLayoutTimeGraph, value);
}
public bool DrawRenderTimeGraph
{
get => _drawRenderTimeGraph;
set => RaiseAndSetIfChanged(ref _drawRenderTimeGraph, value);
}
public double Width
{
get => width;
set => this.RaiseAndSetIfChanged(ref width, value);
get => _width;
set => RaiseAndSetIfChanged(ref _width, value);
}
public double Height
{
get => height;
set => this.RaiseAndSetIfChanged(ref height, value);
get => _height;
set => RaiseAndSetIfChanged(ref _height, value);
}
public MiniCommand ToggleDrawDirtyRects { get; }
public MiniCommand ToggleDrawFps { get; }
public MiniCommand ToggleDrawLayoutTimeGraph { get; }
public MiniCommand ToggleDrawRenderTimeGraph { get; }
public MiniCommand ResizeWindow { get; }
private async Task ResizeWindowAsync()

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

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

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

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

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

@ -31,9 +31,7 @@ using Android.Graphics.Drawables;
namespace Avalonia.Android.Platform.SkiaPlatform
{
class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo,
ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider,
ITopLevelWithSystemNavigationManager
class TopLevelImpl : IAndroidView, ITopLevelImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo
{
private readonly IGlPlatformSurface _gl;
private readonly IFramebufferPlatformSurface _framebuffer;
@ -41,6 +39,9 @@ namespace Avalonia.Android.Platform.SkiaPlatform
private readonly AndroidKeyboardEventsHelper<TopLevelImpl> _keyboardHelper;
private readonly AndroidMotionEventsHelper _pointerHelper;
private readonly AndroidInputMethod<ViewImpl> _textInputMethod;
private readonly INativeControlHostImpl _nativeControlHost;
private readonly IStorageProvider _storageProvider;
private readonly ISystemNavigationManagerImpl _systemNavigationManager;
private ViewImpl _view;
public TopLevelImpl(AvaloniaView avaloniaView, bool placeOnTop = false)
@ -57,10 +58,10 @@ namespace Avalonia.Android.Platform.SkiaPlatform
MaxClientSize = new PixelSize(_view.Resources.DisplayMetrics.WidthPixels,
_view.Resources.DisplayMetrics.HeightPixels).ToSize(RenderScaling);
NativeControlHost = new AndroidNativeControlHostImpl(avaloniaView);
StorageProvider = new AndroidStorageProvider((Activity)avaloniaView.Context);
_nativeControlHost = new AndroidNativeControlHostImpl(avaloniaView);
_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) =>
@ -109,16 +110,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public IEnumerable<object> Surfaces => new object[] { _gl, _framebuffer, Handle };
public IRenderer CreateRenderer(IRenderRoot root) =>
AndroidPlatform.Options.UseCompositor
? 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);
new CompositingRenderer(root, AndroidPlatform.Compositor, () => Surfaces);
public virtual void Hide()
{
@ -303,14 +295,6 @@ namespace Avalonia.Android.Platform.SkiaPlatform
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)
{
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

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

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

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

@ -127,7 +127,9 @@ namespace Avalonia.Input
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,
IPointer pointer,
Visual rootVisual, Point rootVisualPosition,
@ -146,7 +148,9 @@ namespace Avalonia.Input
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,
Visual rootVisual, Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, KeyModifiers modifiers,
@ -167,7 +171,9 @@ namespace Avalonia.Input
{
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;
Source = source;

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

@ -105,5 +105,13 @@ namespace Avalonia.Input.TextInput
UnsubscribeFromParents();
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;
}
}
}

20
src/Avalonia.Base/Layout/LayoutManager.cs

@ -3,8 +3,9 @@ using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Logging;
using Avalonia.Rendering;
using Avalonia.Threading;
using Avalonia.VisualTree;
using Avalonia.Utilities;
#nullable enable
@ -24,6 +25,7 @@ namespace Avalonia.Layout
private bool _disposed;
private bool _queued;
private bool _running;
private int _totalPassCount;
public LayoutManager(ILayoutRoot owner)
{
@ -33,6 +35,8 @@ namespace Avalonia.Layout
public virtual event EventHandler? LayoutUpdated;
internal Action<LayoutPassTiming>? LayoutPassTimed { get; set; }
/// <inheritdoc/>
public virtual void InvalidateMeasure(Layoutable control)
{
@ -116,10 +120,9 @@ namespace Avalonia.Layout
if (!_running)
{
Stopwatch? stopwatch = null;
const LogEventLevel timingLogLevel = LogEventLevel.Information;
bool captureTiming = Logger.IsEnabled(timingLogLevel, LogArea.Layout);
var captureTiming = LayoutPassTimed is not null || Logger.IsEnabled(timingLogLevel, LogArea.Layout);
var startingTimestamp = 0L;
if (captureTiming)
{
@ -129,8 +132,7 @@ namespace Avalonia.Layout
_toMeasure.Count,
_toArrange.Count);
stopwatch = new Stopwatch();
stopwatch.Start();
startingTimestamp = Stopwatch.GetTimestamp();
}
_toMeasure.BeginLoop(MaxPasses);
@ -139,6 +141,7 @@ namespace Avalonia.Layout
try
{
_running = true;
++_totalPassCount;
for (var pass = 0; pass < MaxPasses; ++pass)
{
@ -160,9 +163,10 @@ namespace Avalonia.Layout
if (captureTiming)
{
stopwatch!.Stop();
var elapsed = StopwatchHelper.GetElapsedTime(startingTimestamp);
LayoutPassTimed?.Invoke(new LayoutPassTiming(_totalPassCount, elapsed));
Logger.TryGet(timingLogLevel, LogArea.Layout)?.Log(this, "Layout pass finished in {Time}", stopwatch.Elapsed);
Logger.TryGet(timingLogLevel, LogArea.Layout)?.Log(this, "Layout pass finished in {Time}", elapsed);
}
}

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

@ -1,5 +1,7 @@
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Avalonia.Platform;
using Avalonia.Utilities;
@ -10,6 +12,7 @@ namespace Avalonia.Media.Imaging
/// </summary>
public class Bitmap : IBitmap
{
private bool _isTranscoded;
/// <summary>
/// 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.
@ -100,7 +103,28 @@ namespace Avalonia.Media.Imaging
/// <param name="stride">The number of bytes per row.</param>
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/>
@ -145,6 +169,57 @@ namespace Avalonia.Media.Imaging
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/>
void IImage.Draw(
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.IO;
using System.Runtime.CompilerServices;
using Avalonia.Platform;
namespace Avalonia.Media.Imaging
@ -9,7 +10,9 @@ namespace Avalonia.Media.Imaging
/// </summary>
public class WriteableBitmap : Bitmap
{
// Holds a buffer with pixel format that requires transcoding
private BitmapMemory? _pixelFormatMemory = null;
/// <summary>
/// Initializes a new instance of the <see cref="WriteableBitmap"/> class.
/// </summary>
@ -19,16 +22,67 @@ namespace Avalonia.Media.Imaging
/// <param name="alphaFormat">The alpha format (optional).</param>
/// <returns>An <see cref="IWriteableBitmapImpl"/>.</returns>
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)
{
@ -67,14 +121,25 @@ namespace Avalonia.Media.Imaging
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();
PixelFormat finalFormat = format ?? ri.DefaultPixelFormat;
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()

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>
public PixelRect WithHeight(int height)
{
return new PixelRect(X, Y, Width, Height);
return new PixelRect(X, Y, Width, height);
}
/// <summary>

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

@ -23,5 +23,4 @@ public static class OptionalFeatureProviderExtensions
rv = provider.TryGetFeature<T>();
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.
/// </summary>
public PixelFormat DefaultPixelFormat { get; }
bool IsSupportedBitmapPixelFormat(PixelFormat format);
}
[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"/>.
/// </summary>
[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,
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);
}
}

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

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

6
src/Avalonia.Base/Point.cs

@ -251,6 +251,12 @@ namespace Avalonia
/// <param name="transform">The transform.</param>
/// <returns>The transformed point.</returns>
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>
/// Returns a new point with the specified X coordinate.

27
src/Avalonia.Base/Rect.cs

@ -1,5 +1,6 @@
using System;
using System.Globalization;
using System.Numerics;
using Avalonia.Animation.Animators;
using Avalonia.Utilities;
@ -441,6 +442,32 @@ namespace Avalonia
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>
/// Translates the rectangle by an offset.

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

@ -1,15 +1,12 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia.Collections;
using Avalonia.Collections.Pooled;
using Avalonia.Media;
using Avalonia.Rendering.Composition.Drawing;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Threading;
using Avalonia.VisualTree;
// Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see>
@ -38,6 +35,9 @@ public class CompositingRenderer : IRendererWithCompositor
/// </summary>
public bool RenderOnlyOnRenderThread { get; set; } = true;
/// <inheritdoc/>
public RendererDiagnostics Diagnostics { get; }
public CompositingRenderer(IRenderRoot root, Compositor compositor, Func<IEnumerable<object>> surfaces)
{
_root = root;
@ -46,20 +46,21 @@ public class CompositingRenderer : IRendererWithCompositor
CompositionTarget = compositor.CreateCompositionTarget(surfaces);
CompositionTarget.Root = ((Visual)root).AttachToCompositor(compositor);
_update = Update;
Diagnostics = new RendererDiagnostics();
Diagnostics.PropertyChanged += OnDiagnosticsPropertyChanged;
}
/// <inheritdoc/>
public bool DrawFps
{
get => CompositionTarget.DrawFps;
set => CompositionTarget.DrawFps = value;
}
/// <inheritdoc/>
public bool DrawDirtyRects
private void OnDiagnosticsPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
get => CompositionTarget.DrawDirtyRects;
set => CompositionTarget.DrawDirtyRects = value;
switch (e.PropertyName)
{
case nameof(RendererDiagnostics.DebugOverlays):
CompositionTarget.DebugOverlays = Diagnostics.DebugOverlays;
break;
case nameof(RendererDiagnostics.LastLayoutPassTiming):
CompositionTarget.LastLayoutPassTiming = Diagnostics.LastLayoutPassTiming;
break;
}
}
/// <inheritdoc/>
@ -83,8 +84,16 @@ public class CompositingRenderer : IRendererWithCompositor
}
/// <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;
if (filter != null)
f = v =>
@ -93,8 +102,8 @@ public class CompositingRenderer : IRendererWithCompositor
return filter(dlv.Visual);
return true;
};
var res = CompositionTarget.TryHitTest(p, f);
var res = CompositionTarget.TryHitTest(p, rootVisual, f);
if(res == null)
yield break;
foreach(var v in res)
@ -257,6 +266,7 @@ public class CompositingRenderer : IRendererWithCompositor
_recalculateChildren.Clear();
CompositionTarget.Size = _root.ClientSize;
CompositionTarget.Scaling = _root.RenderScaling;
TriggerSceneInvalidatedOnBatchCompletion(_compositor.RequestCommitAsync());
}
private async void TriggerSceneInvalidatedOnBatchCompletion(Task batchCompletion)

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

@ -61,9 +61,6 @@ internal class CompositionDrawListVisual : CompositionContainerVisual
return false;
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);
}

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

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

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

@ -28,16 +28,15 @@ namespace Avalonia.Rendering.Composition
/// <summary>
/// Attempts to perform a hit-tst
/// </summary>
/// <param name="point"></param>
/// <param name="filter"></param>
/// <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();
if (Root == null)
root ??= Root;
if (root == null)
return null;
var res = new PooledList<CompositionVisual>();
HitTestCore(Root, point, res, filter);
HitTestCore(root, point, res, filter);
return res;
}

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

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using Avalonia.Platform;
using Avalonia.Rendering.Composition.Animations;
using Avalonia.Rendering.Composition.Server;
@ -15,7 +14,7 @@ public partial class Compositor
/// <returns></returns>
public CompositionTarget CreateCompositionTarget(Func<IEnumerable<object>> surfaces)
{
return new CompositionTarget(this, new ServerCompositionTarget(_server, surfaces));
return new CompositionTarget(this, new ServerCompositionTarget(_server, surfaces, DiagnosticTextRenderer));
}
public CompositionContainerVisual CreateContainerVisual() => new(this, new ServerCompositionContainerVisual(_server));

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

@ -35,9 +35,14 @@ namespace Avalonia.Rendering.Composition
private Task? _pendingBatch;
private readonly object _pendingBatchLock = new();
private List<Action> _pendingServerCompositorJobs = new();
private DiagnosticTextRenderer? _diagnosticTextRenderer;
internal IEasing DefaultEasing { get; }
private DiagnosticTextRenderer DiagnosticTextRenderer
=> _diagnosticTextRenderer ??= new(Typeface.Default.GlyphTypeface, 12.0);
internal event Action? AfterCommit;
/// <summary>
/// Creates a new compositor on a specified render loop that would use a particular GPU
@ -88,6 +93,7 @@ namespace Avalonia.Rendering.Composition
{
if (_invokeBeforeCommitWrite.Count > 0)
RequestCommitAsync();
AfterCommit?.Invoke();
}
}

78
src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs

@ -0,0 +1,78 @@
using System;
using Avalonia.Media;
using Avalonia.Platform;
namespace Avalonia.Rendering.Composition.Server
{
/// <summary>
/// A class used to render diagnostic strings (only!), with caching of ASCII glyph runs.
/// </summary>
internal sealed class DiagnosticTextRenderer
{
private const char FirstChar = (char)32;
private const char LastChar = (char)126;
private readonly GlyphRun[] _runs = new GlyphRun[LastChar - FirstChar + 1];
public double GetMaxHeight()
{
var maxHeight = 0.0;
for (var c = FirstChar; c <= LastChar; c++)
{
var height = _runs[c - FirstChar].Size.Height;
if (height > maxHeight)
{
maxHeight = height;
}
}
return maxHeight;
}
public DiagnosticTextRenderer(IGlyphTypeface typeface, double fontRenderingEmSize)
{
var chars = new char[LastChar - FirstChar + 1];
for (var c = FirstChar; c <= LastChar; c++)
{
var index = c - FirstChar;
chars[index] = c;
var glyph = typeface.GetGlyph(c);
_runs[index] = new GlyphRun(typeface, fontRenderingEmSize, chars.AsMemory(index, 1), new[] { glyph });
}
}
public Size MeasureAsciiText(ReadOnlySpan<char> text)
{
var width = 0.0;
var height = 0.0;
foreach (var c in text)
{
var effectiveChar = c is >= FirstChar and <= LastChar ? c : ' ';
var run = _runs[effectiveChar - FirstChar];
width += run.Size.Width;
height = Math.Max(height, run.Size.Height);
}
return new Size(width, height);
}
public void DrawAsciiText(IDrawingContextImpl context, ReadOnlySpan<char> text, IBrush foreground)
{
var offset = 0.0;
var originalTransform = context.Transform;
foreach (var c in text)
{
var effectiveChar = c is >= FirstChar and <= LastChar ? c : ' ';
var run = _runs[effectiveChar - FirstChar];
context.Transform = originalTransform * Matrix.CreateTranslation(offset, 0.0);
context.DrawGlyphRun(foreground, run.PlatformImpl);
offset += run.Size.Width;
}
context.Transform = originalTransform;
}
}
}

58
src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs

@ -1,11 +1,8 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Platform;
using Avalonia.Utilities;
// Special license applies <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see>
@ -17,26 +14,18 @@ namespace Avalonia.Rendering.Composition.Server;
internal class FpsCounter
{
private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
private readonly DiagnosticTextRenderer _textRenderer;
private int _framesThisSecond;
private int _totalFrames;
private int _fps;
private TimeSpan _lastFpsUpdate;
const int FirstChar = 32;
const int LastChar = 126;
// ASCII chars
private GlyphRun[] _runs = new GlyphRun[LastChar - FirstChar + 1];
public FpsCounter(IGlyphTypeface typeface)
{
for (var c = FirstChar; c <= LastChar; c++)
{
var s = new string((char)c, 1);
var glyph = typeface.GetGlyph((uint)(s[0]));
_runs[c - FirstChar] = new GlyphRun(typeface, 18, s.AsMemory(), new ushort[] { glyph });
}
}
public void FpsTick() => _framesThisSecond++;
public FpsCounter(DiagnosticTextRenderer textRenderer)
=> _textRenderer = textRenderer;
public void FpsTick()
=> _framesThisSecond++;
public void RenderFps(IDrawingContextImpl context, string aux)
{
@ -53,27 +42,24 @@ internal class FpsCounter
_lastFpsUpdate = now;
}
var fpsLine = FormattableString.Invariant($"Frame #{_totalFrames:00000000} FPS: {_fps:000} ") + aux;
double width = 0;
double height = 0;
foreach (var ch in fpsLine)
{
var run = _runs[ch - FirstChar];
width += run.Size.Width;
height = Math.Max(height, run.Size.Height);
}
#if NET6_0_OR_GREATER
var fpsLine = string.Create(CultureInfo.InvariantCulture, $"Frame #{_totalFrames:00000000} FPS: {_fps:000} {aux}");
#else
var fpsLine = FormattableString.Invariant($"Frame #{_totalFrames:00000000} FPS: {_fps:000} {aux}");
#endif
var rect = new Rect(0, 0, width + 3, height + 3);
var size = _textRenderer.MeasureAsciiText(fpsLine.AsSpan());
var rect = new Rect(0.0, 0.0, size.Width + 3.0, size.Height + 3.0);
context.DrawRectangle(Brushes.Black, null, rect);
double offset = 0;
foreach (var ch in fpsLine)
{
var run = _runs[ch - FirstChar];
context.Transform = Matrix.CreateTranslation(offset, 0);
context.DrawGlyphRun(Brushes.White, run.PlatformImpl);
offset += run.Size.Width;
}
_textRenderer.DrawAsciiText(context, fpsLine.AsSpan(), Brushes.White);
}
public void Reset()
{
_framesThisSecond = 0;
_totalFrames = 0;
_fps = 0;
}
}

176
src/Avalonia.Base/Rendering/Composition/Server/FrameTimeGraph.cs

@ -0,0 +1,176 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
namespace Avalonia.Rendering.Composition.Server;
/// <summary>
/// Represents a simple time graph for diagnostics purpose, used to show layout and render times.
/// </summary>
internal sealed class FrameTimeGraph
{
private const double HeaderPadding = 2.0;
private readonly IPlatformRenderInterface _renderInterface;
private readonly ImmutableSolidColorBrush _borderBrush;
private readonly ImmutablePen _graphPen;
private readonly double[] _frameValues;
private readonly Size _size;
private readonly Size _headerSize;
private readonly Size _graphSize;
private readonly double _defaultMaxY;
private readonly string _title;
private readonly DiagnosticTextRenderer _textRenderer;
private int _startFrameIndex;
private int _frameCount;
public Size Size
=> _size;
public FrameTimeGraph(int maxFrames, Size size, double defaultMaxY, string title,
DiagnosticTextRenderer textRenderer)
{
Debug.Assert(maxFrames >= 1);
Debug.Assert(size.Width > 0.0);
Debug.Assert(size.Height > 0.0);
_renderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
_borderBrush = new ImmutableSolidColorBrush(0x80808080);
_graphPen = new ImmutablePen(Brushes.Blue);
_frameValues = new double[maxFrames];
_size = size;
_headerSize = new Size(size.Width, textRenderer.GetMaxHeight() + HeaderPadding * 2.0);
_graphSize = new Size(size.Width, size.Height - _headerSize.Height);
_defaultMaxY = defaultMaxY;
_title = title;
_textRenderer = textRenderer;
}
public void AddFrameValue(double value)
{
if (_frameCount < _frameValues.Length)
{
_frameValues[_startFrameIndex + _frameCount] = value;
++_frameCount;
}
else
{
// overwrite oldest value
_frameValues[_startFrameIndex] = value;
if (++_startFrameIndex == _frameValues.Length)
{
_startFrameIndex = 0;
}
}
}
public void Reset()
{
_startFrameIndex = 0;
_frameCount = 0;
}
public void Render(IDrawingContextImpl context)
{
var originalTransform = context.Transform;
context.PushClip(new Rect(_size));
context.DrawRectangle(_borderBrush, null, new RoundedRect(new Rect(_size)));
context.DrawRectangle(_borderBrush, null, new RoundedRect(new Rect(_headerSize)));
context.Transform = originalTransform * Matrix.CreateTranslation(HeaderPadding, HeaderPadding);
_textRenderer.DrawAsciiText(context, _title.AsSpan(), Brushes.Black);
if (_frameCount > 0)
{
var (min, avg, max) = GetYValues();
DrawLabelledValue(context, "Min", min, originalTransform, _headerSize.Width * 0.19);
DrawLabelledValue(context, "Avg", avg, originalTransform, _headerSize.Width * 0.46);
DrawLabelledValue(context, "Max", max, originalTransform, _headerSize.Width * 0.73);
context.Transform = originalTransform * Matrix.CreateTranslation(0.0, _headerSize.Height);
context.DrawGeometry(null, _graphPen, BuildGraphGeometry(Math.Max(max, _defaultMaxY)));
}
context.Transform = originalTransform;
context.PopClip();
}
private void DrawLabelledValue(IDrawingContextImpl context, string label, double value, in Matrix originalTransform,
double left)
{
context.Transform = originalTransform * Matrix.CreateTranslation(left + HeaderPadding, HeaderPadding);
var brush = value <= _defaultMaxY ? Brushes.Black : Brushes.Red;
#if NET6_0_OR_GREATER
Span<char> buffer = stackalloc char[24];
buffer.TryWrite(CultureInfo.InvariantCulture, $"{label}: {value,5:F2}ms", out var charsWritten);
_textRenderer.DrawAsciiText(context, buffer.Slice(0, charsWritten), brush);
#else
var text = FormattableString.Invariant($"{label}: {value,5:F2}ms");
_textRenderer.DrawAsciiText(context, text.AsSpan(), brush);
#endif
}
private IStreamGeometryImpl BuildGraphGeometry(double maxY)
{
Debug.Assert(_frameCount > 0);
var graphGeometry = _renderInterface.CreateStreamGeometry();
using var geometryContext = graphGeometry.Open();
var xRatio = _graphSize.Width / _frameValues.Length;
var yRatio = _graphSize.Height / maxY;
geometryContext.BeginFigure(new Point(0.0, _graphSize.Height - GetFrameValue(0) * yRatio), false);
for (var i = 1; i < _frameCount; ++i)
{
var x = Math.Round(i * xRatio);
var y = _graphSize.Height - GetFrameValue(i) * yRatio;
geometryContext.LineTo(new Point(x, y));
}
geometryContext.EndFigure(false);
return graphGeometry;
}
private (double Min, double Average, double Max) GetYValues()
{
Debug.Assert(_frameCount > 0);
var min = double.MaxValue;
var max = double.MinValue;
var total = 0.0;
for (var i = 0; i < _frameCount; ++i)
{
var y = GetFrameValue(i);
total += y;
if (y < min)
{
min = y;
}
if (y > max)
{
max = y;
}
}
return (min, total / _frameCount, max);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetFrameValue(int frameOffset)
=> _frameValues[(_startFrameIndex + frameOffset) % _frameValues.Length];
}

134
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Diagnostics;
using System.Threading;
using Avalonia.Media;
using Avalonia.Media.Imaging;
@ -21,11 +21,12 @@ namespace Avalonia.Rendering.Composition.Server
{
private readonly ServerCompositor _compositor;
private readonly Func<IEnumerable<object>> _surfaces;
private readonly DiagnosticTextRenderer _diagnosticTextRenderer;
private static long s_nextId = 1;
public long Id { get; }
public ulong Revision { get; private set; }
private IRenderTarget? _renderTarget;
private FpsCounter _fpsCounter = new FpsCounter(Typeface.Default.GlyphTypeface);
private FpsCounter? _fpsCounter;
private FrameTimeGraph? _renderTimeGraph;
private FrameTimeGraph? _layoutTimeGraph;
private Rect _dirtyRect;
private Random _random = new();
private Size _layerSize;
@ -35,18 +36,34 @@ namespace Avalonia.Rendering.Composition.Server
private HashSet<ServerCompositionVisual> _attachedVisuals = new();
private Queue<ServerCompositionVisual> _adornerUpdateQueue = new();
public long Id { get; }
public ulong Revision { get; private set; }
public ICompositionTargetDebugEvents? DebugEvents { get; set; }
public ReadbackIndices Readback { get; } = new();
public int RenderedVisuals { get; set; }
public ServerCompositionTarget(ServerCompositor compositor, Func<IEnumerable<object>> surfaces) :
base(compositor)
private FpsCounter FpsCounter
=> _fpsCounter ??= new FpsCounter(_diagnosticTextRenderer);
private FrameTimeGraph LayoutTimeGraph
=> _layoutTimeGraph ??= CreateTimeGraph("Layout");
private FrameTimeGraph RenderTimeGraph
=> _renderTimeGraph ??= CreateTimeGraph("Render");
public ServerCompositionTarget(ServerCompositor compositor, Func<IEnumerable<object>> surfaces,
DiagnosticTextRenderer diagnosticTextRenderer)
: base(compositor)
{
_compositor = compositor;
_surfaces = surfaces;
_diagnosticTextRenderer = diagnosticTextRenderer;
Id = Interlocked.Increment(ref s_nextId);
}
private FrameTimeGraph CreateTimeGraph(string title)
=> new(360, new Size(360.0, 64.0), 1000.0 / 60.0, title, _diagnosticTextRenderer);
partial void OnIsEnabledChanged()
{
if (IsEnabled)
@ -62,7 +79,33 @@ namespace Avalonia.Rendering.Composition.Server
v.Deactivate();
}
}
partial void OnDebugOverlaysChanged()
{
if ((DebugOverlays & RendererDebugOverlays.Fps) == 0)
{
_fpsCounter?.Reset();
}
if ((DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) == 0)
{
_layoutTimeGraph?.Reset();
}
if ((DebugOverlays & RendererDebugOverlays.RenderTimeGraph) == 0)
{
_renderTimeGraph?.Reset();
}
}
partial void OnLastLayoutPassTimingChanged()
{
if ((DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) != 0)
{
LayoutTimeGraph.AddFrameValue(LastLayoutPassTiming.Elapsed.TotalMilliseconds);
}
}
partial void DeserializeChangesExtra(BatchStreamReader c)
{
_redrawRequested = true;
@ -92,7 +135,10 @@ namespace Avalonia.Rendering.Composition.Server
return;
Revision++;
var captureTiming = (DebugOverlays & RendererDebugOverlays.RenderTimeGraph) != 0;
var startingTimestamp = captureTiming ? Stopwatch.GetTimestamp() : 0L;
// Update happens in a separate phase to extend dirty rect if needed
Root.Update(this);
@ -137,33 +183,69 @@ namespace Avalonia.Rendering.Composition.Server
targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1,
new Rect(_layerSize),
new Rect(Size), BitmapInterpolationMode.LowQuality);
if (DrawDirtyRects)
{
targetContext.DrawRectangle(new ImmutableSolidColorBrush(
new Color(30, (byte)_random.Next(255), (byte)_random.Next(255),
(byte)_random.Next(255)))
, null, _dirtyRect);
}
if (DrawFps)
if (DebugOverlays != RendererDebugOverlays.None)
{
var nativeMem = ByteSizeHelper.ToString((ulong)(
(Compositor.BatchMemoryPool.CurrentUsage + Compositor.BatchMemoryPool.CurrentPool) *
Compositor.BatchMemoryPool.BufferSize), false);
var managedMem = ByteSizeHelper.ToString((ulong)(
(Compositor.BatchObjectPool.CurrentUsage + Compositor.BatchObjectPool.CurrentPool) *
Compositor.BatchObjectPool.ArraySize *
IntPtr.Size), false);
_fpsCounter.RenderFps(targetContext, FormattableString.Invariant($"M:{managedMem} / N:{nativeMem} R:{RenderedVisuals:0000}"));
if (captureTiming)
{
var elapsed = StopwatchHelper.GetElapsedTime(startingTimestamp);
RenderTimeGraph.AddFrameValue(elapsed.TotalMilliseconds);
}
DrawOverlays(targetContext);
}
RenderedVisuals = 0;
_dirtyRect = default;
}
}
private void DrawOverlays(IDrawingContextImpl targetContext)
{
if ((DebugOverlays & RendererDebugOverlays.DirtyRects) != 0)
{
targetContext.DrawRectangle(
new ImmutableSolidColorBrush(
new Color(30, (byte)_random.Next(255), (byte)_random.Next(255), (byte)_random.Next(255))),
null,
_dirtyRect);
}
if ((DebugOverlays & RendererDebugOverlays.Fps) != 0)
{
var nativeMem = ByteSizeHelper.ToString((ulong) (
(Compositor.BatchMemoryPool.CurrentUsage + Compositor.BatchMemoryPool.CurrentPool) *
Compositor.BatchMemoryPool.BufferSize), false);
var managedMem = ByteSizeHelper.ToString((ulong) (
(Compositor.BatchObjectPool.CurrentUsage + Compositor.BatchObjectPool.CurrentPool) *
Compositor.BatchObjectPool.ArraySize *
IntPtr.Size), false);
FpsCounter.RenderFps(targetContext,
FormattableString.Invariant($"M:{managedMem} / N:{nativeMem} R:{RenderedVisuals:0000}"));
}
var top = 0.0;
void DrawTimeGraph(FrameTimeGraph graph)
{
top += 8.0;
targetContext.Transform = Matrix.CreateTranslation(Size.Width - graph.Size.Width - 8.0, top);
graph.Render(targetContext);
top += graph.Size.Height;
}
if ((DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) != 0)
{
DrawTimeGraph(LayoutTimeGraph);
}
if ((DebugOverlays & RendererDebugOverlays.RenderTimeGraph) != 0)
{
DrawTimeGraph(RenderTimeGraph);
}
}
public Rect SnapToDevicePixels(Rect rect) => SnapToDevicePixels(rect, Scaling);
private static Rect SnapToDevicePixels(Rect rect, double scale)

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>
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>
/// Converts a point from screen to client coordinates.
/// </summary>

11
src/Avalonia.Base/Rendering/IRenderer.cs

@ -1,5 +1,4 @@
using System;
using Avalonia.VisualTree;
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Rendering.Composition;
@ -12,15 +11,9 @@ namespace Avalonia.Rendering
public interface IRenderer : IDisposable
{
/// <summary>
/// Gets or sets a value indicating whether the renderer should draw an FPS counter.
/// Gets a value indicating whether the renderer should draw specific diagnostics.
/// </summary>
bool DrawFps { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the renderer should draw a visual representation
/// of its dirty rectangles.
/// </summary>
bool DrawDirtyRects { get; set; }
RendererDiagnostics Diagnostics { get; }
/// <summary>
/// Raised when a portion of the scene has been invalidated.

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
{
/// <summary>
/// A renderer which renders the state of the visual tree without an intermediate scene graph
/// representation.
/// This class is used to render the visual tree into a DrawingContext by doing
/// a simple tree traversal.
/// It's currently used mostly for RenderTargetBitmap.Render and VisualBrush
/// </summary>
/// <remarks>
/// 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
internal class ImmediateRenderer : IVisualBrushRenderer//, IRenderer
{
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>
/// Renders a visual to a render target.
/// </summary>
@ -113,11 +23,8 @@ namespace Avalonia.Rendering
/// <param name="target">The render target.</param>
public static void Render(Visual visual, IRenderTarget target)
{
using (var renderer = new ImmediateRenderer(visual, () => target, updateTransformedBounds: false))
using (var context = new DrawingContext(target.CreateDrawingContext(renderer)))
{
renderer.Render(context, visual, visual.Bounds);
}
using var context = new DrawingContext(target.CreateDrawingContext(new ImmediateRenderer()));
Render(context, visual, visual.Bounds);
}
/// <summary>
@ -127,77 +34,9 @@ namespace Avalonia.Rendering
/// <param name="context">The drawing context.</param>
public static void Render(Visual visual, DrawingContext context)
{
using (var renderer = new ImmediateRenderer(visual,
() => throw new InvalidOperationException("This is not supposed to be called"),
updateTransformedBounds: false))
{
renderer.Render(context, visual, visual.Bounds);
}
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/>
Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
@ -215,18 +54,7 @@ namespace Avalonia.Rendering
internal static void Render(Visual visual, DrawingContext context, bool updateTransformedBounds)
{
using var renderer = new ImmediateRenderer(visual,
() => 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);
}
Render(context, visual, visual.Bounds);
}
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)
{
yield return visual;
}
}
}
private void Render(DrawingContext context, Visual visual, Rect clipRect)
private static void Render(DrawingContext context, Visual visual, Rect clipRect)
{
var opacity = visual.Opacity;
var clipToBounds = visual.ClipToBounds;
@ -338,15 +129,7 @@ namespace Avalonia.Rendering
using (context.PushTransformContainer())
{
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
? visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance)
: (IEnumerable<Visual>)visual.VisualChildren;
@ -362,18 +145,9 @@ namespace Avalonia.Rendering
: clipRect;
Render(context, child, childClipRect);
}
else if (_updateTransformedBounds)
{
ClearTransformedBounds(child);
}
}
}
}
if (!visual.IsVisible && _updateTransformedBounds)
{
ClearTransformedBounds(visual);
}
}
}
}

11
src/Avalonia.Base/Rendering/LayoutPassTiming.cs

@ -0,0 +1,11 @@
using System;
namespace Avalonia.Rendering
{
/// <summary>
/// Represents a single layout pass timing.
/// </summary>
/// <param name="PassCounter">The number of the layout pass.</param>
/// <param name="Elapsed">The elapsed time during the layout pass.</param>
internal readonly record struct LayoutPassTiming(int PassCounter, TimeSpan Elapsed);
}

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

@ -6,9 +6,7 @@ using Avalonia.Reactive;
namespace Avalonia.Rendering;
[Unstable]
// TODO: Make it internal once legacy renderers are removed
public class PlatformRenderInterfaceContextManager
internal class PlatformRenderInterfaceContextManager
{
private readonly IPlatformGraphics? _graphics;
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();
}
}

35
src/Avalonia.Base/Rendering/RendererDebugOverlays.cs

@ -0,0 +1,35 @@
using System;
namespace Avalonia.Rendering;
/// <summary>
/// Represents the various types of overlays that can be drawn by a renderer.
/// </summary>
[Flags]
public enum RendererDebugOverlays
{
/// <summary>
/// Do not draw any overlay.
/// </summary>
None = 0,
/// <summary>
/// Draw a FPS counter.
/// </summary>
Fps = 1 << 0,
/// <summary>
/// Draw invalidated rectangles each frame.
/// </summary>
DirtyRects = 1 << 1,
/// <summary>
/// Draw a graph of past layout times.
/// </summary>
LayoutTimeGraph = 1 << 2,
/// <summary>
/// Draw a graph of past render times.
/// </summary>
RenderTimeGraph = 1 << 3
}

57
src/Avalonia.Base/Rendering/RendererDiagnostics.cs

@ -0,0 +1,57 @@
using System.ComponentModel;
namespace Avalonia.Rendering
{
/// <summary>
/// Manages configurable diagnostics that can be displayed by a renderer.
/// </summary>
public class RendererDiagnostics : INotifyPropertyChanged
{
private RendererDebugOverlays _debugOverlays;
private LayoutPassTiming _lastLayoutPassTiming;
private PropertyChangedEventArgs? _debugOverlaysChangedEventArgs;
private PropertyChangedEventArgs? _lastLayoutPassTimingChangedEventArgs;
/// <summary>
/// Gets or sets which debug overlays are displayed by the renderer.
/// </summary>
public RendererDebugOverlays DebugOverlays
{
get => _debugOverlays;
set
{
if (_debugOverlays != value)
{
_debugOverlays = value;
OnPropertyChanged(_debugOverlaysChangedEventArgs ??= new(nameof(DebugOverlays)));
}
}
}
/// <summary>
/// Gets or sets the last layout pass timing that the renderer may display.
/// </summary>
internal LayoutPassTiming LastLayoutPassTiming
{
get => _lastLayoutPassTiming;
set
{
if (!_lastLayoutPassTiming.Equals(value))
{
_lastLayoutPassTiming = value;
OnPropertyChanged(_lastLayoutPassTimingChangedEventArgs ??= new(nameof(LastLayoutPassTiming)));
}
}
}
/// <inheritdoc />
public event PropertyChangedEventHandler? PropertyChanged;
/// <summary>
/// Called when a property changes on the object.
/// </summary>
/// <param name="args">The property change details.</param>
protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
=> PropertyChanged?.Invoke(this, args);
}
}

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

19
src/Avalonia.Base/Utilities/StopwatchHelper.cs

@ -0,0 +1,19 @@
using System;
using System.Diagnostics;
namespace Avalonia.Utilities;
/// <summary>
/// Allows using <see cref="Stopwatch"/> as timestamps without allocating.
/// </summary>
/// <remarks>Equivalent to Stopwatch.GetElapsedTime in .NET 7.</remarks>
internal static class StopwatchHelper
{
private static readonly double s_timestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency;
public static TimeSpan GetElapsedTime(long startingTimestamp)
=> GetElapsedTime(startingTimestamp, Stopwatch.GetTimestamp());
public static TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp)
=> new((long)((endingTimestamp - startingTimestamp) * s_timestampToTicks));
}

18
src/Avalonia.Base/Visual.cs

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

4
src/Avalonia.Base/composition-schema.xml

@ -39,8 +39,8 @@
<Object Name="CompositionTarget" CustomServerCtor="true">
<Property Name="Root" Type="CompositionVisual?"/>
<Property Name="IsEnabled" Type="bool"/>
<Property Name="DrawDirtyRects" Type="bool"/>
<Property Name="DrawFps" Type="bool"/>
<Property Name="DebugOverlays" Type="RendererDebugOverlays"/>
<Property Name="LastLayoutPassTiming" Type="LayoutPassTiming" Internal="true"/>
<Property Name="Scaling" Type="double"/>
<Property Name="Size" Type="Size" />
</Object>

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

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

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

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

4
src/Avalonia.Controls/Button.cs

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

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

@ -1,10 +1,13 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
namespace Avalonia.Controls.Embedding.Offscreen
{
@ -13,7 +16,7 @@ namespace Avalonia.Controls.Embedding.Offscreen
{
private double _scaling = 1;
private Size _clientSize;
private PlatformRenderInterfaceContextManager _renderInterface = new(null);
private ManualRenderTimer _manualRenderTimer = new();
public IInputRoot? InputRoot { get; private set; }
public bool IsDisposed { get; private set; }
@ -23,10 +26,19 @@ namespace Avalonia.Controls.Embedding.Offscreen
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) =>
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 Size ClientSize
@ -82,5 +94,7 @@ namespace Avalonia.Controls.Embedding.Offscreen
public WindowTransparencyLevel TransparencyLevel { get; private set; }
public IPopupImpl? CreatePopup() => null;
public virtual object? TryGetFeature(Type featureType) => null;
}
}

1
src/Avalonia.Controls/ItemsControl.cs

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

2
src/Avalonia.Controls/NativeControlHost.cs

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

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

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

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

@ -15,7 +15,7 @@ namespace Avalonia.Controls.Notifications
/// </summary>
[TemplatePart("PART_Items", typeof(Panel))]
[PseudoClasses(":topleft", ":topright", ":bottomleft", ":bottomright")]
public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager, ICustomSimpleHitTest
public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager
{
private IList? _items;
@ -160,7 +160,5 @@ namespace Avalonia.Controls.Notifications
PseudoClasses.Set(":bottomleft", position == NotificationPosition.BottomLeft);
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.
var item = GetMenuItem(e.Source as Control) as MenuItem;
if (item?.TransformedBounds == null)
{
if (item == null)
return;
}
var serverTransform = item?.CompositionVisual?.TryGetServerGlobalTransform();
if (serverTransform == null)
return;
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);
}

6
src/Avalonia.Controls/Platform/INativeControlHostImpl.cs

@ -29,10 +29,4 @@ namespace Avalonia.Controls.Platform
void HideWithSize(Size size);
void ShowInBounds(Rect rect);
}
[Unstable]
public interface ITopLevelImplWithNativeControlHost
{
INativeControlHostImpl? NativeControlHost { get; }
}
}

7
src/Avalonia.Controls/Platform/ITopLevelImpl.cs

@ -51,7 +51,7 @@ namespace Avalonia.Platform
/// <see cref="IPopupImpl"/>.
/// </remarks>
[Unstable]
public interface ITopLevelImpl : IDisposable
public interface ITopLevelImpl : IOptionalFeatureProvider, IDisposable
{
/// <summary>
/// Gets the client size of the toplevel.
@ -111,11 +111,6 @@ namespace Avalonia.Platform
/// <param name="root">The toplevel.</param>
IRenderer CreateRenderer(IRenderRoot root);
/// <summary>
/// Invalidates a rect on the toplevel.
/// </summary>
void Invalidate(Rect rect);
/// <summary>
/// Sets the <see cref="IInputRoot"/> for the toplevel.
/// </summary>

11
src/Avalonia.Controls/Platform/ITopLevelImplWithStorageProvider.cs

@ -1,11 +0,0 @@
using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
namespace Avalonia.Controls.Platform;
[Unstable]
public interface ITopLevelImplWithStorageProvider : ITopLevelImpl
{
public IStorageProvider StorageProvider { get; }
}

13
src/Avalonia.Controls/Platform/ITopLevelImplWithTextInputMethod.cs

@ -1,13 +0,0 @@
using Avalonia.Input;
using Avalonia.Input.TextInput;
using Avalonia.Metadata;
using Avalonia.Platform;
namespace Avalonia.Controls.Platform
{
[Unstable]
public interface ITopLevelImplWithTextInputMethod : ITopLevelImpl
{
public ITextInputMethodImpl? TextInputMethod { get; }
}
}

6
src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs

@ -23,10 +23,4 @@ namespace Avalonia.Controls.Platform
{
INativeMenuExporter? NativeMenuExporter { get; }
}
[Unstable]
public interface ITopLevelImplWithNativeMenuExporter : ITopLevelImpl
{
ITopLevelNativeMenuExporter? NativeMenuExporter { get; }
}
}

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

@ -607,30 +607,6 @@ namespace Avalonia.Controls.Presenters
if (Extent.Height > Viewport.Height)
{
double height = isLogical ? scrollable!.ScrollSize.Height : 50;
if(VerticalSnapPointsType == SnapPointsType.MandatorySingle && Content is IScrollSnapPointsInfo)
{
if(_areVerticalSnapPointsRegular)
{
height = _verticalSnapPoint;
}
else if(_verticalSnapPoints != null && _verticalSnapPoints.Count > 0)
{
double yOffset = Offset.Y;
switch (VerticalSnapPointsAlignment)
{
case SnapPointsAlignment.Center:
yOffset += Viewport.Height / 2;
break;
case SnapPointsAlignment.Far:
yOffset += Viewport.Height;
break;
}
var snapPoint = FindNearestSnapPoint(_verticalSnapPoints, yOffset, out var lowerSnapPoint);
height = snapPoint - lowerSnapPoint;
}
}
y += -delta.Y * height;
y = Math.Max(y, 0);
y = Math.Min(y, Extent.Height - Viewport.Height);
@ -639,30 +615,6 @@ namespace Avalonia.Controls.Presenters
if (Extent.Width > Viewport.Width)
{
double width = isLogical ? scrollable!.ScrollSize.Width : 50;
if (HorizontalSnapPointsType == SnapPointsType.MandatorySingle && Content is IScrollSnapPointsInfo)
{
if (_areHorizontalSnapPointsRegular)
{
width = _horizontalSnapPoint;
}
else if(_horizontalSnapPoints != null && _horizontalSnapPoints.Count > 0)
{
double xOffset = Offset.X;
switch (VerticalSnapPointsAlignment)
{
case SnapPointsAlignment.Center:
xOffset += Viewport.Width / 2;
break;
case SnapPointsAlignment.Far:
xOffset += Viewport.Width;
break;
}
var snapPoint = FindNearestSnapPoint(_horizontalSnapPoints, xOffset, out var lowerSnapPoint);
width = snapPoint - lowerSnapPoint;
}
}
x += -delta.X * width;
x = Math.Max(x, 0);
x = Math.Min(x, Extent.Width - Viewport.Width);

10
src/Avalonia.Controls/Primitives/AdornerLayer.cs

@ -14,7 +14,7 @@ namespace Avalonia.Controls.Primitives
/// <remarks>
/// TODO: Need to track position of adorned elements and move the adorner if they move.
/// </remarks>
public class AdornerLayer : Canvas, ICustomSimpleHitTest
public class AdornerLayer : Canvas
{
/// <summary>
/// Allows for getting and setting of the adorned element.
@ -305,17 +305,9 @@ namespace Avalonia.Controls.Primitives
info.Bounds = new TransformedBounds(new Rect(adorned.Bounds.Size), new Rect(adorned.Bounds.Size), Matrix.Identity);
InvalidateMeasure();
});
else
info.Subscription = adorned.GetObservable(TransformedBoundsProperty).Subscribe(x =>
{
info.Bounds = x;
InvalidateMeasure();
});
}
}
public bool HitTest(Point point) => Children.HitTestCustom(point);
private class AdornedElementInfo
{
public IDisposable? Subscription { get; set; }

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

@ -4,7 +4,7 @@ using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives
{
public class ChromeOverlayLayer : Panel, ICustomSimpleHitTest
public class ChromeOverlayLayer : Panel
{
public static Panel? GetOverlayLayer(Visual visual)
{
@ -26,7 +26,5 @@ namespace Avalonia.Controls.Primitives
{
base.Children.Add(c);
}
public bool HitTest(Point point) => Children.HitTestCustom(point);
}
}

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

@ -4,7 +4,7 @@ using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives
{
public class OverlayLayer : Canvas, ICustomSimpleHitTest
public class OverlayLayer : Canvas
{
public Size AvailableSize { get; private set; }
public static OverlayLayer? GetOverlayLayer(Visual visual)
@ -22,8 +22,6 @@ namespace Avalonia.Controls.Primitives
return null;
}
public bool HitTest(Point point) => Children.HitTestCustom(point);
protected override Size MeasureOverride(Size availableSize)
{
foreach (Control child in Children)

16
src/Avalonia.Controls/Primitives/Popup.cs

@ -8,6 +8,7 @@ using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Input.TextInput;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
@ -378,11 +379,8 @@ namespace Avalonia.Controls.Primitives
if (InheritsTransform && placementTarget is Control c)
{
SubscribeToEventHandler<Control, EventHandler<AvaloniaPropertyChangedEventArgs>>(
c,
PlacementTargetPropertyChanged,
(x, handler) => x.PropertyChanged += handler,
(x, handler) => x.PropertyChanged -= handler).DisposeWith(handlerCleanup);
TransformTrackingHelper.Track(c, PlacementTargetTransformChanged)
.DisposeWith(handlerCleanup);
}
else
{
@ -872,13 +870,11 @@ namespace Avalonia.Controls.Primitives
Close();
}
}
private void PlacementTargetPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
private void PlacementTargetTransformChanged(Visual v, Matrix? matrix)
{
if (_openState is not null && e.Property == Visual.TransformedBoundsProperty)
{
if (_openState is not null)
UpdateHostSizing(_openState.PopupHost, _openState.TopLevel, _openState.PlacementTarget);
}
}
private void WindowLostFocus()

3
src/Avalonia.Controls/Remote/RemoteWidget.cs

@ -2,6 +2,7 @@
using System.Runtime.InteropServices;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Remote.Protocol;
using Avalonia.Remote.Protocol.Viewport;
using Avalonia.Threading;
@ -72,7 +73,7 @@ namespace Avalonia.Controls.Remote
{
if (_lastFrame != null && _lastFrame.Width != 0 && _lastFrame.Height != 0)
{
var fmt = (PixelFormat) _lastFrame.Format;
var fmt = new PixelFormat((PixelFormatEnum) _lastFrame.Format);
if (_bitmap == null || _bitmap.PixelSize.Width != _lastFrame.Width ||
_bitmap.PixelSize.Height != _lastFrame.Height)
{

34
src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs

@ -11,6 +11,7 @@ using Avalonia.Platform;
using Avalonia.Remote.Protocol;
using Avalonia.Remote.Protocol.Input;
using Avalonia.Remote.Protocol.Viewport;
using Avalonia.Rendering;
using Avalonia.Threading;
using Key = Avalonia.Input.Key;
using PixelFormat = Avalonia.Platform.PixelFormat;
@ -19,7 +20,7 @@ using ProtocolPixelFormat = Avalonia.Remote.Protocol.Viewport.PixelFormat;
namespace Avalonia.Controls.Remote.Server
{
[Unstable]
public class RemoteServerTopLevelImpl : OffscreenTopLevelImplBase, IFramebufferPlatformSurface
internal class RemoteServerTopLevelImpl : OffscreenTopLevelImplBase, IFramebufferPlatformSurface, ITopLevelImpl
{
private readonly IAvaloniaRemoteTransportConnection _transport;
private LockedFramebuffer? _framebuffer;
@ -28,7 +29,8 @@ namespace Avalonia.Controls.Remote.Server
private long _lastReceivedFrame = -1;
private long _nextFrameNumber = 1;
private ClientViewportAllocatedMessage? _pendingAllocation;
private bool _invalidated;
private bool _queuedNextRender;
private bool _inRender;
private Vector _dpi = new Vector(96, 96);
private ProtocolPixelFormat[]? _supportedFormats;
@ -38,6 +40,14 @@ namespace Avalonia.Controls.Remote.Server
_transport.OnMessage += OnMessage;
KeyboardDevice = AvaloniaLocator.Current.GetRequiredService<IKeyboardDevice>();
QueueNextRender();
}
IRenderer ITopLevelImpl.CreateRenderer(IRenderRoot root)
{
var r = (IRendererWithCompositor)base.CreateRenderer(root);
r.Compositor.AfterCommit += QueueNextRender;
return r;
}
private static RawPointerEventType GetAvaloniaEventType (Avalonia.Remote.Protocol.Input.MouseButton button, bool pressed)
@ -125,7 +135,7 @@ namespace Avalonia.Controls.Remote.Server
lock(_lock)
{
_dpi = new Vector(renderInfo.DpiX, renderInfo.DpiY);
_invalidated = true;
_queuedNextRender = true;
}
Dispatcher.UIThread.Post(RenderIfNeeded);
@ -282,7 +292,7 @@ namespace Avalonia.Controls.Remote.Server
{
if (width > 0 && height > 0)
{
_framebuffer = new LockedFramebuffer(handle.AddrOfPinnedObject(), new PixelSize(width, height), width * bpp, _dpi, (PixelFormat)fmt,
_framebuffer = new LockedFramebuffer(handle.AddrOfPinnedObject(), new PixelSize(width, height), width * bpp, _dpi, new((PixelFormatEnum)fmt),
null);
Paint?.Invoke(new Rect(0, 0, width, height));
}
@ -315,7 +325,7 @@ namespace Avalonia.Controls.Remote.Server
{
lock (_lock)
{
if (_lastReceivedFrame != _lastSentFrame || !_invalidated || _supportedFormats == null)
if (_lastReceivedFrame != _lastSentFrame || !_queuedNextRender || _supportedFormats == null)
return;
}
@ -327,23 +337,25 @@ namespace Avalonia.Controls.Remote.Server
format = fmt;
break;
}
_inRender = true;
var frame = RenderFrame((int) ClientSize.Width, (int) ClientSize.Height, format);
lock (_lock)
{
_lastSentFrame = _nextFrameNumber++;
frame.SequenceId = _lastSentFrame;
_invalidated = false;
_queuedNextRender = false;
}
_inRender = false;
_transport.Send(frame);
}
public override void Invalidate(Rect rect)
private void QueueNextRender()
{
if (!IsDisposed)
if (!_inRender && !IsDisposed)
{
_invalidated = true;
Dispatcher.UIThread.Post(RenderIfNeeded);
_queuedNextRender = true;
DispatcherTimer.RunOnce(RenderIfNeeded, TimeSpan.FromMilliseconds(2), DispatcherPriority.Background);
}
}

2
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@ -11,6 +11,7 @@ using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Logging;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.Utilities;
using Avalonia.VisualTree;
@ -121,6 +122,7 @@ namespace Avalonia.Controls
/// <summary>
/// Gets or sets the template used to display each item.
/// </summary>
[InheritDataTypeFromItems(nameof(Items))]
public IDataTemplate? ItemTemplate
{
get => GetValue(ItemTemplateProperty);

79
src/Avalonia.Controls/TopLevel.cs

@ -1,7 +1,7 @@
using System;
using System.ComponentModel;
using Avalonia.Reactive;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Notifications;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
@ -18,7 +18,6 @@ using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Styling;
using Avalonia.Utilities;
using Avalonia.VisualTree;
using Avalonia.Input.Platform;
using System.Linq;
@ -108,6 +107,7 @@ namespace Avalonia.Controls
private Border? _transparencyFallbackBorder;
private TargetWeakEventSubscriber<TopLevel, ResourcesChangedEventArgs>? _resourcesChangesSubscriber;
private IStorageProvider? _storageProvider;
private LayoutDiagnosticBridge? _layoutDiagnosticBridge;
/// <summary>
/// Initializes static members of the <see cref="TopLevel"/> class.
@ -192,7 +192,7 @@ namespace Avalonia.Controls
ClientSize = impl.ClientSize;
FrameSize = impl.FrameSize;
this.GetObservable(PointerOverElementProperty)
.Select(
x => (x as InputElement)?.GetObservable(CursorProperty) ?? Observable.Empty<Cursor>())
@ -214,9 +214,9 @@ namespace Avalonia.Controls
_pointerOverPreProcessor = new PointerOverPreProcessor(this);
_pointerOverPreProcessorSubscription = _inputManager?.PreProcess.Subscribe(_pointerOverPreProcessor);
if(impl is ITopLevelWithSystemNavigationManager topLevelWithSystemNavigation)
if(impl.TryGetFeature<ISystemNavigationManagerImpl>() is {} systemNavigationManager)
{
topLevelWithSystemNavigation.SystemNavigationManager.BackRequested += (s, e) =>
systemNavigationManager.BackRequested += (s, e) =>
{
e.RoutedEvent = BackRequestedEvent;
RaiseEvent(e);
@ -333,8 +333,17 @@ namespace Avalonia.Controls
{
get
{
if (_layoutManager == null)
if (_layoutManager is null)
{
_layoutManager = CreateLayoutManager();
if (_layoutManager is LayoutManager typedLayoutManager && Renderer is not null)
{
_layoutDiagnosticBridge = new LayoutDiagnosticBridge(Renderer.Diagnostics, typedLayoutManager);
_layoutDiagnosticBridge.SetupBridge();
}
}
return _layoutManager;
}
}
@ -387,15 +396,9 @@ namespace Avalonia.Controls
public IStorageProvider StorageProvider => _storageProvider
??= AvaloniaLocator.Current.GetService<IStorageProviderFactory>()?.CreateProvider(this)
?? (PlatformImpl as ITopLevelImplWithStorageProvider)?.StorageProvider
?? PlatformImpl?.TryGetFeature<IStorageProvider>()
?? throw new InvalidOperationException("StorageProvider platform implementation is not available.");
/// <inheritdoc/>
void IRenderRoot.Invalidate(Rect rect)
{
PlatformImpl?.Invalidate(rect);
}
/// <inheritdoc/>
Point IRenderRoot.PointToClient(PixelPoint p)
{
@ -468,6 +471,9 @@ namespace Avalonia.Controls
Renderer?.Dispose();
Renderer = null!;
_layoutDiagnosticBridge?.Dispose();
_layoutDiagnosticBridge = null;
_pointerOverPreProcessor?.OnCompleted();
_pointerOverPreProcessorSubscription?.Dispose();
_backGestureSubscription?.Dispose();
@ -654,7 +660,50 @@ namespace Avalonia.Controls
// Do nothing becuase TopLevel should't apply MirrorTransform on himself.
}
ITextInputMethodImpl? ITextInputMethodRoot.InputMethod =>
(PlatformImpl as ITopLevelImplWithTextInputMethod)?.TextInputMethod;
ITextInputMethodImpl? ITextInputMethodRoot.InputMethod => PlatformImpl?.TryGetFeature<ITextInputMethodImpl>();
/// <summary>
/// Provides layout pass timing from the layout manager to the renderer, for diagnostics purposes.
/// </summary>
private sealed class LayoutDiagnosticBridge : IDisposable
{
private readonly RendererDiagnostics _diagnostics;
private readonly LayoutManager _layoutManager;
private bool _isHandling;
public LayoutDiagnosticBridge(RendererDiagnostics diagnostics, LayoutManager layoutManager)
{
_diagnostics = diagnostics;
_layoutManager = layoutManager;
diagnostics.PropertyChanged += OnDiagnosticsPropertyChanged;
}
public void SetupBridge()
{
var needsHandling = (_diagnostics.DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) != 0;
if (needsHandling != _isHandling)
{
_isHandling = needsHandling;
_layoutManager.LayoutPassTimed = needsHandling
? timing => _diagnostics.LastLayoutPassTiming = timing
: null;
}
}
private void OnDiagnosticsPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(RendererDiagnostics.DebugOverlays))
{
SetupBridge();
}
}
public void Dispose()
{
_diagnostics.PropertyChanged -= OnDiagnosticsPropertyChanged;
_layoutManager.LayoutPassTimed = null;
}
}
}
}

14
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@ -11,7 +11,7 @@ using Avalonia.Threading;
namespace Avalonia.DesignerSupport.Remote
{
class PreviewerWindowImpl : RemoteServerTopLevelImpl, IWindowImpl, ITopLevelImplWithStorageProvider
class PreviewerWindowImpl : RemoteServerTopLevelImpl, IWindowImpl
{
private readonly IAvaloniaRemoteTransportConnection _transport;
@ -92,8 +92,16 @@ namespace Avalonia.DesignerSupport.Remote
public bool NeedsManagedDecorations => false;
public IStorageProvider StorageProvider => new NoopStorageProvider();
public override object TryGetFeature(Type featureType)
{
if (featureType == typeof(IStorageProvider))
{
return new NoopStorageProvider();
}
return base.TryGetFeature(featureType);
}
public void Activate()
{
}

15
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@ -14,6 +14,8 @@ using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Platform.Storage.FileIO;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
namespace Avalonia.DesignerSupport.Remote
{
@ -61,9 +63,15 @@ namespace Avalonia.DesignerSupport.Remote
}));
}
public IRenderer CreateRenderer(IRenderRoot root) => new ImmediateRenderer((Visual)root, () =>
new PlatformRenderInterfaceContextManager(null)
.CreateRenderTarget(Surfaces));
class DummyRenderTimer : IRenderTimer
{
public event Action<TimeSpan> Tick;
public bool RunsInBackground => false;
}
public IRenderer CreateRenderer(IRenderRoot root) =>
new CompositingRenderer(root,
new Compositor(new RenderLoop(new DummyRenderTimer(), Dispatcher.UIThread), null), () => Surfaces);
public void Dispose()
{
@ -186,6 +194,7 @@ namespace Avalonia.DesignerSupport.Remote
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { }
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1);
public object TryGetFeature(Type featureType) => null;
}
class ClipboardStub : IClipboard

14
src/Avalonia.Diagnostics/Diagnostics/Conventions.cs

@ -1,12 +1,20 @@
using System;
using System.IO;
namespace Avalonia.Diagnostics
{
internal static class Conventions
{
public static string DefaultScreenshotsRoot =>
System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures, Environment.SpecialFolderOption.Create),
"Screenshots");
public static string DefaultScreenshotsRoot
{
get
{
var dir = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "Screenshots");
Directory.CreateDirectory(dir);
return dir;
}
}
public static IScreenshotHandler DefaultScreenshotHandler { get; } =
new Screenshots.FilePickerHandler();

109
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@ -1,11 +1,13 @@
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Avalonia.Controls;
using Avalonia.Diagnostics.Models;
using Avalonia.Input;
using Avalonia.Metadata;
using Avalonia.Threading;
using Avalonia.Reactive;
using Avalonia.Rendering;
namespace Avalonia.Diagnostics.ViewModels
{
@ -21,8 +23,6 @@ namespace Avalonia.Diagnostics.ViewModels
private string? _focusedControl;
private IInputElement? _pointerOverElement;
private bool _shouldVisualizeMarginPadding = true;
private bool _shouldVisualizeDirtyRects;
private bool _showFpsOverlay;
private bool _freezePopups;
private string? _pointerOverElementName;
private IInputRoot? _pointerOverRoot;
@ -75,69 +75,76 @@ namespace Avalonia.Diagnostics.ViewModels
set => RaiseAndSetIfChanged(ref _shouldVisualizeMarginPadding, value);
}
public bool ShouldVisualizeDirtyRects
public void ToggleVisualizeMarginPadding()
=> ShouldVisualizeMarginPadding = !ShouldVisualizeMarginPadding;
private IRenderer? TryGetRenderer()
=> _root switch
{
TopLevel topLevel => topLevel.Renderer,
Controls.Application app => app.RendererRoot,
_ => null
};
private bool GetDebugOverlay(RendererDebugOverlays overlay)
=> ((TryGetRenderer()?.Diagnostics.DebugOverlays ?? RendererDebugOverlays.None) & overlay) != 0;
private void SetDebugOverlay(RendererDebugOverlays overlay, bool enable,
[CallerMemberName] string? propertyName = null)
{
get => _shouldVisualizeDirtyRects;
set
if (TryGetRenderer() is not { } renderer)
{
var changed = true;
if (_root is TopLevel topLevel && topLevel.Renderer is { })
{
topLevel.Renderer.DrawDirtyRects = value;
}
else if (_root is Controls.Application app && app.RendererRoot is { })
{
app.RendererRoot.DrawDirtyRects = value;
}
else
{
changed = false;
}
if (changed)
{
RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value);
}
return;
}
}
public void ToggleVisualizeDirtyRects()
{
ShouldVisualizeDirtyRects = !ShouldVisualizeDirtyRects;
var oldValue = renderer.Diagnostics.DebugOverlays;
var newValue = enable ? oldValue | overlay : oldValue & ~overlay;
if (oldValue == newValue)
{
return;
}
renderer.Diagnostics.DebugOverlays = newValue;
RaisePropertyChanged(propertyName);
}
public void ToggleVisualizeMarginPadding()
public bool ShowDirtyRectsOverlay
{
ShouldVisualizeMarginPadding = !ShouldVisualizeMarginPadding;
get => GetDebugOverlay(RendererDebugOverlays.DirtyRects);
set => SetDebugOverlay(RendererDebugOverlays.DirtyRects, value);
}
public void ToggleDirtyRectsOverlay()
=> ShowDirtyRectsOverlay = !ShowDirtyRectsOverlay;
public bool ShowFpsOverlay
{
get => _showFpsOverlay;
set
{
var changed = true;
if (_root is TopLevel topLevel && topLevel.Renderer is { })
{
topLevel.Renderer.DrawFps = value;
}
else if (_root is Controls.Application app && app.RendererRoot is { })
{
app.RendererRoot.DrawFps = value;
}
else
{
changed = false;
}
if(changed)
RaiseAndSetIfChanged(ref _showFpsOverlay, value);
}
get => GetDebugOverlay(RendererDebugOverlays.Fps);
set => SetDebugOverlay(RendererDebugOverlays.Fps, value);
}
public void ToggleFpsOverlay()
=> ShowFpsOverlay = !ShowFpsOverlay;
public bool ShowLayoutTimeGraphOverlay
{
ShowFpsOverlay = !ShowFpsOverlay;
get => GetDebugOverlay(RendererDebugOverlays.LayoutTimeGraph);
set => SetDebugOverlay(RendererDebugOverlays.LayoutTimeGraph, value);
}
public void ToggleLayoutTimeGraphOverlay()
=> ShowLayoutTimeGraphOverlay = !ShowLayoutTimeGraphOverlay;
public bool ShowRenderTimeGraphOverlay
{
get => GetDebugOverlay(RendererDebugOverlays.RenderTimeGraph);
set => SetDebugOverlay(RendererDebugOverlays.RenderTimeGraph, value);
}
public void ToggleRenderTimeGraphOverlay()
=> ShowRenderTimeGraphOverlay = !ShowRenderTimeGraphOverlay;
public ConsoleViewModel Console { get; }
public ViewModelBase? Content
@ -254,10 +261,10 @@ namespace Avalonia.Diagnostics.ViewModels
_pointerOverSubscription.Dispose();
_logicalTree.Dispose();
_visualTree.Dispose();
if (_root is TopLevel top)
if (TryGetRenderer() is { } renderer)
{
top.Renderer.DrawDirtyRects = false;
top.Renderer.DrawFps = false;
renderer.Diagnostics.DebugOverlays = RendererDebugOverlays.None;
}
}

4
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ViewModelBase.cs

@ -20,7 +20,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
}
protected bool RaiseAndSetIfChanged<T>([NotNullIfNotNull("value")] ref T field, T value, [CallerMemberName] string propertyName = null!)
protected bool RaiseAndSetIfChanged<T>([NotNullIfNotNull("value")] ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (!EqualityComparer<T>.Default.Equals(field, value))
{
@ -32,7 +32,7 @@ namespace Avalonia.Diagnostics.ViewModels
return false;
}
protected void RaisePropertyChanged([CallerMemberName] string propertyName = null!)
protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null)
{
var e = new PropertyChangedEventArgs(propertyName);
OnPropertyChanged(e);

32
src/Avalonia.Diagnostics/Diagnostics/Views/LayoutExplorerView.axaml.cs

@ -40,15 +40,14 @@ namespace Avalonia.Diagnostics.Views
_layoutRoot = this.GetControl<Grid>("LayoutRoot");
void SubscribeToBounds(Visual visual)
Visual? visual = _contentArea;
while (visual != null && !ReferenceEquals(visual, this))
{
visual.GetPropertyChangedObservable(TransformedBoundsProperty)
visual.GetPropertyChangedObservable(BoundsProperty)
.Subscribe(UpdateSizeGuidelines);
visual = visual.VisualParent;
}
SubscribeToBounds(_borderArea);
SubscribeToBounds(_paddingArea);
SubscribeToBounds(_contentArea);
}
private void InitializeComponent()
@ -56,22 +55,27 @@ namespace Avalonia.Diagnostics.Views
AvaloniaXamlLoader.Load(this);
}
private void UpdateSizeGuidelines(AvaloniaPropertyChangedEventArgs e)
private void UpdateSizeGuidelines(AvaloniaPropertyChangedEventArgs _)
{
void UpdateGuidelines(Visual area)
{
if (area.TransformedBounds is TransformedBounds bounds)
// That's what TransformedBounds.Bounds actually was.
// The code below doesn't really make sense to me, so I've just changed v.TransformedBounds.Bounds
// to GetPseudoTransformedBounds
Rect GetPseudoTransformedBounds(Visual v) => new(v.Bounds.Size);
var bounds = GetPseudoTransformedBounds(area);
{
// Horizontal guideline
{
var sizeArea = TranslateToRoot((_horizontalSize.TransformedBounds ?? default).Bounds.BottomLeft,
var sizeArea = TranslateToRoot(GetPseudoTransformedBounds(_horizontalSize).BottomLeft,
_horizontalSize);
var start = TranslateToRoot(bounds.Bounds.BottomLeft, area);
var start = TranslateToRoot(bounds.BottomLeft, area);
SetPosition(_horizontalSizeBegin, start);
var end = TranslateToRoot(bounds.Bounds.BottomRight, area);
var end = TranslateToRoot(bounds.BottomRight, area);
SetPosition(_horizontalSizeEnd, end.WithX(end.X - 1));
@ -83,13 +87,13 @@ namespace Avalonia.Diagnostics.Views
// Vertical guideline
{
var sizeArea = TranslateToRoot((_verticalSize.TransformedBounds ?? default).Bounds.TopRight, _verticalSize);
var sizeArea = TranslateToRoot(GetPseudoTransformedBounds(_verticalSize).TopRight, _verticalSize);
var start = TranslateToRoot(bounds.Bounds.TopRight, area);
var start = TranslateToRoot(bounds.TopRight, area);
SetPosition(_verticalSizeBegin, start);
var end = TranslateToRoot(bounds.Bounds.BottomRight, area);
var end = TranslateToRoot(bounds.BottomRight, area);
SetPosition(_verticalSizeEnd, end.WithY(end.Y - 1));

24
src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml

@ -65,28 +65,42 @@
</MenuItem>
</MenuItem>
</MenuItem>
<MenuItem Header="_Options">
<MenuItem Header="Visualize margin/padding" Command="{Binding ToggleVisualizeMarginPadding}">
<MenuItem Header="_Overlays">
<MenuItem Header="Margin/padding" Command="{Binding ToggleVisualizeMarginPadding}">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding ShouldVisualizeMarginPadding}"
IsEnabled="False" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Visualize dirty rects" Command="{Binding ToggleVisualizeDirtyRects}">
<MenuItem Header="Dirty rects" Command="{Binding ToggleDirtyRectsOverlay}">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding ShouldVisualizeDirtyRects}"
IsChecked="{Binding ShowDirtyRectsOverlay}"
IsEnabled="False" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Show fps overlay" Command="{Binding ToggleFpsOverlay}">
<MenuItem Header="FPS" Command="{Binding ToggleFpsOverlay}">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding ShowFpsOverlay}"
IsEnabled="False" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Layout time graph" Command="{Binding ToggleLayoutTimeGraphOverlay}">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding ShowLayoutTimeGraphOverlay}"
IsEnabled="False" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Render time graph" Command="{Binding ToggleRenderTimeGraphOverlay}">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding ShowRenderTimeGraphOverlay}"
IsEnabled="False" />
</MenuItem.Icon>
</MenuItem>
</MenuItem>
</Menu>

8
src/Avalonia.Diagnostics/Diagnostics/VisualExtensions.cs

@ -17,11 +17,11 @@ namespace Avalonia.Diagnostics
/// <param name="dpi">Dpi quality.</param>
public static void RenderTo(this Control source, Stream destination, double dpi = 96)
{
if (source.TransformedBounds == null)
{
var transform = source.CompositionVisual?.TryGetServerGlobalTransform();
if (transform == null)
return;
}
var rect = source.TransformedBounds.Value.Clip;
var rect = new Rect(source.Bounds.Size).TransformToAABB(transform.Value);
var top = rect.TopLeft;
var pixelSize = new PixelSize((int)rect.Width, (int)rect.Height);
var dpiVector = new Vector(dpi, dpi);

1
src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs

@ -21,7 +21,6 @@ namespace Avalonia
return builder
.UseHeadless(new AvaloniaHeadlessPlatformOptions
{
UseCompositor = true,
UseHeadlessDrawing = false
})
.AfterSetup(_ =>

4
src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

@ -72,8 +72,7 @@ namespace Avalonia.Headless
.Bind<ITextShaperImpl>().ToSingleton<HeadlessTextShaperStub>()
.Bind<IWindowingPlatform>().ToConstant(new HeadlessWindowingPlatform())
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();
if (opts.UseCompositor)
Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(), null);
Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(), null);
}
@ -88,7 +87,6 @@ namespace Avalonia.Headless
public class AvaloniaHeadlessPlatformOptions
{
public bool UseCompositor { get; set; } = true;
public bool UseHeadlessDrawing { get; set; } = true;
}

3
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -30,6 +30,7 @@ namespace Avalonia.Headless
public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul;
public PixelFormat DefaultPixelFormat => PixelFormat.Rgba8888;
public bool IsSupportedBitmapPixelFormat(PixelFormat format) => true;
public IGeometryImpl CreateEllipseGeometry(Rect rect) => new HeadlessGeometryStub(rect);
@ -353,6 +354,8 @@ namespace Avalonia.Headless
}
public PixelFormat? Format { get; }
public ILockedFramebuffer Lock()
{
Version++;

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

Loading…
Cancel
Save