Browse Source

remove deferred renderer

feature/remove-legacy-renderers
Dan Walmsley 3 years ago
parent
commit
aabb69fb9d
  1. 1
      samples/ControlCatalog.NetCore/Program.cs
  2. 4
      samples/RenderDemo/App.xaml.cs
  3. 13
      src/Android/Avalonia.Android/AndroidPlatform.cs
  4. 7
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  5. 733
      src/Avalonia.Base/Rendering/DeferredRenderer.cs
  6. 70
      src/Avalonia.Base/Rendering/RenderLayers.cs
  7. 482
      src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  8. 24
      src/Avalonia.Base/Rendering/SceneGraph/ISceneBuilder.cs
  9. 106
      src/Avalonia.Base/Rendering/SceneGraph/IVisualNode.cs
  10. 350
      src/Avalonia.Base/Rendering/SceneGraph/Scene.cs
  11. 485
      src/Avalonia.Base/Rendering/SceneGraph/SceneBuilder.cs
  12. 74
      src/Avalonia.Base/Rendering/SceneGraph/SceneLayer.cs
  13. 206
      src/Avalonia.Base/Rendering/SceneGraph/SceneLayers.cs
  14. 448
      src/Avalonia.Base/Rendering/SceneGraph/VisualNode.cs
  15. 1
      src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs
  16. 5
      src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  17. 5
      src/Avalonia.Headless/HeadlessWindowImpl.cs
  18. 6
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  19. 14
      src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs
  20. 17
      src/Avalonia.Native/WindowImplBase.cs
  21. 14
      src/Avalonia.X11/X11Platform.cs
  22. 9
      src/Avalonia.X11/X11Window.cs
  23. 19
      src/Windows/Avalonia.Win32/Win32Platform.cs
  24. 12
      src/Windows/Avalonia.Win32/WindowImpl.cs
  25. 779
      tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests.cs
  26. 577
      tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs
  27. 225
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs
  28. 1087
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs
  29. 258
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs
  30. 35
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneLayersTests.cs
  31. 33
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneTests.cs
  32. 123
      tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/VisualNodeTests.cs

1
samples/ControlCatalog.NetCore/Program.cs

@ -56,7 +56,6 @@ namespace ControlCatalog.NetCore
.UseHeadless(new AvaloniaHeadlessPlatformOptions
{
UseHeadlessDrawing = true,
UseCompositor = true
})
.AfterSetup(_ =>
{

4
samples/RenderDemo/App.xaml.cs

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

13
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -53,20 +53,15 @@ namespace Avalonia.Android
{
EglPlatformOpenGlInterface.TryInitialize();
}
if (Options.UseCompositor)
{
Compositor = new Compositor(
AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(),
AvaloniaLocator.Current.GetService<IPlatformOpenGlInterface>());
}
Compositor = new Compositor(
AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(),
AvaloniaLocator.Current.GetService<IPlatformOpenGlInterface>());
}
}
public sealed class AndroidPlatformOptions
{
public bool UseDeferredRendering { get; set; } = false;
public bool UseGpu { get; set; } = true;
public bool UseCompositor { get; set; } = true;
}
}

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

@ -89,12 +89,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)
: AndroidPlatform.Options.UseDeferredRendering
? new DeferredRenderer(root, AvaloniaLocator.Current.GetRequiredService<IRenderLoop>()) { RenderOnlyOnRenderThread = true }
: new ImmediateRenderer(root);
public IRenderer CreateRenderer(IRenderRoot root) => new CompositingRenderer(root, AndroidPlatform.Compositor);
public virtual void Hide()
{

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

@ -1,733 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
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 IVisual _root;
private readonly ISceneBuilder _sceneBuilder;
private bool _running;
private bool _disposed;
private volatile IRef<Scene>? _scene;
private DirtyVisuals? _dirty;
private HashSet<IVisual>? _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;
/// <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="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,
ISceneBuilder? sceneBuilder = null,
IDispatcher? dispatcher = null,
IDeferredRendererLock? rendererLock = null) : base(true)
{
_dispatcher = dispatcher ?? Dispatcher.UIThread;
_root = root ?? throw new ArgumentNullException(nameof(root));
_sceneBuilder = sceneBuilder ?? new SceneBuilder();
Layers = new RenderLayers();
_renderLoop = renderLoop;
_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(
IVisual 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(IVisual 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(IVisual 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<IVisual> HitTest(Point p, IVisual root, Func<IVisual, 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<IVisual>();
}
/// <inheritdoc/>
public IVisual? HitTestFirst(Point p, IVisual root, Func<IVisual, 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;
}
}
}
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);
}
}
Scene? TryGetChildScene(IRef<IDrawOperation>? op) => (op?.Item as BrushDrawOperation)?.Aux as Scene;
/// <inheritdoc/>
Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
{
return TryGetChildScene(_currentDraw)?.Size ?? Size.Empty;
}
/// <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
{
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, IVisual? layer, Rect clipBounds)
{
if (layer == null || node.LayerRoot == layer)
{
clipBounds = node.ClipBounds.Intersect(clipBounds);
if (!clipBounds.IsEmpty && 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 as IRenderTargetWithCorruptionInfo)?.IsCorrupted == true)
{
RenderTarget!.Dispose();
RenderTarget = null;
}
if (RenderTarget == null)
{
RenderTarget = ((IRenderRoot)_root).CreateRenderTarget();
}
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));
var scene = sceneRef.Item;
if (_dirty == null)
{
_dirty = new DirtyVisuals();
_recalculateChildren = new HashSet<IVisual>();
_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, $"frame-{id}-layer-{index++}.png");
layer.Bitmap.Item.Save(fileName);
}
}
}
}

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

@ -1,70 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.VisualTree;
namespace Avalonia.Rendering
{
public class RenderLayers : IEnumerable<RenderLayer>
{
private readonly List<RenderLayer> _inner = new List<RenderLayer>();
private readonly Dictionary<IVisual, RenderLayer> _index = new Dictionary<IVisual, RenderLayer>();
public int Count => _inner.Count;
public RenderLayer this[IVisual 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(IVisual layerRoot, [NotNullWhen(true)] out RenderLayer? value)
{
return _index.TryGetValue(layerRoot, out value);
}
public IEnumerator<RenderLayer> GetEnumerator() => _inner.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

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

@ -1,482 +0,0 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Utilities;
using Avalonia.Media.Imaging;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// A drawing context which builds a scene graph.
/// </summary>
internal class DeferredDrawingContextImpl : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport
{
private readonly ISceneBuilder _sceneBuilder;
private VisualNode? _node;
private int _childIndex;
private int _drawOperationindex;
/// <summary>
/// Initializes a new instance of the <see cref="DeferredDrawingContextImpl"/> class.
/// </summary>
/// <param name="sceneBuilder">
/// A scene builder used for constructing child scenes for visual brushes.
/// </param>
/// <param name="layers">The scene layers.</param>
public DeferredDrawingContextImpl(ISceneBuilder sceneBuilder, SceneLayers layers)
{
_sceneBuilder = sceneBuilder;
Layers = layers;
}
/// <inheritdoc/>
public Matrix Transform { get; set; } = Matrix.Identity;
/// <summary>
/// Gets the layers in the scene being built.
/// </summary>
public SceneLayers Layers { get; }
/// <summary>
/// Informs the drawing context of the visual node that is about to be rendered.
/// </summary>
/// <param name="node">The visual node.</param>
/// <returns>
/// An object which when disposed will commit the changes to visual node.
/// </returns>
public UpdateState BeginUpdate(VisualNode node)
{
_ = node ?? throw new ArgumentNullException(nameof(node));
if (_node != null)
{
if (_childIndex < _node.Children.Count)
{
_node.ReplaceChild(_childIndex, node);
}
else
{
_node.AddChild(node);
}
++_childIndex;
}
var state = new UpdateState(this, _node, _childIndex, _drawOperationindex);
_node = node;
_childIndex = _drawOperationindex = 0;
return state;
}
/// <inheritdoc/>
public void Clear(Color color)
{
// Cannot clear a deferred scene.
}
/// <inheritdoc/>
public void Dispose()
{
// Nothing to do here since we allocate no unmanaged resources.
}
/// <summary>
/// Removes any remaining drawing operations from the visual node.
/// </summary>
/// <remarks>
/// Drawing operations are updated in place, overwriting existing drawing operations if
/// they are different. Once drawing has completed for the current visual node, it is
/// possible that there are stale drawing operations at the end of the list. This method
/// trims these stale drawing operations.
/// </remarks>
public void TrimChildren()
{
_node!.TrimChildren(_childIndex);
}
/// <inheritdoc/>
public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry)
{
var next = NextDrawAs<GeometryNode>();
if (next == null || !next.Item.Equals(Transform, brush, pen, geometry))
{
Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush)));
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
{
var next = NextDrawAs<ImageNode>();
if (next == null || !next.Item.Equals(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode))
{
Add(new ImageNode(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode));
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect)
{
// This method is currently only used to composite layers so shouldn't be called here.
throw new NotSupportedException();
}
/// <inheritdoc/>
public void DrawLine(IPen pen, Point p1, Point p2)
{
var next = NextDrawAs<LineNode>();
if (next == null || !next.Item.Equals(Transform, pen, p1, p2))
{
Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush)));
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect,
BoxShadows boxShadows = default)
{
var next = NextDrawAs<RectangleNode>();
if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows))
{
Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush)));
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect)
{
var next = NextDrawAs<ExperimentalAcrylicNode>();
if (next == null || !next.Item.Equals(Transform, material, rect))
{
Add(new ExperimentalAcrylicNode(Transform, material, rect));
}
else
{
++_drawOperationindex;
}
}
public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect)
{
var next = NextDrawAs<EllipseNode>();
if (next == null || !next.Item.Equals(Transform, brush, pen, rect))
{
Add(new EllipseNode(Transform, brush, pen, rect, CreateChildScene(brush)));
}
else
{
++_drawOperationindex;
}
}
public void Custom(ICustomDrawOperation custom)
{
var next = NextDrawAs<CustomDrawOperation>();
if (next == null || !next.Item.Equals(Transform, custom))
Add(new CustomDrawOperation(custom, Transform));
else
++_drawOperationindex;
}
public object? GetFeature(Type t) => null;
/// <inheritdoc/>
public void DrawGlyphRun(IBrush foreground, GlyphRun 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, IVisual visual);
}
}

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

@ -1,106 +0,0 @@
using System;
using System.Collections.Generic;
using Avalonia.Platform;
using Avalonia.Utilities;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// Represents a node in the low-level scene graph representing an <see cref="IVisual"/>.
/// </summary>
public interface IVisualNode : IDisposable
{
/// <summary>
/// Gets the visual to which the node relates.
/// </summary>
IVisual 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; }
}
}

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

@ -1,350 +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<IVisual, 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(IVisual rootVisual)
: this(
new VisualNode(rootVisual, null),
new Dictionary<IVisual, IVisualNode>(),
new SceneLayers(rootVisual),
0)
{
_index.Add(rootVisual, Root);
}
private Scene(VisualNode root, Dictionary<IVisual, 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<IVisual, 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(IVisual 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<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool>? filter)
{
var node = FindNode(root);
return (node != null) ? new HitTestEnumerable(node, filter, p, Root) : Enumerable.Empty<IVisual>();
}
/// <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 IVisual? HitTestFirst(Point p, IVisual root, Func<IVisual, 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<IVisual, 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 IVisual HitTestFirst(IVisualNode root, Point p, Func<IVisual, bool>? filter)
{
using var enumerator = new HitTestEnumerator(root, filter, p, Root);
enumerator.MoveNext();
return enumerator.Current;
}
private class HitTestEnumerable : IEnumerable<IVisual>
{
private readonly IVisualNode _root;
private readonly Func<IVisual, bool>? _filter;
private readonly IVisualNode _sceneRoot;
private readonly Point _point;
public HitTestEnumerable(IVisualNode root, Func<IVisual, bool>? filter, Point point, IVisualNode sceneRoot)
{
_root = root;
_filter = filter;
_point = point;
_sceneRoot = sceneRoot;
}
public IEnumerator<IVisual> GetEnumerator()
{
return new HitTestEnumerator(_root, _filter, _point, _sceneRoot);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
private struct HitTestEnumerator : IEnumerator<IVisual>
{
private readonly PooledStack<Entry> _nodeStack;
private readonly Func<IVisual, bool>? _filter;
private readonly IVisualNode _sceneRoot;
private IVisual? _current;
private readonly Point _point;
public HitTestEnumerator(IVisualNode root, Func<IVisual, 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 IVisual 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);
}
}

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, IVisual 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, IVisual 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, IVisual 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.TransformedBounds = transformed;
if (forceRecurse)
{
var visualChildren = (IList<IVisual>) 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 (IVisual 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 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 = Rect.Empty;
Rect verticalDirtyRect = Rect.Empty;
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, IVisual 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.TransformedBounds = 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 = Rect.Empty;
}
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(IVisual 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;
}
}
}

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

@ -1,74 +0,0 @@
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.VisualTree;
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(IVisual 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 IVisual 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 IVisual _root;
private readonly List<SceneLayer> _inner;
private readonly Dictionary<IVisual, 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(IVisual 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(IVisual root, int capacity)
{
_root = root;
_inner = new List<SceneLayer>(capacity);
_index = new Dictionary<IVisual, 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[IVisual 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(IVisual 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(IVisual 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(IVisual 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(IVisual 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(IVisual 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 System.Reactive.Disposables;
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="IVisual"/>.
/// </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(IVisual visual, IVisualNode? parent)
{
Visual = visual ?? throw new ArgumentNullException(nameof(visual));
Parent = parent;
HasAncestorGeometryClip = parent != null &&
(parent.HasAncestorGeometryClip || parent.GeometryClip != null);
}
/// <inheritdoc/>
public IVisual 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 IVisual? 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.IsEmpty)
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;
}
}
}

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

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

5
src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

@ -72,8 +72,8 @@ 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 +88,6 @@ namespace Avalonia.Headless
public class AvaloniaHeadlessPlatformOptions
{
public bool UseCompositor { get; set; } = true;
public bool UseHeadlessDrawing { get; set; } = true;
}

5
src/Avalonia.Headless/HeadlessWindowImpl.cs

@ -54,10 +54,7 @@ namespace Avalonia.Headless
public Action<Size, PlatformResizeReason> Resized { get; set; }
public Action<double> ScalingChanged { get; set; }
public IRenderer CreateRenderer(IRenderRoot root)
=> AvaloniaHeadlessPlatform.Compositor != null
? new CompositingRenderer(root, AvaloniaHeadlessPlatform.Compositor)
: new DeferredRenderer(root, AvaloniaLocator.Current.GetRequiredService<IRenderLoop>());
public IRenderer CreateRenderer(IRenderRoot root) => new CompositingRenderer(root, AvaloniaHeadlessPlatform.Compositor);
public void Invalidate(Rect rect)
{

6
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -138,11 +138,7 @@ namespace Avalonia.Native
}
}
if (_options.UseDeferredRendering && _options.UseCompositor)
{
Compositor = new Compositor(renderLoop, _platformGl);
}
Compositor = new Compositor(renderLoop, _platformGl);
}
public ITrayIconImpl CreateTrayIcon()

14
src/Avalonia.Native/AvaloniaNativePlatformExtensions.cs

@ -31,20 +31,6 @@ namespace Avalonia
/// </summary>
public class AvaloniaNativePlatformOptions
{
/// <summary>
/// Deferred renderer would be used when set to true. Immediate renderer when set to false. The default value is true.
/// </summary>
/// <remarks>
/// Avalonia has two rendering modes: Immediate and Deferred rendering.
/// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements.
/// </remarks>
public bool UseDeferredRendering { get; set; } = true;
/// <summary>
/// Enables new compositing rendering with UWP-like API
/// </summary>
public bool UseCompositor { get; set; } = true;
/// <summary>
/// Determines whether to use GPU for rendering in your project. The default value is true.
/// </summary>

17
src/Avalonia.Native/WindowImplBase.cs

@ -53,8 +53,7 @@ namespace Avalonia.Native
protected IInputRoot _inputRoot;
IAvnWindowBase _native;
private object _syncRoot = new object();
private bool _deferredRendering = false;
private bool _gpu = false;
private bool _gpu;
private readonly MouseDevice _mouse;
private readonly IKeyboardDevice _keyboard;
private readonly ICursorFactory _cursorFactory;
@ -70,7 +69,6 @@ namespace Avalonia.Native
{
_factory = factory;
_gpu = opts.UseGpu && glFeature != null;
_deferredRendering = opts.UseDeferredRendering;
_keyboard = AvaloniaLocator.Current.GetService<IKeyboardDevice>();
_mouse = new MouseDevice();
@ -372,17 +370,10 @@ namespace Avalonia.Native
if (customRendererFactory != null)
return customRendererFactory.Create(root, loop);
if (_deferredRendering)
return new CompositingRenderer(root, AvaloniaNativePlatform.Compositor)
{
if (AvaloniaNativePlatform.Compositor != null)
return new CompositingRenderer(root, AvaloniaNativePlatform.Compositor)
{
RenderOnlyOnRenderThread = false
};
return new DeferredRenderer(root, loop);
}
return new ImmediateRenderer(root);
RenderOnlyOnRenderThread = false
};
}
public virtual void Dispose()

14
src/Avalonia.X11/X11Platform.cs

@ -106,8 +106,7 @@ namespace Avalonia.X11
if (gl != null)
AvaloniaLocator.CurrentMutable.Bind<IPlatformGpu>().ToConstant(gl);
if (options.UseCompositor)
Compositor = new Compositor(AvaloniaLocator.Current.GetService<IRenderLoop>()!, gl);
Compositor = new Compositor(AvaloniaLocator.Current.GetService<IRenderLoop>()!, gl);
}
@ -227,17 +226,6 @@ namespace Avalonia
/// </summary>
public bool UseDBusFilePicker { get; set; } = true;
/// <summary>
/// Deferred renderer would be used when set to true. Immediate renderer when set to false. The default value is true.
/// </summary>
/// <remarks>
/// Avalonia has two rendering modes: Immediate and Deferred rendering.
/// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements.
/// </remarks>
public bool UseDeferredRendering { get; set; } = true;
public bool UseCompositor { get; set; } = true;
/// <summary>
/// Determines whether to use IME.
/// IME would be enabled by default if the current user input language is one of the following: Mandarin, Japanese, Vietnamese or Korean.

9
src/Avalonia.X11/X11Window.cs

@ -391,14 +391,7 @@ namespace Avalonia.X11
if (customRendererFactory != null)
return customRendererFactory.Create(root, loop);
return _platform.Options.UseDeferredRendering
? _platform.Options.UseCompositor
? new CompositingRenderer(root, this._platform.Compositor)
: new DeferredRenderer(root, loop)
{
RenderOnlyOnRenderThread = true
}
: (IRenderer)new X11ImmediateRendererProxy(root, loop);
return new CompositingRenderer(root, this._platform.Compositor);
}
void OnEvent(ref XEvent ev)

19
src/Windows/Avalonia.Win32/Win32Platform.cs

@ -41,17 +41,6 @@ namespace Avalonia
/// </summary>
public class Win32PlatformOptions
{
/// <summary>
/// Deferred renderer would be used when set to true. Immediate renderer when set to false. The default value is true.
/// </summary>
/// <remarks>
/// Avalonia has two rendering modes: Immediate and Deferred rendering.
/// Immediate re-renders the whole scene when some element is changed on the scene. Deferred re-renders only changed elements.
/// </remarks>
public bool UseDeferredRendering { get; set; } = true;
public bool UseCompositor { get; set; } = true;
/// <summary>
/// Enables ANGLE for Windows. For every Windows version that is above Windows 7, the default is true otherwise it's false.
/// </summary>
@ -123,8 +112,7 @@ namespace Avalonia.Win32
/// Gets the actual WindowsVersion. Same as the info returned from RtlGetVersion.
/// </summary>
public static Version WindowsVersion { get; } = RtlGetVersion();
public static bool UseDeferredRendering => Options.UseDeferredRendering;
internal static bool UseOverlayPopups => Options.OverlayPopups;
public static Win32PlatformOptions Options { get; private set; }
@ -166,9 +154,8 @@ namespace Avalonia.Win32
if (OleContext.Current != null)
AvaloniaLocator.CurrentMutable.Bind<IPlatformDragSource>().ToSingleton<DragSource>();
if (Options.UseCompositor)
Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(), gl);
Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(), gl);
}
public bool HasMessages()

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

@ -546,18 +546,8 @@ namespace Avalonia.Win32
if (customRendererFactory != null)
return customRendererFactory.Create(root, loop);
if (Win32Platform.Compositor != null)
return new CompositingRenderer(root, Win32Platform.Compositor);
return Win32Platform.UseDeferredRendering
? _isUsingComposition
? new DeferredRenderer(root, loop)
{
RenderOnlyOnRenderThread = true
}
: (IRenderer)new DeferredRenderer(root, loop, rendererLock: _rendererLock)
: new ImmediateRenderer(root);
return new CompositingRenderer(root, Win32Platform.Compositor);
}
public void Resize(Size value, PlatformResizeReason reason)

779
tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests.cs

@ -1,779 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Subjects;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Threading;
using Avalonia.UnitTests;
using Avalonia.Media.Imaging;
using Avalonia.VisualTree;
using Moq;
using Xunit;
namespace Avalonia.Base.UnitTests.Rendering
{
public class DeferredRendererTests
{
[Fact]
public void First_Frame_Calls_SceneBuilder_UpdateAll()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var root = new TestRoot();
var sceneBuilder = MockSceneBuilder(root);
CreateTargetAndRunFrame(root, sceneBuilder: sceneBuilder.Object);
sceneBuilder.Verify(x => x.UpdateAll(It.IsAny<Scene>()));
}
}
[Fact]
public void Frame_Does_Not_Call_SceneBuilder_If_No_Dirty_Controls()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var dispatcher = new ImmediateDispatcher();
var loop = new Mock<IRenderLoop>();
var root = new TestRoot();
var sceneBuilder = MockSceneBuilder(root);
var target = new DeferredRenderer(
root,
loop.Object,
sceneBuilder: sceneBuilder.Object);
target.Start();
IgnoreFirstFrame(target, sceneBuilder);
RunFrame(target);
sceneBuilder.Verify(x => x.UpdateAll(It.IsAny<Scene>()), Times.Never);
sceneBuilder.Verify(x => x.Update(It.IsAny<Scene>(), It.IsAny<Visual>()), Times.Never);
}
}
[Fact]
public void Should_Update_Dirty_Controls_In_Order()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var dispatcher = new ImmediateDispatcher();
var loop = new Mock<IRenderLoop>();
Border border;
Decorator decorator;
Canvas canvas;
var root = new TestRoot
{
Child = decorator = new Decorator
{
Child = border = new Border { Child = canvas = new Canvas() }
}
};
var sceneBuilder = MockSceneBuilder(root);
var target = new DeferredRenderer(
root,
loop.Object,
sceneBuilder: sceneBuilder.Object,
dispatcher: dispatcher);
target.Start();
IgnoreFirstFrame(target, sceneBuilder);
target.AddDirty(border);
target.AddDirty(canvas);
target.AddDirty(root);
target.AddDirty(decorator);
var result = new List<IVisual>();
sceneBuilder.Setup(x => x.Update(It.IsAny<Scene>(), It.IsAny<IVisual>()))
.Callback<Scene, IVisual>((_, v) => result.Add(v));
RunFrame(target);
Assert.Equal(new List<IVisual> { root, decorator, border, canvas }, result);
}
}
[Fact]
public void Should_Add_Dirty_Rect_On_Child_Remove()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var dispatcher = new ImmediateDispatcher();
var loop = new Mock<IRenderLoop>();
Decorator decorator;
Border border;
var root = new TestRoot
{
Width = 100,
Height = 100,
Child = decorator = new Decorator
{
Child = border = new Border { Width = 50, Height = 50, Background = Brushes.Red, },
}
};
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
var sceneBuilder = new SceneBuilder();
var target = new DeferredRenderer(
root,
loop.Object,
sceneBuilder: sceneBuilder,
dispatcher: dispatcher);
root.Renderer = target;
target.Start();
RunFrame(target);
decorator.Child = null;
RunFrame(target);
var scene = target.UnitTestScene();
var stackNode = scene.FindNode(decorator);
var dirty = scene.Layers[0].Dirty.ToList();
Assert.Equal(1, dirty.Count);
Assert.Equal(new Rect(25, 25, 50, 50), dirty[0]);
}
}
[Fact]
public void Should_Update_VisualNode_Order_On_Child_Remove_Insert()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var dispatcher = new ImmediateDispatcher();
var loop = new Mock<IRenderLoop>();
StackPanel stack;
Canvas canvas1;
Canvas canvas2;
var root = new TestRoot
{
Child = stack = new StackPanel
{
Children = { (canvas1 = new Canvas()), (canvas2 = new Canvas()), }
}
};
var sceneBuilder = new SceneBuilder();
var target = new DeferredRenderer(
root,
loop.Object,
sceneBuilder: sceneBuilder,
dispatcher: dispatcher);
root.Renderer = target;
target.Start();
RunFrame(target);
stack.Children.Remove(canvas2);
stack.Children.Insert(0, canvas2);
RunFrame(target);
var scene = target.UnitTestScene();
var stackNode = scene.FindNode(stack);
Assert.Same(stackNode.Children[0].Visual, canvas2);
Assert.Same(stackNode.Children[1].Visual, canvas1);
}
}
[Fact]
public void Should_Update_VisualNode_Order_On_Child_Move()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var dispatcher = new ImmediateDispatcher();
var loop = new Mock<IRenderLoop>();
StackPanel stack;
Canvas canvas1;
Canvas canvas2;
var root = new TestRoot
{
Child = stack = new StackPanel
{
Children = { (canvas1 = new Canvas()), (canvas2 = new Canvas()), }
}
};
var sceneBuilder = new SceneBuilder();
var target = new DeferredRenderer(
root,
loop.Object,
sceneBuilder: sceneBuilder,
dispatcher: dispatcher);
root.Renderer = target;
target.Start();
RunFrame(target);
stack.Children.Move(1, 0);
RunFrame(target);
var scene = target.UnitTestScene();
var stackNode = scene.FindNode(stack);
Assert.Same(stackNode.Children[0].Visual, canvas2);
Assert.Same(stackNode.Children[1].Visual, canvas1);
}
}
[Fact]
public void Should_Update_VisualNode_Order_On_ZIndex_Change()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var dispatcher = new ImmediateDispatcher();
var loop = new Mock<IRenderLoop>();
StackPanel stack;
Canvas canvas1;
Canvas canvas2;
var root = new TestRoot
{
Child = stack = new StackPanel
{
Children =
{
(canvas1 = new Canvas { ZIndex = 1 }), (canvas2 = new Canvas { ZIndex = 2 }),
}
}
};
var sceneBuilder = new SceneBuilder();
var target = new DeferredRenderer(
root,
loop.Object,
sceneBuilder: sceneBuilder,
dispatcher: dispatcher);
root.Renderer = target;
target.Start();
RunFrame(target);
canvas1.ZIndex = 3;
RunFrame(target);
var scene = target.UnitTestScene();
var stackNode = scene.FindNode(stack);
Assert.Same(stackNode.Children[0].Visual, canvas2);
Assert.Same(stackNode.Children[1].Visual, canvas1);
}
}
[Fact]
public void Should_Update_VisualNode_Order_On_ZIndex_Change_With_Dirty_Ancestor()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var dispatcher = new ImmediateDispatcher();
var loop = new Mock<IRenderLoop>();
StackPanel stack;
Canvas canvas1;
Canvas canvas2;
var root = new TestRoot
{
Child = stack = new StackPanel
{
Children =
{
(canvas1 = new Canvas { ZIndex = 1 }), (canvas2 = new Canvas { ZIndex = 2 }),
}
}
};
var sceneBuilder = new SceneBuilder();
var target = new DeferredRenderer(
root,
loop.Object,
sceneBuilder: sceneBuilder,
dispatcher: dispatcher);
root.Renderer = target;
target.Start();
RunFrame(target);
root.InvalidateVisual();
canvas1.ZIndex = 3;
RunFrame(target);
var scene = target.UnitTestScene();
var stackNode = scene.FindNode(stack);
Assert.Same(stackNode.Children[0].Visual, canvas2);
Assert.Same(stackNode.Children[1].Visual, canvas1);
}
}
[Fact]
public void Should_Update_VisualNodes_When_Child_Moved_To_New_Parent()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var dispatcher = new ImmediateDispatcher();
var loop = new Mock<IRenderLoop>();
Decorator moveFrom;
Decorator moveTo;
Canvas moveMe;
var root = new TestRoot
{
Child = new StackPanel
{
Children =
{
(moveFrom = new Decorator { Child = moveMe = new Canvas(), }),
(moveTo = new Decorator()),
}
}
};
var sceneBuilder = new SceneBuilder();
var target = new DeferredRenderer(
root,
loop.Object,
sceneBuilder: sceneBuilder,
dispatcher: dispatcher);
root.Renderer = target;
target.Start();
RunFrame(target);
moveFrom.Child = null;
moveTo.Child = moveMe;
RunFrame(target);
var scene = target.UnitTestScene();
var moveFromNode = (VisualNode)scene.FindNode(moveFrom);
var moveToNode = (VisualNode)scene.FindNode(moveTo);
Assert.Empty(moveFromNode.Children);
Assert.Equal(1, moveToNode.Children.Count);
Assert.Same(moveMe, moveToNode.Children[0].Visual);
}
}
[Fact]
public void Should_Update_VisualNodes_When_Child_Moved_To_New_Parent_And_New_Root()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var dispatcher = new ImmediateDispatcher();
var loop = new Mock<IRenderLoop>();
Decorator moveFrom;
Decorator moveTo;
Canvas moveMe;
var root = new TestRoot
{
Child = new StackPanel
{
Children = { (moveFrom = new Decorator { Child = moveMe = new Canvas(), }) }
}
};
var otherRoot = new TestRoot { Child = new StackPanel { Children = { (moveTo = new Decorator()) } } };
var sceneBuilder = new SceneBuilder();
var target = new DeferredRenderer(
root,
loop.Object,
sceneBuilder: sceneBuilder,
dispatcher: dispatcher);
var otherSceneBuilder = new SceneBuilder();
var otherTarget = new DeferredRenderer(
otherRoot,
loop.Object,
sceneBuilder: otherSceneBuilder,
dispatcher: dispatcher);
root.Renderer = target;
otherRoot.Renderer = otherTarget;
target.Start();
otherTarget.Start();
RunFrame(target);
RunFrame(otherTarget);
moveFrom.Child = null;
moveTo.Child = moveMe;
RunFrame(target);
RunFrame(otherTarget);
var scene = target.UnitTestScene();
var otherScene = otherTarget.UnitTestScene();
var moveFromNode = (VisualNode)scene.FindNode(moveFrom);
var moveToNode = (VisualNode)otherScene.FindNode(moveTo);
Assert.Empty(moveFromNode.Children);
Assert.Equal(1, moveToNode.Children.Count);
Assert.Same(moveMe, moveToNode.Children[0].Visual);
}
}
[Fact]
public void Should_Push_Opacity_For_Controls_With_Less_Than_1_Opacity()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var root = new TestRoot
{
Width = 100, Height = 100, Child = new Border { Background = Brushes.Red, Opacity = 0.5, }
};
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
var target = CreateTargetAndRunFrame(root);
var context = GetLayerContext(target, root);
var animation = new BehaviorSubject<double>(0.5);
context.Verify(x => x.PushOpacity(0.5), Times.Once);
context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), default), Times.Once);
context.Verify(x => x.PopOpacity(), Times.Once);
}
}
[Fact]
public void Should_Not_Draw_Controls_With_0_Opacity()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var root = new TestRoot
{
Width = 100,
Height = 100,
Child = new Border
{
Background = Brushes.Red,
Opacity = 0,
Child = new Border { Background = Brushes.Green, }
}
};
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
var target = CreateTargetAndRunFrame(root);
var context = GetLayerContext(target, root);
var animation = new BehaviorSubject<double>(0.5);
context.Verify(x => x.PushOpacity(0.5), Times.Never);
context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), default), Times.Never);
context.Verify(x => x.PopOpacity(), Times.Never);
}
}
[Fact]
public void Should_Push_Opacity_Mask()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var root = new TestRoot
{
Width = 100,
Height = 100,
Child = new Border { Background = Brushes.Red, OpacityMask = Brushes.Green, }
};
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
var target = CreateTargetAndRunFrame(root);
var context = GetLayerContext(target, root);
var animation = new BehaviorSubject<double>(0.5);
context.Verify(x => x.PushOpacityMask(Brushes.Green, new Rect(0, 0, 100, 100)), Times.Once);
context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), default), Times.Once);
context.Verify(x => x.PopOpacityMask(), Times.Once);
}
}
[Fact]
public void Should_Create_Layer_For_Root()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var root = new TestRoot();
var rootLayer = new Mock<IRenderTargetBitmapImpl>();
var sceneBuilder = new Mock<ISceneBuilder>();
sceneBuilder.Setup(x => x.UpdateAll(It.IsAny<Scene>()))
.Callback<Scene>(scene =>
{
scene.Size = root.ClientSize;
scene.Layers.Add(root).Dirty.Add(new Rect(root.ClientSize));
});
var renderInterface = new Mock<IPlatformRenderInterface>();
var target = CreateTargetAndRunFrame(root, sceneBuilder: sceneBuilder.Object);
Assert.Single(target.Layers);
}
}
[Fact(Skip = "Layers are disabled. See #2244")]
public void Should_Create_And_Delete_Layers_For_Controls_With_Animated_Opacity()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
Border border;
var root = new TestRoot
{
Width = 100,
Height = 100,
Child = new Border
{
Background = Brushes.Red,
Child = border = new Border
{
Background = Brushes.Green, Child = new Canvas(), Opacity = 0.9,
}
}
};
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
var timer = new Mock<IRenderTimer>();
var target = CreateTargetAndRunFrame(root, timer);
Assert.Equal(new[] { root }, target.Layers.Select(x => x.LayerRoot));
var animation = new BehaviorSubject<double>(0.5);
border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
RunFrame(target);
Assert.Equal(new IVisual[] { root, border }, target.Layers.Select(x => x.LayerRoot));
animation.OnCompleted();
RunFrame(target);
Assert.Equal(new[] { root }, target.Layers.Select(x => x.LayerRoot));
}
}
[Fact(Skip = "Layers are disabled. See #2244")]
public void Should_Not_Create_Layer_For_Childless_Control_With_Animated_Opacity()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
Border border;
var root = new TestRoot
{
Width = 100,
Height = 100,
Child = new Border
{
Background = Brushes.Red, Child = border = new Border { Background = Brushes.Green, }
}
};
var animation = new BehaviorSubject<double>(0.5);
border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
var timer = new Mock<IRenderTimer>();
var target = CreateTargetAndRunFrame(root, timer);
Assert.Single(target.Layers);
}
}
[Fact(Skip = "Layers are disabled. See #2244")]
public void Should_Not_Push_Opacity_For_Transparent_Layer_Root_Control()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
Border border;
var root = new TestRoot
{
Width = 100,
Height = 100,
Child = border = new Border { Background = Brushes.Red, Child = new Canvas(), }
};
var animation = new BehaviorSubject<double>(0.5);
border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
var target = CreateTargetAndRunFrame(root);
var context = GetLayerContext(target, border);
context.Verify(x => x.PushOpacity(0.5), Times.Never);
context.Verify(x => x.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100), default), Times.Once);
context.Verify(x => x.PopOpacity(), Times.Never);
}
}
[Fact(Skip = "Layers are disabled. See #2244")]
public void Should_Draw_Transparent_Layer_With_Correct_Opacity()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
Border border;
var root = new TestRoot
{
Width = 100,
Height = 100,
Child = border = new Border { Background = Brushes.Red, Child = new Canvas(), }
};
var animation = new BehaviorSubject<double>(0.5);
border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
var target = CreateTargetAndRunFrame(root);
var context = Mock.Get(target.RenderTarget.CreateDrawingContext(null));
var borderLayer = target.Layers[border].Bitmap;
context.Verify(x => x.DrawBitmap(borderLayer, 0.5, It.IsAny<Rect>(), It.IsAny<Rect>(),
BitmapInterpolationMode.Default));
}
}
[Fact]
public void Can_Dirty_Control_In_SceneInvalidated()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
Border border1;
Border border2;
var root = new TestRoot
{
Width = 100,
Height = 100,
Child = new StackPanel
{
Children =
{
(border1 = new Border { Background = Brushes.Red, Child = new Canvas(), }),
(border2 = new Border { Background = Brushes.Red, Child = new Canvas(), }),
}
}
};
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
var target = CreateTargetAndRunFrame(root);
var invalidated = false;
target.SceneInvalidated += (s, e) =>
{
invalidated = true;
target.AddDirty(border2);
};
target.AddDirty(border1);
target.Paint(new Rect(root.DesiredSize));
Assert.True(invalidated);
Assert.True(((IRenderLoopTask)target).NeedsUpdate);
}
}
private DeferredRenderer CreateTargetAndRunFrame(
TestRoot root,
Mock<IRenderTimer> timer = null,
ISceneBuilder sceneBuilder = null,
IDispatcher dispatcher = null)
{
timer = timer ?? new Mock<IRenderTimer>();
dispatcher = dispatcher ?? new ImmediateDispatcher();
var target = new DeferredRenderer(
root,
new RenderLoop(timer.Object, dispatcher),
sceneBuilder: sceneBuilder,
dispatcher: dispatcher);
root.Renderer = target;
target.Start();
RunFrame(target);
return target;
}
private Mock<IDrawingContextImpl> GetLayerContext(DeferredRenderer renderer, IControl layerRoot)
{
return Mock.Get(renderer.Layers[layerRoot].Bitmap.Item.CreateDrawingContext(null));
}
private void IgnoreFirstFrame(IRenderLoopTask task, Mock<ISceneBuilder> sceneBuilder)
{
RunFrame(task);
sceneBuilder.Invocations.Clear();
}
private void RunFrame(IRenderLoopTask task)
{
task.Update(TimeSpan.Zero);
task.Render();
}
private IRenderTargetBitmapImpl CreateLayer()
{
return Mock.Of<IRenderTargetBitmapImpl>(x =>
x.CreateDrawingContext(It.IsAny<IVisualBrushRenderer>()) == Mock.Of<IDrawingContextImpl>());
}
private Mock<ISceneBuilder> MockSceneBuilder(IRenderRoot root)
{
var result = new Mock<ISceneBuilder>();
result.Setup(x => x.UpdateAll(It.IsAny<Scene>()))
.Callback<Scene>(x => x.Layers.Add(root).Dirty.Add(new Rect(root.ClientSize)));
return result;
}
}
}

577
tests/Avalonia.Base.UnitTests/Rendering/DeferredRendererTests_HitTesting.cs

@ -1,577 +0,0 @@
using System;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Shapes;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
using Xunit;
namespace Avalonia.Base.UnitTests.Rendering
{
public class DeferredRendererTests_HitTesting
{
[Fact]
public void HitTest_Should_Find_Controls_At_Point()
{
using (TestApplication())
{
var root = new TestRoot
{
Width = 200,
Height = 200,
Child = new Border
{
Width = 100,
Height = 100,
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
}
};
root.Renderer = new DeferredRenderer(root, null);
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
var result = root.Renderer.HitTest(new Point(100, 100), root, null);
Assert.Equal(new[] { root.Child }, result);
}
}
[Fact]
public void HitTest_Should_Not_Find_Empty_Controls_At_Point()
{
using (TestApplication())
{
var root = new TestRoot
{
Width = 200,
Height = 200,
Child = new Border
{
Width = 100,
Height = 100,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
}
};
root.Renderer = new DeferredRenderer(root, null);
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
var result = root.Renderer.HitTest(new Point(100, 100), root, null);
Assert.Empty(result);
}
}
[Fact]
public void HitTest_Should_Not_Find_Invisible_Controls_At_Point()
{
using (TestApplication())
{
Border visible;
var root = new TestRoot
{
Width = 200,
Height = 200,
Child = new Border
{
Width = 100,
Height = 100,
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
IsVisible = false,
Child = visible = new Border
{
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
}
}
};
root.Renderer = new DeferredRenderer(root, null);
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
var result = root.Renderer.HitTest(new Point(100, 100), root, null);
Assert.Empty(result);
}
}
[Fact]
public void HitTest_Should_Not_Find_Control_Outside_Point()
{
using (TestApplication())
{
var root = new TestRoot
{
Width = 200,
Height = 200,
Child = new Border
{
Width = 100,
Height = 100,
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
}
};
root.Renderer = new DeferredRenderer(root, null);
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
var result = root.Renderer.HitTest(new Point(10, 10), root, null);
Assert.Empty(result);
}
}
[Fact]
public void HitTest_Should_Return_Top_Controls_First()
{
using (TestApplication())
{
Panel container;
var root = new TestRoot
{
Child = container = new Panel
{
Width = 200,
Height = 200,
Children =
{
new Border
{
Width = 100,
Height = 100,
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
},
new Border
{
Width = 50,
Height = 50,
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
}
}
}
};
root.Renderer = new DeferredRenderer(root, null);
root.Measure(Size.Infinity);
root.Arrange(new Rect(container.DesiredSize));
var result = root.Renderer.HitTest(new Point(100, 100), root, null);
Assert.Equal(new[] { container.Children[1], container.Children[0] }, result);
}
}
[Fact]
public void HitTest_Should_Return_Top_Controls_First_With_ZIndex()
{
using (TestApplication())
{
Panel container;
var root = new TestRoot
{
Child = container = new Panel
{
Width = 200,
Height = 200,
Children =
{
new Border
{
Width = 100,
Height = 100,
ZIndex = 1,
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
},
new Border
{
Width = 50,
Height = 50,
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
},
new Border
{
Width = 75,
Height = 75,
ZIndex = 2,
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
}
}
}
};
root.Renderer = new DeferredRenderer(root, null);
root.Measure(Size.Infinity);
root.Arrange(new Rect(container.DesiredSize));
var result = root.Renderer.HitTest(new Point(100, 100), root, null);
Assert.Equal(new[] { container.Children[2], container.Children[0], container.Children[1] }, result);
}
}
[Fact]
public void HitTest_Should_Find_Control_Translated_Outside_Parent_Bounds()
{
using (TestApplication())
{
Border target;
Panel container;
var root = new TestRoot
{
Child = container = new Panel
{
Width = 200,
Height = 200,
Background = Brushes.Red,
ClipToBounds = false,
Children =
{
new Border
{
Width = 100,
Height = 100,
ZIndex = 1,
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
Child = target = new Border
{
Width = 50,
Height = 50,
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
RenderTransform = new TranslateTransform(110, 110),
}
},
}
}
};
root.Renderer = new DeferredRenderer(root, null);
container.Measure(Size.Infinity);
container.Arrange(new Rect(container.DesiredSize));
var result = root.Renderer.HitTest(new Point(120, 120), root, null);
Assert.Equal(new IVisual[] { target, container }, result);
}
}
[Fact]
public void HitTest_Should_Not_Find_Control_Outside_Parent_Bounds_When_Clipped()
{
using (TestApplication())
{
Border target;
Panel container;
var root = new TestRoot
{
Child = container = new Panel
{
Width = 100,
Height = 200,
Background = Brushes.Red,
Children =
{
new Panel()
{
Width = 100,
Height = 100,
Background = Brushes.Red,
Margin = new Thickness(0, 100, 0, 0),
ClipToBounds = true,
Children =
{
(target = new Border()
{
Width = 100,
Height = 100,
Background = Brushes.Red,
Margin = new Thickness(0, -100, 0, 0)
})
}
}
}
}
};
root.Renderer = new DeferredRenderer(root, null);
root.Measure(Size.Infinity);
root.Arrange(new Rect(container.DesiredSize));
var result = root.Renderer.HitTest(new Point(50, 50), root, null);
Assert.Equal(new[] { container }, result);
}
}
[Fact]
public void HitTest_Should_Not_Find_Control_Outside_Scroll_Viewport()
{
using (TestApplication())
{
Border target;
Border item1;
Border item2;
ScrollContentPresenter scroll;
Panel container;
var root = new TestRoot
{
Child = container = new Panel
{
Width = 100,
Height = 200,
Background = Brushes.Red,
Children =
{
(target = new Border()
{
Name = "b1",
Width = 100,
Height = 100,
Background = Brushes.Red,
}),
new Border()
{
Name = "b2",
Width = 100,
Height = 100,
Background = Brushes.Red,
Margin = new Thickness(0, 100, 0, 0),
Child = scroll = new ScrollContentPresenter()
{
CanHorizontallyScroll = true,
CanVerticallyScroll = true,
Content = new StackPanel()
{
Children =
{
(item1 = new Border()
{
Name = "b3",
Width = 100,
Height = 100,
Background = Brushes.Red,
}),
(item2 = new Border()
{
Name = "b4",
Width = 100,
Height = 100,
Background = Brushes.Red,
}),
}
}
}
}
}
}
};
scroll.UpdateChild();
root.Renderer = new DeferredRenderer(root, null);
root.Measure(Size.Infinity);
root.Arrange(new Rect(container.DesiredSize));
root.Renderer.Paint(Rect.Empty);
var result = root.Renderer.HitTest(new Point(50, 150), root, null).First();
Assert.Equal(item1, result);
result = root.Renderer.HitTest(new Point(50, 50), root, null).First();
Assert.Equal(target, result);
scroll.Offset = new Vector(0, 100);
// We don't have LayoutManager set up so do the layout pass manually.
scroll.Parent.InvalidateArrange();
container.InvalidateArrange();
container.Arrange(new Rect(container.DesiredSize));
root.Renderer.Paint(Rect.Empty);
result = root.Renderer.HitTest(new Point(50, 150), root, null).First();
Assert.Equal(item2, result);
result = root.Renderer.HitTest(new Point(50, 50), root, null).First();
Assert.Equal(target, result);
}
}
[Fact]
public void HitTest_Should_Not_Find_Path_When_Outside_Fill()
{
using (TestApplication())
{
Path path;
var root = new TestRoot
{
Width = 200,
Height = 200,
Child = path = new Path
{
Width = 200,
Height = 200,
Fill = Brushes.Red,
Data = StreamGeometry.Parse("M100,0 L0,100 100,100")
}
};
root.Renderer = new DeferredRenderer(root, null);
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
var result = root.Renderer.HitTest(new Point(100, 100), root, null);
Assert.Equal(new[] { path }, result);
result = root.Renderer.HitTest(new Point(10, 10), root, null);
Assert.Empty(result);
}
}
[Fact]
public void HitTest_Should_Respect_Geometry_Clip()
{
using (TestApplication())
{
Border border;
Canvas canvas;
var root = new TestRoot
{
Width = 400,
Height = 400,
Child = border = new Border
{
Background = Brushes.Red,
Clip = StreamGeometry.Parse("M100,0 L0,100 100,100"),
Width = 200,
Height = 200,
Child = canvas = new Canvas
{
Background = Brushes.Yellow,
Margin = new Thickness(10),
}
}
};
root.Renderer = new DeferredRenderer(root, null);
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
Assert.Equal(new Rect(100, 100, 200, 200), border.Bounds);
var context = new DrawingContext(Mock.Of<IDrawingContextImpl>());
var result = root.Renderer.HitTest(new Point(200, 200), root, null);
Assert.Equal(new IVisual[] { canvas, border }, result);
result = root.Renderer.HitTest(new Point(110, 110), root, null);
Assert.Empty(result);
}
}
[Fact]
public void HitTest_Should_Accommodate_ICustomHitTest()
{
using (TestApplication())
{
Border border;
var root = new TestRoot
{
Width = 300,
Height = 200,
Child = border = new CustomHitTestBorder
{
Width = 100,
Height = 100,
Background = Brushes.Red,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
}
};
root.Renderer = new DeferredRenderer(root, null);
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
var result = root.Renderer.HitTest(new Point(75, 100), root, null);
Assert.Equal(new[] { border }, result);
result = root.Renderer.HitTest(new Point(125, 100), root, null);
Assert.Equal(new[] { border }, result);
result = root.Renderer.HitTest(new Point(175, 100), root, null);
Assert.Empty(result);
}
}
[Fact]
public void HitTest_Should_Not_Hit_Controls_Next_Pixel()
{
using (TestApplication())
{
Border targetRectangle;
var root = new TestRoot
{
Width = 50,
Height = 200,
Child = new StackPanel
{
Orientation = Orientation.Vertical,
HorizontalAlignment = HorizontalAlignment.Left,
Children =
{
new Border { Width = 50, Height = 50, Background = Brushes.Red},
{ targetRectangle = new Border { Width = 50, Height = 50, Background = Brushes.Green} }
}
}
};
root.Renderer = new DeferredRenderer(root, null);
root.Measure(Size.Infinity);
root.Arrange(new Rect(root.DesiredSize));
var result = root.Renderer.HitTest(new Point(25, 50), root, null);
Assert.Equal(new[] { targetRectangle }, result);
}
}
private IDisposable TestApplication()
{
return UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
}
}
}

225
tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DeferredDrawingContextImplTests.cs

@ -1,225 +0,0 @@
using System.Linq;
using Avalonia.Media;
using Avalonia.Rendering.SceneGraph;
using Avalonia.UnitTests;
using Avalonia.Utilities;
using Avalonia.VisualTree;
using Moq;
using Xunit;
namespace Avalonia.Base.UnitTests.Rendering.SceneGraph
{
public class DeferredDrawingContextImplTests
{
[Fact]
public void Should_Add_VisualNode()
{
var parent = new VisualNode(new TestRoot(), null);
var child = new VisualNode(Mock.Of<IVisual>(), parent);
var layers = new SceneLayers(parent.Visual);
var target = new DeferredDrawingContextImpl(null, layers);
target.BeginUpdate(parent);
target.BeginUpdate(child);
Assert.Equal(1, parent.Children.Count);
Assert.Same(child, parent.Children[0]);
}
[Fact]
public void Should_Not_Replace_Identical_VisualNode()
{
var parent = new VisualNode(new TestRoot(), null);
var child = new VisualNode(Mock.Of<IVisual>(), parent);
var layers = new SceneLayers(parent.Visual);
parent.AddChild(child);
var target = new DeferredDrawingContextImpl(null, layers);
target.BeginUpdate(parent);
target.BeginUpdate(child);
Assert.Equal(1, parent.Children.Count);
Assert.Same(child, parent.Children[0]);
}
[Fact]
public void Should_Replace_Different_VisualNode()
{
var parent = new VisualNode(new TestRoot(), null);
var child1 = new VisualNode(Mock.Of<IVisual>(), parent);
var child2 = new VisualNode(Mock.Of<IVisual>(), parent);
var layers = new SceneLayers(parent.Visual);
parent.AddChild(child1);
var target = new DeferredDrawingContextImpl(null, layers);
target.BeginUpdate(parent);
target.BeginUpdate(child2);
Assert.Equal(1, parent.Children.Count);
Assert.Same(child2, parent.Children[0]);
}
[Fact]
public void TrimChildren_Should_Trim_Children()
{
var root = new TestRoot();
var node = new VisualNode(root, null) { LayerRoot = root };
node.AddChild(new VisualNode(Mock.Of<IVisual>(), node) { LayerRoot = root });
node.AddChild(new VisualNode(Mock.Of<IVisual>(), node) { LayerRoot = root });
node.AddChild(new VisualNode(Mock.Of<IVisual>(), node) { LayerRoot = root });
node.AddChild(new VisualNode(Mock.Of<IVisual>(), node) { LayerRoot = root });
var layers = new SceneLayers(root);
var target = new DeferredDrawingContextImpl(null, layers);
var child1 = new VisualNode(Mock.Of<IVisual>(), node) { LayerRoot = root };
var child2 = new VisualNode(Mock.Of<IVisual>(), node) { LayerRoot = root };
target.BeginUpdate(node);
using (target.BeginUpdate(child1)) { }
using (target.BeginUpdate(child2)) { }
target.TrimChildren();
Assert.Equal(2, node.Children.Count);
}
[Fact]
public void Should_Add_DrawOperations()
{
var node = new VisualNode(new TestRoot(), null);
var layers = new SceneLayers(node.Visual);
var target = new DeferredDrawingContextImpl(null, layers);
node.LayerRoot = node.Visual;
using (target.BeginUpdate(node))
{
target.DrawRectangle(Brushes.Red, new Pen(Brushes.Green, 1), new Rect(0, 0, 100, 100));
}
Assert.Equal(1, node.DrawOperations.Count);
Assert.IsType<RectangleNode>(node.DrawOperations[0].Item);
}
[Fact]
public void Should_Not_Replace_Identical_DrawOperation()
{
var node = new VisualNode(new TestRoot(), null);
var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), default));
var layers = new SceneLayers(node.Visual);
var target = new DeferredDrawingContextImpl(null, layers);
node.LayerRoot = node.Visual;
node.AddDrawOperation(operation);
using (target.BeginUpdate(node))
{
target.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 100));
}
Assert.Equal(1, node.DrawOperations.Count);
Assert.Same(operation.Item, node.DrawOperations.Single().Item);
Assert.IsType<RectangleNode>(node.DrawOperations[0].Item);
}
[Fact]
public void Should_Replace_Different_DrawOperation()
{
var node = new VisualNode(new TestRoot(), null);
var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), default));
var layers = new SceneLayers(node.Visual);
var target = new DeferredDrawingContextImpl(null, layers);
node.LayerRoot = node.Visual;
node.AddDrawOperation(operation);
using (target.BeginUpdate(node))
{
target.DrawRectangle(Brushes.Green, null, new Rect(0, 0, 100, 100));
}
Assert.Equal(1, node.DrawOperations.Count);
Assert.NotSame(operation, node.DrawOperations.Single());
Assert.IsType<RectangleNode>(node.DrawOperations[0].Item);
}
[Fact]
public void Should_Update_DirtyRects()
{
var node = new VisualNode(new TestRoot(), null);
var operation = new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), default);
var layers = new SceneLayers(node.Visual);
var target = new DeferredDrawingContextImpl(null, layers);
node.LayerRoot = node.Visual;
using (target.BeginUpdate(node))
{
target.DrawRectangle(Brushes.Green, null, new Rect(0, 0, 100, 100));
}
Assert.Equal(new Rect(0, 0, 100, 100), layers.Single().Dirty.Single());
}
[Fact]
public void Should_Trim_DrawOperations()
{
var node = new VisualNode(new TestRoot(), null);
node.LayerRoot = node.Visual;
for (var i = 0; i < 4; ++i)
{
var drawOperation = new Mock<IDrawOperation>();
using (var r = RefCountable.Create(drawOperation.Object))
{
node.AddDrawOperation(r);
}
}
var drawOperations = node.DrawOperations.Select(op => op.Item).ToList();
var layers = new SceneLayers(node.Visual);
var target = new DeferredDrawingContextImpl(null, layers);
using (target.BeginUpdate(node))
{
target.DrawRectangle(Brushes.Green, null, new Rect(0, 0, 10, 100));
target.DrawRectangle(Brushes.Blue, null, new Rect(0, 0, 20, 100));
}
Assert.Equal(2, node.DrawOperations.Count);
foreach (var i in drawOperations)
{
Mock.Get(i).Verify(x => x.Dispose());
}
}
[Fact]
public void Trimmed_DrawOperations_Releases_Reference()
{
var node = new VisualNode(new TestRoot(), null);
var operation = RefCountable.Create(new RectangleNode(Matrix.Identity, Brushes.Red, null, new Rect(0, 0, 100, 100), default));
var layers = new SceneLayers(node.Visual);
var target = new DeferredDrawingContextImpl(null, layers);
node.LayerRoot = node.Visual;
node.AddDrawOperation(operation);
Assert.Equal(2, operation.RefCount);
using (target.BeginUpdate(node))
{
target.DrawRectangle(Brushes.Green, null, new Rect(0, 0, 100, 100));
}
Assert.Equal(1, node.DrawOperations.Count);
Assert.NotSame(operation, node.DrawOperations.Single());
Assert.Equal(1, operation.RefCount);
}
}
}

1087
tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs

File diff suppressed because it is too large

258
tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneBuilderTests_Layers.cs

@ -1,258 +0,0 @@
using System.Linq;
using System.Reactive.Subjects;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Media;
using Avalonia.Rendering.SceneGraph;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
namespace Avalonia.Base.UnitTests.Rendering.SceneGraph
{
public partial class SceneBuilderTests
{
[Fact(Skip = "Layers are disabled. See #2244")]
public void Control_With_Animated_Opacity_And_Children_Should_Start_New_Layer()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
Decorator decorator;
Border border;
Canvas canvas;
var tree = new TestRoot
{
Padding = new Thickness(10),
Width = 100,
Height = 120,
Child = decorator = new Decorator
{
Padding = new Thickness(11),
Child = border = new Border
{
Background = Brushes.Red,
Padding = new Thickness(12),
Child = canvas = new Canvas()
}
}
};
var layout = tree.LayoutManager;
layout.ExecuteInitialLayoutPass();
var animation = new BehaviorSubject<double>(0.5);
border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
var scene = new Scene(tree);
var sceneBuilder = new SceneBuilder();
sceneBuilder.UpdateAll(scene);
var rootNode = (VisualNode)scene.Root;
var borderNode = (VisualNode)scene.FindNode(border);
var canvasNode = (VisualNode)scene.FindNode(canvas);
Assert.Same(tree, rootNode.LayerRoot);
Assert.Same(border, borderNode.LayerRoot);
Assert.Same(border, canvasNode.LayerRoot);
Assert.Equal(0.5, scene.Layers[border].Opacity);
Assert.Equal(2, scene.Layers.Count());
Assert.Empty(scene.Layers.Select(x => x.LayerRoot).Except(new IVisual[] { tree, border }));
animation.OnCompleted();
scene = scene.CloneScene();
sceneBuilder.Update(scene, border);
rootNode = (VisualNode)scene.Root;
borderNode = (VisualNode)scene.FindNode(border);
canvasNode = (VisualNode)scene.FindNode(canvas);
Assert.Same(tree, rootNode.LayerRoot);
Assert.Same(tree, borderNode.LayerRoot);
Assert.Same(tree, canvasNode.LayerRoot);
Assert.Single(scene.Layers);
var rootDirty = scene.Layers[tree].Dirty;
Assert.Single(rootDirty);
Assert.Equal(new Rect(21, 21, 58, 78), rootDirty.Single());
}
}
[Fact]
public void Control_With_Animated_Opacity_And_No_Children_Should_Not_Start_New_Layer()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
Decorator decorator;
Border border;
var tree = new TestRoot
{
Padding = new Thickness(10),
Width = 100,
Height = 120,
Child = decorator = new Decorator
{
Padding = new Thickness(11),
Child = border = new Border
{
Background = Brushes.Red,
}
}
};
var layout = tree.LayoutManager;
layout.ExecuteInitialLayoutPass();
var animation = new BehaviorSubject<double>(0.5);
border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
var scene = new Scene(tree);
var sceneBuilder = new SceneBuilder();
sceneBuilder.UpdateAll(scene);
Assert.Single(scene.Layers);
}
}
[Fact(Skip = "Layers are disabled. See #2244")]
public void Removing_Control_With_Animated_Opacity_Should_Remove_Layers()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
Decorator decorator;
Border border;
Canvas canvas;
var tree = new TestRoot
{
Padding = new Thickness(10),
Width = 100,
Height = 120,
Child = decorator = new Decorator
{
Padding = new Thickness(11),
Child = border = new Border
{
Background = Brushes.Red,
Padding = new Thickness(12),
Child = canvas = new Canvas
{
Children = { new TextBlock() },
}
}
}
};
var layout = tree.LayoutManager;
layout.ExecuteInitialLayoutPass();
var animation = new BehaviorSubject<double>(0.5);
border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
canvas.Bind(Canvas.OpacityProperty, animation, BindingPriority.Animation);
var scene = new Scene(tree);
var sceneBuilder = new SceneBuilder();
sceneBuilder.UpdateAll(scene);
Assert.Equal(3, scene.Layers.Count);
decorator.Child = null;
scene = scene.CloneScene();
sceneBuilder.Update(scene, border);
Assert.Equal(1, scene.Layers.Count);
}
}
[Fact(Skip = "Layers are disabled. See #2244")]
public void Hiding_Transparent_Control_Should_Remove_Layers()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
Decorator decorator;
Border border;
Canvas canvas;
var tree = new TestRoot
{
Padding = new Thickness(10),
Width = 100,
Height = 120,
Child = decorator = new Decorator
{
Padding = new Thickness(11),
Child = border = new Border
{
Background = Brushes.Red,
Padding = new Thickness(12),
Child = canvas = new Canvas
{
Children = { new TextBlock() },
}
}
}
};
var layout = tree.LayoutManager;
layout.ExecuteInitialLayoutPass();
var animation = new BehaviorSubject<double>(0.5);
border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
canvas.Bind(Canvas.OpacityProperty, animation, BindingPriority.Animation);
var scene = new Scene(tree);
var sceneBuilder = new SceneBuilder();
sceneBuilder.UpdateAll(scene);
Assert.Equal(3, scene.Layers.Count);
border.IsVisible = false;
scene = scene.CloneScene();
sceneBuilder.Update(scene, border);
Assert.Equal(1, scene.Layers.Count);
}
}
[Fact(Skip = "Layers are disabled. See #2244")]
public void GeometryClip_Should_Affect_Child_Layers()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var clip = StreamGeometry.Parse("M100,0 L0,100 100,100");
Decorator decorator;
Border border;
var tree = new TestRoot
{
Child = decorator = new Decorator
{
Clip = clip,
Margin = new Thickness(12, 16),
Child = border = new Border
{
Opacity = 0.5,
Child = new Canvas(),
}
}
};
var layout = tree.LayoutManager;
layout.ExecuteInitialLayoutPass();
var animation = new BehaviorSubject<double>(0.5);
border.Bind(Border.OpacityProperty, animation, BindingPriority.Animation);
var scene = new Scene(tree);
var sceneBuilder = new SceneBuilder();
sceneBuilder.UpdateAll(scene);
var borderLayer = scene.Layers[border];
Assert.Equal(
Matrix.CreateTranslation(12, 16),
((MockStreamGeometryImpl)borderLayer.GeometryClip).Transform);
}
}
}
}

35
tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneLayersTests.cs

@ -1,35 +0,0 @@
using System.Linq;
using Avalonia.Controls;
using Avalonia.Rendering.SceneGraph;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
namespace Avalonia.Base.UnitTests.Rendering.SceneGraph
{
public class SceneLayersTests
{
[Fact]
public void Layers_Should_Be_Ordered()
{
Border border;
Decorator decorator;
var root = new TestRoot
{
Child = border = new Border
{
Child = decorator = new Decorator(),
}
};
var target = new SceneLayers(root);
target.Add(root);
target.Add(decorator);
target.Add(border);
var result = target.Select(x => x.LayerRoot).ToArray();
Assert.Equal(new IVisual[] { root, border, decorator }, result);
}
}
}

33
tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/SceneTests.cs

@ -1,33 +0,0 @@
using System.Linq;
using Avalonia.Controls;
using Avalonia.Rendering.SceneGraph;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Base.UnitTests.Rendering.SceneGraph
{
public class SceneTests
{
[Fact]
public void Cloning_Scene_Should_Retain_Layers_But_Not_DirtyRects()
{
Decorator decorator;
var tree = new TestRoot
{
Child = decorator = new Decorator(),
};
var scene = new Scene(tree);
scene.Layers.Add(tree);
scene.Layers.Add(decorator);
scene.Layers[tree].Dirty.Add(new Rect(0, 0, 100, 100));
scene.Layers[decorator].Dirty.Add(new Rect(0, 0, 50, 100));
scene = scene.CloneScene();
Assert.Equal(2, scene.Layers.Count());
Assert.Empty(scene.Layers[0].Dirty);
Assert.Empty(scene.Layers[1].Dirty);
}
}
}

123
tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/VisualNodeTests.cs

@ -1,123 +0,0 @@
using Avalonia.Rendering.SceneGraph;
using Avalonia.Utilities;
using Avalonia.VisualTree;
using Moq;
using Xunit;
namespace Avalonia.Base.UnitTests.Rendering.SceneGraph
{
public class VisualNodeTests
{
[Fact]
public void Empty_Children_Collections_Should_Be_Shared()
{
var node1 = new VisualNode(Mock.Of<IVisual>(), null);
var node2 = new VisualNode(Mock.Of<IVisual>(), null);
Assert.Same(node1.Children, node2.Children);
}
[Fact]
public void Adding_Child_Should_Create_Collection()
{
var node = new VisualNode(Mock.Of<IVisual>(), null);
var collection = node.Children;
node.AddChild(Mock.Of<IVisualNode>(x => x.Parent == node));
Assert.NotSame(collection, node.Children);
}
[Fact]
public void Empty_DrawOperations_Collections_Should_Be_Shared()
{
var node1 = new VisualNode(Mock.Of<IVisual>(), null);
var node2 = new VisualNode(Mock.Of<IVisual>(), null);
Assert.Same(node1.DrawOperations, node2.DrawOperations);
}
[Fact]
public void Adding_DrawOperation_Should_Create_Collection()
{
var node = new VisualNode(Mock.Of<IVisual>(), null);
var collection = node.DrawOperations;
node.AddDrawOperation(RefCountable.Create(Mock.Of<IDrawOperation>()));
Assert.NotSame(collection, node.DrawOperations);
}
[Fact]
public void Cloned_Nodes_Should_Share_DrawOperations_Collection()
{
var node1 = new VisualNode(Mock.Of<IVisual>(), null);
node1.AddDrawOperation(RefCountable.Create(Mock.Of<IDrawOperation>()));
var node2 = node1.Clone(null);
Assert.Same(node1.DrawOperations, node2.DrawOperations);
}
[Fact]
public void Adding_DrawOperation_To_Cloned_Node_Should_Create_New_Collection()
{
var node1 = new VisualNode(Mock.Of<IVisual>(), null);
var operation1 = RefCountable.Create(Mock.Of<IDrawOperation>());
node1.AddDrawOperation(operation1);
var node2 = node1.Clone(null);
var operation2 = RefCountable.Create(Mock.Of<IDrawOperation>());
node2.ReplaceDrawOperation(0, operation2);
Assert.NotSame(node1.DrawOperations, node2.DrawOperations);
Assert.Equal(1, node1.DrawOperations.Count);
Assert.Equal(1, node2.DrawOperations.Count);
Assert.Same(operation1.Item, node1.DrawOperations[0].Item);
Assert.Same(operation2.Item, node2.DrawOperations[0].Item);
}
[Fact]
public void DrawOperations_In_Cloned_Node_Are_Cloned()
{
var node1 = new VisualNode(Mock.Of<IVisual>(), null);
var operation1 = RefCountable.Create(Mock.Of<IDrawOperation>());
node1.AddDrawOperation(operation1);
var node2 = node1.Clone(null);
var operation2 = RefCountable.Create(Mock.Of<IDrawOperation>());
node2.AddDrawOperation(operation2);
Assert.Same(node1.DrawOperations[0].Item, node2.DrawOperations[0].Item);
Assert.NotSame(node1.DrawOperations[0], node2.DrawOperations[0]);
}
[Fact]
public void SortChildren_Does_Not_Throw_On_Null_Children()
{
var node = new VisualNode(Mock.Of<IVisual>(), null);
var scene = new Scene(Mock.Of<IVisual>());
node.SortChildren(scene);
}
[Fact]
public void TrimChildren_Should_Work_Correctly()
{
var parent = new VisualNode(Mock.Of<IVisual>(), null);
var child1 = new VisualNode(Mock.Of<IVisual>(), parent);
var child2 = new VisualNode(Mock.Of<IVisual>(), parent);
var child3 = new VisualNode(Mock.Of<IVisual>(), parent);
parent.AddChild(child1);
parent.AddChild(child2);
parent.AddChild(child3);
parent.TrimChildren(2);
Assert.Equal(2, parent.Children.Count);
Assert.False(child1.Disposed);
Assert.False(child2.Disposed);
Assert.True(child3.Disposed);
}
}
}
Loading…
Cancel
Save