Browse Source

WIP

feature/composition-tree-inspector
Nikita Tsukanov 3 years ago
parent
commit
3ba496db5c
  1. 4
      samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
  2. 1
      samples/ControlCatalog/ControlCatalog.csproj
  3. 2
      samples/ControlCatalog/MainWindow.xaml.cs
  4. 15
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  5. 5
      src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
  6. 204
      src/Avalonia.Base/Rendering/Composition/CompositionTreeSnapshot.cs
  7. 16
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs
  8. 8
      src/Avalonia.Base/Rendering/Composition/ICompositionVisualWithDiagnosticsInfo.cs
  9. 8
      src/Avalonia.Base/Rendering/Composition/ICompositionVisualWithDrawList.cs
  10. 26
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs
  11. 5
      src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs
  12. 34
      src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
  13. 135
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/CompositionTreeSnapshotPageViewModel.cs
  14. 40
      src/Avalonia.Diagnostics/Diagnostics/Views/CompositionTreeSnapshotItemView.axaml
  15. 65
      src/Avalonia.Diagnostics/Diagnostics/Views/CompositionTreeSnapshotItemView.axaml.cs
  16. 52
      src/Avalonia.Diagnostics/Diagnostics/Views/CompositionTreeSnapshotView.axaml
  17. 55
      src/Avalonia.Diagnostics/Diagnostics/Views/CompositionTreeSnapshotView.axaml.cs
  18. 66
      src/Avalonia.Diagnostics/Diagnostics/Views/TypedUserControl.cs
  19. 3
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  20. 8
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  21. 9
      src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs
  22. 6
      src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs
  23. 2
      src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs
  24. 22
      src/Skia/Avalonia.Skia/SkiaBackendContext.cs
  25. 6
      src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs
  26. 7
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  27. 28
      src/tools/DevGenerators/CompositionGenerator/Generator.cs
  28. 4
      tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
  29. 4
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  30. 33
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  31. 15
      tests/Avalonia.UnitTests/TestRoot.cs

4
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

@ -2,10 +2,8 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<TargetFramework>net7.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RuntimeFrameworkVersion>6.0.8</RuntimeFrameworkVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(RunNativeAotCompilation)' == 'true'">

1
samples/ControlCatalog/ControlCatalog.csproj

@ -27,6 +27,7 @@
<ProjectReference Include="..\..\src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Simple\Avalonia.Themes.Simple.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
<ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />

2
samples/ControlCatalog/MainWindow.xaml.cs

@ -3,6 +3,7 @@ using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Diagnostics;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using ControlCatalog.ViewModels;
@ -19,6 +20,7 @@ namespace ControlCatalog
DataContext = new MainWindowViewModel();
_recentMenu = ((NativeMenu.GetMenu(this)?.Items[0] as NativeMenuItem)?.Menu?.Items[2] as NativeMenuItem)?.Menu;
DevTools.AttachCompositionSnapshot(this);
}
public static string MenuQuitHeader => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "Quit Avalonia" : "E_xit";

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

@ -217,5 +217,20 @@ namespace Avalonia.Platform
/// Indicates that the context is no longer usable. This method should be thread-safe
/// </summary>
bool IsLost { get; }
/// <summary>
/// Creates a new <see cref="IRenderTargetBitmapImpl"/> that can be used as a render layer
/// for the current render target.
/// </summary>
/// <param name="size">The size of the layer in DIPs.</param>
/// <returns>An <see cref="IRenderTargetBitmapImpl"/></returns>
/// <remarks>
/// Depending on the rendering backend used, a layer created via this method may be more
/// performant than a standard render target bitmap. In particular the Direct2D backend
/// has to do a format conversion each time a standard render target bitmap is rendered,
/// but a layer created via this method has no such overhead.
/// </remarks>
IDrawingContextLayerImpl CreateLayer(Size size, double scaling);
}
}

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

@ -64,9 +64,6 @@ internal class CompositionDrawListVisual : CompositionContainerVisual
return custom.HitTest(pt);
}
foreach (var op in DrawList!)
if (op.Item.HitTest(pt))
return true;
return false;
return DrawList?.HitTest(pt) == true;
}
}

204
src/Avalonia.Base/Rendering/Composition/CompositionTreeSnapshot.cs

@ -0,0 +1,204 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Rendering.Composition.Drawing;
using Avalonia.Rendering.Composition.Server;
using Avalonia.Utilities;
// ReSharper disable UnusedAutoPropertyAccessor.Global
namespace Avalonia.Rendering.Composition;
internal class CompositionTreeSnapshot : IAsyncDisposable
{
public Compositor Compositor { get; }
public CompositionTreeSnapshotItem Root { get; }
public Bitmap Bitmap { get; }
private CompositionTreeSnapshot(Compositor compositor, ServerCompositionVisual root, Bitmap bitmap)
{
Compositor = compositor;
Root = new CompositionTreeSnapshotItem(this, root);
Bitmap = bitmap;
}
public bool IsDisposed { get; private set; }
public ValueTask DisposeAsync()
{
IsDisposed = true;
Bitmap.Dispose();
return new ValueTask(Compositor.InvokeServerJobAsync(() =>
{
Root.Destroy();
}));
}
public static Task<CompositionTreeSnapshot?> TakeAsync(CompositionVisual visual)
{
return visual.Compositor.InvokeServerJobAsync(() =>
{
if (visual.Root == null)
return null;
Bitmap bitmap;
var ri = visual.Compositor.Server.RenderInterface;
using (ri.EnsureCurrent())
{
using (var layer =
ri.Value.CreateLayer(new Size(Math.Ceiling(visual.Size.X), Math.Ceiling(visual.Size.Y)), 1))
{
var visualBrushHelper = new CompositorDrawingContextProxy.VisualBrushRenderer();
using (var context = layer.CreateDrawingContext(visualBrushHelper))
{
context.Clear(Colors.Transparent);
visual.Server.Render(new CompositorDrawingContextProxy(context, visualBrushHelper), new Rect(0,
0,
visual.Server.Size.X,
visual.Server.Size.Y));
}
using (var ms = new MemoryStream())
{
layer.Save(ms);
ms.Position = 0;
bitmap = new Bitmap(ms);
}
}
}
return new CompositionTreeSnapshot(visual.Compositor, visual.Server, bitmap);
});
}
private CompositionTreeSnapshotItem? HitTest(CompositionTreeSnapshotItem item, Point pt)
{
if (!MatrixUtils.ToMatrix(item.Transform).TryInvert(out var inverted))
return null;
pt = inverted.Transform(pt);
if (double.IsNaN(pt.X) || double.IsNaN(pt.Y) || double.IsInfinity(pt.X) || double.IsInfinity(pt.Y))
return null;
if (item.ClipToBounds && (item.Size.X < pt.X || item.Size.Y < pt.Y || pt.X < 0 || pt.Y < 0))
return null;
if (item.GeometryClip?.FillContains(pt) == false && item.GeometryClip.FillContains(pt) == false)
return null;
for (var c = item.Children.Count - 1; c >= 0; c--)
{
var ch = item.Children[c];
var chResult = HitTest(ch, pt);
if (chResult != null)
return chResult;
}
if (item.HitTest(pt))
return item;
return null;
}
public CompositionTreeSnapshotItem? HitTest(Point pt) => HitTest(Root, pt);
}
internal class CompositionTreeSnapshotItem
{
private readonly CompositionTreeSnapshot _snapshot;
public string? Name { get; }
private CompositionDrawList? _drawList;
internal CompositionTreeSnapshotItem(CompositionTreeSnapshot snapshot, ServerCompositionVisual visual)
{
_snapshot = snapshot;
Name = (visual as ICompositionVisualWithDiagnosticsInfo)?.Name ?? visual.GetType().Name;
_drawList = (visual as ICompositionVisualWithDrawList)?.DrawList?.Clone();
DrawOperations = _drawList?.Select(x => x.Item.GetType().Name).ToList() ??
(IReadOnlyList<string>)Array.Empty<string>();
visual.PopulateDiagnosticProperties(Properties);
Transform = visual.CombinedTransformMatrix;
ClipToBounds = visual.ClipToBounds;
GeometryClip = visual.Clip;
Size = visual.Size;
if (visual is ServerCompositionContainerVisual container)
Children = container.Children.List.Select(x => new CompositionTreeSnapshotItem(snapshot, x)).ToList();
else
Children = Array.Empty<CompositionTreeSnapshotItem>();
}
public bool HitTest(Point v) => _drawList?.HitTest(v) == true;
public IGeometryImpl? GeometryClip { get; set; }
public bool ClipToBounds { get; }
public Matrix4x4 Transform { get; }
public Vector2 Size { get; }
public IReadOnlyList<string> DrawOperations { get; }
public IReadOnlyList<CompositionTreeSnapshotItem> Children { get; }
public Dictionary<string, object?> Properties { get; } = new();
public Task<Bitmap?> RenderToBitmapAsync(int? drawOperationIndex)
{
if (_snapshot.IsDisposed || _drawList == null || _drawList.Count == 0)
return Task.FromResult<Bitmap?>(null);
return _snapshot.Compositor.InvokeServerJobAsync(() =>
{
using (_snapshot.Compositor.Server.RenderInterface.EnsureCurrent())
{
if (_snapshot.IsDisposed)
return null;
var margin = 20;
var bounds =
drawOperationIndex == null
? _drawList.CalculateBounds()
: _drawList[drawOperationIndex.Value].Item.Bounds;
using (var layer = _snapshot.Compositor.Server.RenderInterface.Value.CreateLayer(bounds.Size.Inflate(new Thickness(margin)), 1))
{
var visualBrushHelper = new CompositorDrawingContextProxy.VisualBrushRenderer();
using (var ctx = layer.CreateDrawingContext(visualBrushHelper))
{
var proxy = new CompositorDrawingContextProxy(ctx, visualBrushHelper);
proxy.PostTransform = Matrix.CreateTranslation(-bounds.Left + margin, -bounds.Top + margin);
proxy.Transform = Matrix.Identity;
if (drawOperationIndex != null)
_drawList[drawOperationIndex.Value].Item.Render(proxy);
else
foreach (var item in _drawList)
item.Item.Render(proxy);
}
using (var ms = new MemoryStream())
{
layer.Save(ms);
ms.Position = 0;
return new Bitmap(
RefCountable.Create(AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>()
.LoadBitmap(ms)));
}
}
}
});
}
internal void Destroy()
{
_drawList?.Dispose();
_drawList = null;
foreach (var ch in Children)
ch.Destroy();
}
}

16
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs

@ -50,6 +50,22 @@ internal class CompositionDrawList : PooledList<IRef<IDrawOperation>>
canvas.VisualBrushDrawList = null;
}
public Rect CalculateBounds()
{
var rect = default(Rect);
foreach (var cmd in this)
rect = rect.Union(cmd.Item.Bounds);
return rect;
}
public bool HitTest(Point pt)
{
foreach (var op in this)
if (op.Item.HitTest(pt))
return true;
return false;
}
}
/// <summary>

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

@ -0,0 +1,8 @@
using System.Collections.Generic;
namespace Avalonia.Rendering.Composition;
internal interface ICompositionVisualWithDiagnosticsInfo
{
public string? Name { get; }
}

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

@ -0,0 +1,8 @@
using Avalonia.Rendering.Composition.Drawing;
namespace Avalonia.Rendering.Composition;
internal interface ICompositionVisualWithDrawList
{
CompositionDrawList? DrawList { get; }
}

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

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Avalonia.Collections.Pooled;
using Avalonia.Platform;
@ -14,7 +15,8 @@ namespace Avalonia.Rendering.Composition.Server;
/// <summary>
/// Server-side counterpart of <see cref="CompositionDrawListVisual"/>
/// </summary>
internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisual
internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisual,
ICompositionVisualWithDrawList, ICompositionVisualWithDiagnosticsInfo
{
#if DEBUG
// This is needed for debugging purposes so we could see inspect the associated visual from debugger
@ -24,6 +26,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua
public ServerCompositionDrawListVisual(ServerCompositor compositor, Visual v) : base(compositor)
{
Name = v.GetType().Name;
#if DEBUG
UiVisual = v;
#endif
@ -31,22 +34,8 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua
Rect? _contentBounds;
public override Rect OwnContentBounds
{
get
{
if (_contentBounds == null)
{
var rect = default(Rect);
if(_renderCommands!=null)
foreach (var cmd in _renderCommands)
rect = rect.Union(cmd.Item.Bounds);
_contentBounds = rect;
}
return _contentBounds.Value;
}
}
public override Rect OwnContentBounds =>
(_contentBounds ??= _renderCommands?.CalculateBounds()) ?? default;
protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt)
{
@ -74,4 +63,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua
return UiVisual.GetType().ToString();
}
#endif
public CompositionDrawList? DrawList => _renderCommands;
public string? Name { get; }
}

5
src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs

@ -52,6 +52,11 @@ namespace Avalonia.Rendering.Composition.Server
return default;
}
public virtual void PopulateDiagnosticProperties(Dictionary<string, object?> properties)
{
}
ExpressionVariant IExpressionObject.GetProperty(string name) => GetPropertyForAnimation(name);
public void Activate()

34
src/Avalonia.Diagnostics/Diagnostics/DevTools.cs

@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Diagnostics.Views;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.Reactive;
using Avalonia.Rendering.Composition;
namespace Avalonia.Diagnostics
{
@ -143,5 +145,37 @@ namespace Avalonia.Diagnostics
}
return Disposable.Create(() => window?.Close());
}
public static async void ShowCompositionSnapshot(TopLevel tl)
{
var visual = ElementComposition.GetElementVisual(tl);
if (visual == null)
return;
var snapshot = await CompositionTreeSnapshot.TakeAsync(visual);
if (snapshot == null)
return;
new Window
{
Content = new CompositionTreeSnapshotView
{
DataContext = new CompositionTreeSnapshotViewModel(tl, snapshot)
}
}.Show();
}
public static void AttachCompositionSnapshot(TopLevel tl)
{
tl.AddHandler(InputElement.KeyDownEvent, (_, e) =>
{
if (e.Key == Key.F11)
{
ShowCompositionSnapshot(tl);
e.Handled = true;
}
}, RoutingStrategies.Tunnel);
}
}
}

135
src/Avalonia.Diagnostics/Diagnostics/ViewModels/CompositionTreeSnapshotPageViewModel.cs

@ -0,0 +1,135 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
namespace Avalonia.Diagnostics.ViewModels;
internal class CompositionTreeSnapshotViewModel : ViewModelBase, IDisposable
{
private CompositionTreeSnapshotItemViewModel? _selectedNode;
private bool _isPicking;
private CompositionTreeSnapshot _snapshot;
public CompositionTreeSnapshot Snapshot
{
get => _snapshot;
private set => RaiseAndSetIfChanged(ref _snapshot, value);
}
public TopLevel TopLevel { get; }
public AvaloniaList<CompositionTreeSnapshotItemViewModel> RootItems { get; }
public bool IsPicking
{
get => _isPicking;
set => RaiseAndSetIfChanged(ref _isPicking, value);
}
public void PickItem(Point pos)
{
IsPicking = false;
var item = Snapshot.HitTest(pos);
if (item != null)
SelectedNode = ExpandToItem(RootItems![0], item);
}
private CompositionTreeSnapshotItemViewModel? ExpandToItem(CompositionTreeSnapshotItemViewModel currentVm, CompositionTreeSnapshotItem item)
{
if (currentVm.Item == item)
{
SelectedNode = currentVm;
return currentVm;
}
foreach (var ch in currentVm.Children)
{
var chRes = ExpandToItem(ch, item);
if (chRes != null)
{
currentVm.IsExpanded = true;
return chRes;
}
}
return null;
}
public CompositionTreeSnapshotItemViewModel? SelectedNode
{
get => _selectedNode;
set => RaiseAndSetIfChanged(ref _selectedNode, value);
}
public CompositionTreeSnapshotViewModel(TopLevel topLevel, CompositionTreeSnapshot snapshot)
{
_snapshot = snapshot;
TopLevel = topLevel;
RootItems = new(new CompositionTreeSnapshotItemViewModel(snapshot.Root));
}
public void Dispose()
{
IsPicking = false;
Snapshot.DisposeAsync();
}
}
internal class CompositionTreeSnapshotItemViewModel : ViewModelBase
{
private bool _isExpanded = false;
private CompositionTreeSnapshotItemPropertyViewModel? _selectedProperty;
private int _selectedDrawOperationIndex;
public CompositionTreeSnapshotItem Item { get; }
public IReadOnlyList<CompositionTreeSnapshotItemViewModel> Children { get; }
public IAvaloniaReadOnlyList<CompositionTreeSnapshotItemPropertyViewModel> Properties { get; }
public CompositionTreeSnapshotItemPropertyViewModel? SelectedProperty
{
get => _selectedProperty;
set => RaiseAndSetIfChanged(ref _selectedProperty, value);
}
public int SelectedDrawOperationIndex
{
get => _selectedDrawOperationIndex;
set => RaiseAndSetIfChanged(ref _selectedDrawOperationIndex, value);
}
public bool IsExpanded
{
get => _isExpanded;
set => RaiseAndSetIfChanged(ref _isExpanded, value);
}
public CompositionTreeSnapshotItemViewModel(CompositionTreeSnapshotItem item)
{
Item = item;
Children = item.Children.Select(x => new CompositionTreeSnapshotItemViewModel(x)).ToList();
Properties =
new AvaloniaList<CompositionTreeSnapshotItemPropertyViewModel>(item.Properties
.Select(x => new CompositionTreeSnapshotItemPropertyViewModel(x.Key, x.Value))
.OrderBy(x => x.Name));
}
}
internal class CompositionTreeSnapshotItemPropertyViewModel : ViewModelBase
{
public string Name { get; }
public object? Value { get; }
public CompositionTreeSnapshotItemPropertyViewModel(string name, object? value)
{
Name = name;
Value = value;
}
}

40
src/Avalonia.Diagnostics/Diagnostics/Views/CompositionTreeSnapshotItemView.axaml

@ -0,0 +1,40 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Avalonia.Diagnostics.Views.CompositionTreeSnapshotItemView"
x:DataType="vm:CompositionTreeSnapshotItemViewModel">
<Grid ColumnDefinitions="0.5*,4,0.5*">
<DataGrid
Items="{Binding Properties}"
BorderThickness="0"
RowBackground="Transparent"
SelectedItem="{Binding SelectedProperty, Mode=TwoWay}"
CanUserResizeColumns="true">
<DataGrid.Columns>
<DataGridTextColumn Header="Property" Binding="{Binding Name}" IsReadOnly="True"
x:DataType="vm:CompositionTreeSnapshotItemPropertyViewModel" />
<DataGridTextColumn Header="Value" Binding="{Binding Value}"
x:DataType="vm:CompositionTreeSnapshotItemPropertyViewModel" />
</DataGrid.Columns>
<DataGrid.Styles>
<Style Selector="DataGridRow TextBox">
<Setter Property="SelectionBrush" Value="LightBlue" />
</Style>
</DataGrid.Styles>
</DataGrid>
<GridSplitter Grid.Column="1"/>
<Grid Grid.Column="2" RowDefinitions="*,4,*">
<DockPanel>
<Button DockPanel.Dock="Bottom" Click="ResetDrawOperationIndex">Show all</Button>
<ListBox Items="{Binding Item.DrawOperations}" SelectedIndex="{Binding SelectedDrawOperationIndex}"/>
</DockPanel>
<GridSplitter Grid.Row="1"/>
<Image Grid.Row="2" x:Name="Image" Stretch="Uniform"/>
</Grid>
</Grid>
</UserControl>

65
src/Avalonia.Diagnostics/Diagnostics/Views/CompositionTreeSnapshotItemView.axaml.cs

@ -0,0 +1,65 @@
using System;
using System.ComponentModel;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
namespace Avalonia.Diagnostics.Views;
internal partial class CompositionTreeSnapshotItemView : TypedUserControl<CompositionTreeSnapshotItemViewModel>
{
private readonly Image _image;
public CompositionTreeSnapshotItemView()
{
InitializeComponent();
_image = this.FindControl<Image>("Image")!;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
protected override void Subscribe()
{
UpdateImage();
base.Subscribe();
}
protected override void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Model.SelectedDrawOperationIndex))
UpdateImage();
base.OnModelPropertyChanged(sender, e);
}
private void ResetDrawOperationIndex(object? sender, RoutedEventArgs e)
{
if (Model != null)
Model.SelectedDrawOperationIndex = -1;
}
async void UpdateImage()
{
var drawForModel = Model;
var drawForIndex = Model!.SelectedDrawOperationIndex;
await Task.Delay(100);
if (drawForModel != Model || drawForIndex != drawForModel.SelectedDrawOperationIndex)
return;
var image = await Model!.Item.RenderToBitmapAsync(Model.SelectedDrawOperationIndex == -1 ? null : Model.SelectedDrawOperationIndex);
if (drawForModel != Model || drawForIndex != drawForModel.SelectedDrawOperationIndex)
{
image?.Dispose();
return;
}
var oldSource = _image.Source;
_image.Source = image;
(oldSource as IDisposable)?.Dispose();
}
}

52
src/Avalonia.Diagnostics/Diagnostics/Views/CompositionTreeSnapshotView.axaml

@ -0,0 +1,52 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
xmlns:views="clr-namespace:Avalonia.Diagnostics.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Avalonia.Diagnostics.Views.CompositionTreeSnapshotView"
x:DataType="vm:CompositionTreeSnapshotViewModel">
<UserControl.Styles>
<SimpleTheme />
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Simple.xaml" />
<StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/ThicknessEditor.axaml" />
<StyleInclude Source="avares://Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml" />
</UserControl.Styles>
<Grid>
<DockPanel>
<StackPanel Spacing="4" DockPanel.Dock="Top" Orientation="Horizontal">
<ToggleButton IsChecked="{Binding IsPicking}">Pick element</ToggleButton>
<Button Click="FreezeMe">Freeze Me</Button>
</StackPanel>
<Grid ColumnDefinitions="0.35*,4,0.65*">
<TreeView
x:DataType="vm:CompositionTreeSnapshotViewModel"
BorderThickness="0"
Items="{Binding Path=RootItems}"
SelectedItem="{Binding SelectedNode, Mode=TwoWay}"
SelectionMode="Single">
<TreeView.DataTemplates>
<TreeDataTemplate DataType="vm:CompositionTreeSnapshotItemViewModel"
ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Item.Name}" />
</TreeDataTemplate>
</TreeView.DataTemplates>
<TreeView.Styles>
<Style Selector="TreeViewItem" x:DataType="vm:CompositionTreeSnapshotItemViewModel">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="Background" Value="Transparent" />
</Style>
</TreeView.Styles>
</TreeView>
<GridSplitter Background="{DynamicResource ThemeControlMidBrush}" Width="1" Grid.Column="1" />
<views:CompositionTreeSnapshotItemView DataContext="{Binding SelectedNode}" Grid.Column="2" />
</Grid>
</DockPanel>
<Border IsVisible="{Binding IsPicking}" Background="Black" PointerPressed="PickElement">
<Image x:Name="ElementPicker" Source="{Binding Snapshot.Bitmap}" Stretch="Uniform"/>
</Border>
</Grid>
</UserControl>

55
src/Avalonia.Diagnostics/Diagnostics/Views/CompositionTreeSnapshotView.axaml.cs

@ -0,0 +1,55 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Threading;
namespace Avalonia.Diagnostics.Views;
internal class CompositionTreeSnapshotView : TypedUserControl<CompositionTreeSnapshotViewModel>
{
private readonly Image _elementPicker;
public CompositionTreeSnapshotView()
{
AvaloniaXamlLoader.Load(this);
_elementPicker = this.FindControl<Image>("ElementPicker")!;
}
protected override void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(CompositionTreeSnapshotViewModel.SelectedNode))
DispatcherTimer.RunOnce(() =>
{
var item = this.GetLogicalDescendants().OfType<TreeViewItem>()
.FirstOrDefault(i => i.DataContext == Model!.SelectedNode);
item?.BringIntoView();
}, TimeSpan.FromMilliseconds(20));
}
protected override void Unsubscribe()
{
Model!.IsPicking = false;
base.Unsubscribe();
}
private void PickElement(object sender, PointerPressedEventArgs e)
{
var pos = e.GetPosition(_elementPicker);
var scale = Stretch.Uniform.CalculateScaling(Bounds.Size, _elementPicker.Bounds.Size);
pos = new Point(pos.X * scale.X, pos.Y * scale.Y);
Model?.PickItem(pos);
}
private void FreezeMe(object? sender, RoutedEventArgs e)
{
Model?.RootItems?.Clear();
}
}

66
src/Avalonia.Diagnostics/Diagnostics/Views/TypedUserControl.cs

@ -0,0 +1,66 @@
using System;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.LogicalTree;
using Avalonia.Styling;
using Avalonia.Threading;
namespace Avalonia.Diagnostics.Views;
internal class TypedUserControl<T> : UserControl, IStyleable where T : class, INotifyPropertyChanged
{
Type IStyleable.StyleKey => typeof(UserControl);
protected T? Model { get; private set; }
protected virtual void OnModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
}
protected virtual void Subscribe()
{
}
protected virtual void Unsubscribe()
{
}
void UpdateSubscriptions()
{
if (Model != null)
{
Model.PropertyChanged -= OnModelPropertyChanged;
Unsubscribe();
}
Model = IsAttachedToVisualTree ? DataContext as T : null;
if (Model != null)
{
Model.PropertyChanged += OnModelPropertyChanged;
Subscribe();
}
}
protected override void OnDataContextChanged(EventArgs e)
{
UpdateSubscriptions();
base.OnDataContextChanged(e);
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
UpdateSubscriptions();
base.OnAttachedToVisualTree(e);
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
UpdateSubscriptions();
base.OnDetachedFromVisualTree(e);
}
}

3
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -52,6 +52,9 @@ namespace Avalonia.Headless
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces) => new HeadlessRenderTarget();
public bool IsLost => false;
public IDrawingContextLayerImpl CreateLayer(Size size, double scaling) => new HeadlessBitmapStub(size, new Vector(96, 96));
public object TryGetFeature(Type featureType) => null;
public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi)

8
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -38,7 +38,7 @@ namespace Avalonia.Skia
private readonly SKPaint _fillPaint = SKPaintCache.Get();
private readonly SKPaint _boxShadowPaint = SKPaintCache.Get();
private static SKShader s_acrylicNoiseShader;
private readonly ISkiaGpuRenderSession _session;
private readonly GRSurfaceOrigin? _surfaceOrigin;
private bool _leased = false;
/// <summary>
@ -81,7 +81,7 @@ namespace Avalonia.Skia
/// </summary>
public ISkiaGpu Gpu;
public ISkiaGpuRenderSession CurrentSession;
public GRSurfaceOrigin? SurfaceOrigin;
}
class SkiaLeaseFeature : ISkiaSharpApiLeaseFeature
@ -143,7 +143,7 @@ namespace Avalonia.Skia
Surface = createInfo.Surface;
Canvas = createInfo.Canvas ?? createInfo.Surface?.Canvas;
_session = createInfo.CurrentSession;
_surfaceOrigin = createInfo.SurfaceOrigin;
if (Canvas == null)
{
@ -1189,7 +1189,7 @@ namespace Avalonia.Skia
DisableTextLcdRendering = !_canTextUseLcdRendering,
GrContext = _grContext,
Gpu = _gpu,
Session = _session,
SurfaceOrigin = _surfaceOrigin,
DisableManualFbo = !isLayer,
};

9
src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs

@ -10,6 +10,11 @@ namespace Avalonia.Skia
/// </summary>
public interface ISkiaGpu : IPlatformGraphicsContext
{
/// <summary>
/// Skia's GrContext
/// </summary>
GRContext GrContext { get; }
/// <summary>
/// Attempts to create custom render target from given surfaces.
/// </summary>
@ -21,8 +26,8 @@ namespace Avalonia.Skia
/// Creates an offscreen render target surface
/// </summary>
/// <param name="size">size in pixels.</param>
/// <param name="session">An optional custom render session.</param>
ISkiaSurface TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session);
/// <param name="surfaceOrigin">The expected surface origin</param>
ISkiaSurface TryCreateSurface(PixelSize size, GRSurfaceOrigin? surfaceOrigin);
}
public interface ISkiaSurface : IDisposable

6
src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs

@ -75,7 +75,7 @@ namespace Avalonia.Skia
return null;
}
public ISkiaSurface TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session)
public ISkiaSurface TryCreateSurface(PixelSize size, GRSurfaceOrigin? surfaceOrigin)
{
// Only windows platform needs our FBO trickery
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
@ -90,8 +90,8 @@ namespace Avalonia.Skia
return null;
try
{
var surface = new FboSkiaSurface(this, _grContext, _glContext, size,
session?.SurfaceOrigin ?? GRSurfaceOrigin.TopLeft);
var surface = new FboSkiaSurface(this, _grContext, _glContext, size,
surfaceOrigin ?? GRSurfaceOrigin.TopLeft);
_canCreateSurfaces = true;
return surface;
}

2
src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs

@ -34,7 +34,7 @@ namespace Avalonia.Skia
VisualBrushRenderer = visualBrushRenderer,
DisableTextLcdRendering = true,
Gpu = _skiaGpu,
CurrentSession = session
SurfaceOrigin = session.SurfaceOrigin
};
return new DrawingContextImpl(nfo, session);

22
src/Skia/Avalonia.Skia/SkiaBackendContext.cs

@ -44,6 +44,28 @@ internal class SkiaContext : IPlatformRenderInterfaceContext
}
public bool IsLost => _gpu.IsLost;
public IDrawingContextLayerImpl CreateLayer(Size size, double scaling)
{
var dpi = new Vector(96 * scaling, 96 * scaling);
var pixelSize = PixelSize.FromSizeWithDpi(size, dpi);
using (_gpu?.EnsureCurrent())
{
var createInfo = new SurfaceRenderTarget.CreateInfo
{
Width = pixelSize.Width,
Height = pixelSize.Height,
Dpi = dpi,
Format = null,
DisableTextLcdRendering = false,
GrContext = _gpu?.GrContext,
Gpu = _gpu,
DisableManualFbo = true
};
return new SurfaceRenderTarget(createInfo);
}
}
public object TryGetFeature(Type featureType) => _gpu?.TryGetFeature(featureType);
}

6
src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs

@ -52,7 +52,7 @@ namespace Avalonia.Skia
_gpu = createInfo.Gpu;
if (!createInfo.DisableManualFbo)
_surface = _gpu?.TryCreateSurface(PixelSize, createInfo.Session);
_surface = _gpu?.TryCreateSurface(PixelSize, createInfo.SurfaceOrigin);
if (_surface == null)
_surface = new SkiaSurfaceWrapper(CreateSurface(createInfo.GrContext, PixelSize.Width, PixelSize.Height,
createInfo.Format));
@ -222,9 +222,9 @@ namespace Avalonia.Skia
public ISkiaGpu Gpu;
public ISkiaGpuRenderSession Session;
public bool DisableManualFbo;
public GRSurfaceOrigin? SurfaceOrigin;
}
}
}

7
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@ -228,6 +228,13 @@ namespace Avalonia.Direct2D1
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces) => _platform.CreateRenderTarget(surfaces);
public bool IsLost => false;
public IDrawingContextLayerImpl CreateLayer(Size size, double scaling)
{
var platform = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
var dpi = new Vector(96 * scaling, 96 * scaling);
var pixelSize = PixelSize.FromSizeWithDpi(size, dpi);
return (IDrawingContextLayerImpl)platform.CreateRenderTargetBitmap(pixelSize, dpi);
}
}
public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) =>

28
src/tools/DevGenerators/CompositionGenerator/Generator.cs

@ -139,6 +139,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
var resetBody = Block();
var startAnimationBody = Block();
var serverGetPropertyBody = Block();
var serverPopulateDiagnosticsPropertiesBody = Block();
var serverGetCompositionPropertyBody = Block();
var serializeMethodBody = SerializeChangesPrologue(cl);
var deserializeMethodBody = DeserializeChangesPrologue(cl);
@ -237,7 +238,8 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
startAnimationBody = ApplyStartAnimation(startAnimationBody, cl, prop);
}
serverPopulateDiagnosticsPropertiesBody =
ApplyPopulateDiagnosticsProperty(serverPopulateDiagnosticsPropertiesBody, prop);
serverGetPropertyBody = ApplyGetProperty(serverGetPropertyBody, prop);
serverGetCompositionPropertyBody = ApplyGetProperty(serverGetCompositionPropertyBody, prop, CompositionPropertyField(prop));
@ -288,6 +290,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator
server = WithGetPropertyForAnimation(server, serverGetPropertyBody);
server = WithGetCompositionProperty(server, serverGetCompositionPropertyBody);
server = WithPopulateDiagnosticProperties(server, serverPopulateDiagnosticsPropertiesBody);
if(cl.Implements.Count > 0)
foreach (var impl in cl.Implements)
@ -445,6 +448,15 @@ return;
return body;
}
BlockSyntax ApplyPopulateDiagnosticsProperty(BlockSyntax body, GProperty prop)
{
if (_objects.Contains(prop.Type.Trim('?')))
return body.AddStatements(
ParseStatement($"diagnostics[\"{prop.Name}\"] = {prop.Name} == null ? \"Null\" : \"Not null\";"));
return body.AddStatements(
ParseStatement($"diagnostics[\"{prop.Name}\"] = {prop.Name};"));
}
private static BlockSyntax SerializeChangesPrologue(GClass cl)
{
@ -540,6 +552,20 @@ var changed = reader.Read<{ChangedFieldsTypeName(cl)}>();
return cl.AddMembers(method);
}
static ClassDeclarationSyntax WithPopulateDiagnosticProperties(ClassDeclarationSyntax cl, BlockSyntax body)
{
if (body.Statements.Count == 0)
return cl;
body = body.AddStatements(
ParseStatement("base.PopulateDiagnosticProperties(diagnostics);"));
var method = ((MethodDeclarationSyntax)ParseMemberDeclaration(
$"public override void PopulateDiagnosticProperties(Dictionary<string, object?> diagnostics){{}}")!)
.WithBody(body);
return cl.AddMembers(method);
}
static ClassDeclarationSyntax WithStartAnimation(ClassDeclarationSyntax cl, BlockSyntax body)
{
body = body.AddStatements(

4
tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs

@ -17,6 +17,10 @@ namespace Avalonia.Base.UnitTests.VisualTree
}
public bool IsLost => false;
public IDrawingContextLayerImpl CreateLayer(Size size, double scaling)
{
throw new NotImplementedException();
}
public object TryGetFeature(Type featureType) => null;

4
tests/Avalonia.Benchmarks/NullRenderingPlatform.cs

@ -48,6 +48,10 @@ namespace Avalonia.Benchmarks
}
public bool IsLost => false;
public IDrawingContextLayerImpl CreateLayer(Size size, double scaling)
{
throw new NotImplementedException();
}
public object TryGetFeature(Type featureType) => null;

33
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -36,22 +36,31 @@ namespace Avalonia.UnitTests
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
{
var m = new Mock<IDrawingContextImpl>();
m.Setup(c => c.CreateLayer(It.IsAny<Size>()))
.Returns(() =>
{
var r = new Mock<IDrawingContextLayerImpl>();
r.Setup(r => r.CreateDrawingContext(It.IsAny<IVisualBrushRenderer>()))
.Returns(CreateDrawingContext(null));
return r.Object;
}
);
return m.Object;
return MockPlatformRenderInterface.CreateDrawingContext(visualBrushRenderer);
}
public bool IsCorrupted => false;
}
public static IDrawingContextLayerImpl CreateLayerShared(Size size, double scaling)
{
var r = new Mock<IDrawingContextLayerImpl>();
r.Setup(r => r.CreateDrawingContext(It.IsAny<IVisualBrushRenderer>()))
.Returns(CreateDrawingContext(null));
return r.Object;
}
public IDrawingContextLayerImpl CreateLayer(Size size, double scaling) => CreateLayerShared(size, scaling);
private static IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
{
var m = new Mock<IDrawingContextImpl>();
m.Setup(c => c.CreateLayer(It.IsAny<Size>()))
.Returns(() => CreateLayerShared(default, 0));
return m.Object;
}
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
{

15
tests/Avalonia.UnitTests/TestRoot.cs

@ -63,21 +63,6 @@ namespace Avalonia.UnitTests
IStyleHost IStyleHost.StylingParent => StylingParent;
public IRenderTarget CreateRenderTarget()
{
var dc = new Mock<IDrawingContextImpl>();
dc.Setup(x => x.CreateLayer(It.IsAny<Size>())).Returns(() =>
{
var layerDc = new Mock<IDrawingContextImpl>();
var layer = new Mock<IDrawingContextLayerImpl>();
layer.Setup(x => x.CreateDrawingContext(It.IsAny<IVisualBrushRenderer>())).Returns(layerDc.Object);
return layer.Object;
});
var result = new Mock<IRenderTarget>();
result.Setup(x => x.CreateDrawingContext(It.IsAny<IVisualBrushRenderer>())).Returns(dc.Object);
return result.Object;
}
public void Invalidate(Rect rect)
{

Loading…
Cancel
Save