committed by
GitHub
93 changed files with 735 additions and 8022 deletions
@ -0,0 +1,34 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.Metadata; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Instructs the compiler to resolve the compiled bindings data type for the item-specific properties of collection-like controls.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// A typical usage example is a ListBox control, where <see cref="InheritDataTypeFromItemsAttribute"/> is defined on the ItemTemplate property,
|
||||
|
/// allowing the template to inherit the data type from the Items collection binding.
|
||||
|
/// </remarks>
|
||||
|
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] |
||||
|
public sealed class InheritDataTypeFromItemsAttribute : Attribute |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="InheritDataTypeFromItemsAttribute"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="ancestorItemsProperty">The name of the property whose item type should be used on the target property.</param>
|
||||
|
public InheritDataTypeFromItemsAttribute(string ancestorItemsProperty) |
||||
|
{ |
||||
|
AncestorItemsProperty = ancestorItemsProperty; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The name of the property whose item type should be used on the target property.
|
||||
|
/// </summary>
|
||||
|
public string AncestorItemsProperty { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The ancestor type to be used in a lookup for the <see cref="AncestorProperty"/>.
|
||||
|
/// If null, the declaring type of the target property is used.
|
||||
|
/// </summary>
|
||||
|
public Type? AncestorType { get; set; } |
||||
|
} |
||||
@ -1,780 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Diagnostics.CodeAnalysis; |
|
||||
using System.IO; |
|
||||
using System.Linq; |
|
||||
using System.Threading.Tasks; |
|
||||
using Avalonia.Logging; |
|
||||
using Avalonia.Media; |
|
||||
using Avalonia.Media.Immutable; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.Rendering.SceneGraph; |
|
||||
using Avalonia.Threading; |
|
||||
using Avalonia.Utilities; |
|
||||
using Avalonia.VisualTree; |
|
||||
|
|
||||
namespace Avalonia.Rendering |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// A renderer which renders the state of the visual tree to an intermediate scene graph
|
|
||||
/// representation which is then rendered on a rendering thread.
|
|
||||
/// </summary>
|
|
||||
public class DeferredRenderer : RendererBase, IRenderer, IRenderLoopTask, IVisualBrushRenderer |
|
||||
{ |
|
||||
private readonly IDispatcher? _dispatcher; |
|
||||
private readonly IRenderLoop? _renderLoop; |
|
||||
private readonly Func<IRenderTarget>? _renderTargetFactory; |
|
||||
private readonly PlatformRenderInterfaceContextManager? _renderInterface; |
|
||||
private readonly Visual _root; |
|
||||
private readonly ISceneBuilder _sceneBuilder; |
|
||||
|
|
||||
private bool _running; |
|
||||
private bool _disposed; |
|
||||
private volatile IRef<Scene>? _scene; |
|
||||
private DirtyVisuals? _dirty; |
|
||||
private HashSet<Visual>? _recalculateChildren; |
|
||||
private IRef<IRenderTargetBitmapImpl>? _overlay; |
|
||||
private int _lastSceneId = -1; |
|
||||
private DisplayDirtyRects _dirtyRectsDisplay = new DisplayDirtyRects(); |
|
||||
private IRef<IDrawOperation>? _currentDraw; |
|
||||
private readonly IDeferredRendererLock _lock; |
|
||||
private readonly object _sceneLock = new object(); |
|
||||
private readonly object _startStopLock = new object(); |
|
||||
private readonly object _renderLoopIsRenderingLock = new object(); |
|
||||
private readonly Action _updateSceneIfNeededDelegate; |
|
||||
private List<Action>? _pendingRenderThreadJobs; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="DeferredRenderer"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="root">The control to render.</param>
|
|
||||
/// <param name="renderLoop">The render loop.</param>
|
|
||||
/// <param name="renderTargetFactory">The target render factory.</param>
|
|
||||
/// <param name="renderInterface">The Platform Render Context.</param>
|
|
||||
/// <param name="sceneBuilder">The scene builder to use. Optional.</param>
|
|
||||
/// <param name="dispatcher">The dispatcher to use. Optional.</param>
|
|
||||
/// <param name="rendererLock">Lock object used before trying to access render target</param>
|
|
||||
public DeferredRenderer( |
|
||||
IRenderRoot root, |
|
||||
IRenderLoop renderLoop, |
|
||||
Func<IRenderTarget> renderTargetFactory, |
|
||||
PlatformRenderInterfaceContextManager? renderInterface = null, |
|
||||
ISceneBuilder? sceneBuilder = null, |
|
||||
IDispatcher? dispatcher = null, |
|
||||
IDeferredRendererLock? rendererLock = null) : base(true) |
|
||||
{ |
|
||||
_dispatcher = dispatcher ?? Dispatcher.UIThread; |
|
||||
_root = root as Visual ?? throw new ArgumentNullException(nameof(root)); |
|
||||
_sceneBuilder = sceneBuilder ?? new SceneBuilder(); |
|
||||
Layers = new RenderLayers(); |
|
||||
_renderLoop = renderLoop; |
|
||||
_renderTargetFactory = renderTargetFactory; |
|
||||
_renderInterface = renderInterface; |
|
||||
_lock = rendererLock ?? new ManagedDeferredRendererLock(); |
|
||||
_updateSceneIfNeededDelegate = UpdateSceneIfNeeded; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="DeferredRenderer"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="root">The control to render.</param>
|
|
||||
/// <param name="renderTarget">The render target.</param>
|
|
||||
/// <param name="sceneBuilder">The scene builder to use. Optional.</param>
|
|
||||
/// <remarks>
|
|
||||
/// This constructor is intended to be used for unit testing.
|
|
||||
/// </remarks>
|
|
||||
public DeferredRenderer( |
|
||||
Visual root, |
|
||||
IRenderTarget renderTarget, |
|
||||
ISceneBuilder? sceneBuilder = null) : base(true) |
|
||||
{ |
|
||||
_root = root ?? throw new ArgumentNullException(nameof(root)); |
|
||||
RenderTarget = renderTarget ?? throw new ArgumentNullException(nameof(renderTarget)); |
|
||||
_sceneBuilder = sceneBuilder ?? new SceneBuilder(); |
|
||||
Layers = new RenderLayers(); |
|
||||
_lock = new ManagedDeferredRendererLock(); |
|
||||
_updateSceneIfNeededDelegate = UpdateSceneIfNeeded; |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public bool DrawFps { get; set; } |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public bool DrawDirtyRects { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets a path to which rendered frame should be rendered for debugging.
|
|
||||
/// </summary>
|
|
||||
public string? DebugFramesPath { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Forces the renderer to only draw frames on the render thread. Makes Paint to wait until frame is rendered
|
|
||||
/// </summary>
|
|
||||
public bool RenderOnlyOnRenderThread { get; set; } |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public event EventHandler<SceneInvalidatedEventArgs>? SceneInvalidated; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the render layers.
|
|
||||
/// </summary>
|
|
||||
internal RenderLayers Layers { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the current render target.
|
|
||||
/// </summary>
|
|
||||
internal IRenderTarget? RenderTarget { get; private set; } |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void AddDirty(Visual visual) |
|
||||
{ |
|
||||
_dirty?.Add(visual); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Disposes of the renderer and detaches from the render loop.
|
|
||||
/// </summary>
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
lock (_sceneLock) |
|
||||
{ |
|
||||
if (_disposed) |
|
||||
return; |
|
||||
_disposed = true; |
|
||||
var scene = _scene; |
|
||||
_scene = null; |
|
||||
scene?.Dispose(); |
|
||||
} |
|
||||
|
|
||||
Stop(); |
|
||||
// Wait for any in-progress rendering to complete
|
|
||||
lock(_renderLoopIsRenderingLock){} |
|
||||
DisposeRenderTarget(); |
|
||||
} |
|
||||
|
|
||||
public void RecalculateChildren(Visual visual) => _recalculateChildren?.Add(visual); |
|
||||
|
|
||||
void DisposeRenderTarget() |
|
||||
{ |
|
||||
using (var l = _lock.TryLock()) |
|
||||
{ |
|
||||
if(l == null) |
|
||||
{ |
|
||||
// We are still trying to render on the render thread, try again a bit later
|
|
||||
DispatcherTimer.RunOnce(DisposeRenderTarget, TimeSpan.FromMilliseconds(50), |
|
||||
DispatcherPriority.Background); |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
Layers.Clear(); |
|
||||
RenderTarget?.Dispose(); |
|
||||
RenderTarget = null; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public IEnumerable<Visual> HitTest(Point p, Visual root, Func<Visual, bool>? filter) |
|
||||
{ |
|
||||
EnsureCanHitTest(); |
|
||||
|
|
||||
//It's safe to access _scene here without a lock since
|
|
||||
//it's only changed from UI thread which we are currently on
|
|
||||
return _scene?.Item.HitTest(p, root, filter) ?? Enumerable.Empty<Visual>(); |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public Visual? HitTestFirst(Point p, Visual root, Func<Visual, bool>? filter) |
|
||||
{ |
|
||||
EnsureCanHitTest(); |
|
||||
|
|
||||
//It's safe to access _scene here without a lock since
|
|
||||
//it's only changed from UI thread which we are currently on
|
|
||||
return _scene?.Item.HitTestFirst(p, root, filter); |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void Paint(Rect rect) |
|
||||
{ |
|
||||
if (RenderOnlyOnRenderThread) |
|
||||
{ |
|
||||
// Renderer is stopped and doesn't tick on the render thread
|
|
||||
// This indicates a bug somewhere in our code
|
|
||||
// (currently happens when a window gets minimized with Show desktop on Windows 10)
|
|
||||
if(!_running) |
|
||||
return; |
|
||||
|
|
||||
while (true) |
|
||||
{ |
|
||||
Scene? scene; |
|
||||
bool? updated; |
|
||||
lock (_sceneLock) |
|
||||
{ |
|
||||
updated = UpdateScene(); |
|
||||
scene = _scene?.Item; |
|
||||
} |
|
||||
|
|
||||
// Renderer is in invalid state, skip drawing
|
|
||||
if(updated == null) |
|
||||
return; |
|
||||
|
|
||||
// Wait for the scene to be rendered or disposed
|
|
||||
scene?.Rendered.Wait(); |
|
||||
|
|
||||
// That was an up-to-date scene, we can return immediately
|
|
||||
if (updated == true) |
|
||||
return; |
|
||||
} |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
var t = (IRenderLoopTask)this; |
|
||||
if (t.NeedsUpdate) |
|
||||
UpdateScene(); |
|
||||
if (_scene?.Item != null) |
|
||||
Render(true); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void Resized(Size size) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void Start() |
|
||||
{ |
|
||||
lock (_startStopLock) |
|
||||
{ |
|
||||
if (!_running && _renderLoop != null) |
|
||||
{ |
|
||||
_renderLoop.Add(this); |
|
||||
_running = true; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void Stop() |
|
||||
{ |
|
||||
lock (_startStopLock) |
|
||||
{ |
|
||||
if (_running && _renderLoop != null) |
|
||||
{ |
|
||||
_renderLoop.Remove(this); |
|
||||
_running = false; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public ValueTask<object?> TryGetRenderInterfaceFeature(Type featureType) |
|
||||
{ |
|
||||
if (_renderInterface == null) |
|
||||
return new((object?)null); |
|
||||
|
|
||||
var tcs = new TaskCompletionSource<object?>(); |
|
||||
_pendingRenderThreadJobs ??= new(); |
|
||||
_pendingRenderThreadJobs.Add(() => |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
using (_renderInterface.EnsureCurrent()) |
|
||||
{ |
|
||||
tcs.TrySetResult(_renderInterface.Value.TryGetFeature(featureType)); |
|
||||
} |
|
||||
} |
|
||||
catch (Exception e) |
|
||||
{ |
|
||||
tcs.TrySetException(e); |
|
||||
} |
|
||||
}); |
|
||||
return new ValueTask<object?>(tcs.Task); |
|
||||
} |
|
||||
|
|
||||
bool NeedsUpdate => _dirty == null || _dirty.Count > 0; |
|
||||
bool IRenderLoopTask.NeedsUpdate => NeedsUpdate; |
|
||||
|
|
||||
void IRenderLoopTask.Update(TimeSpan time) => UpdateScene(); |
|
||||
|
|
||||
void IRenderLoopTask.Render() |
|
||||
{ |
|
||||
lock (_renderLoopIsRenderingLock) |
|
||||
{ |
|
||||
lock(_startStopLock) |
|
||||
if(!_running) |
|
||||
return; |
|
||||
Render(false); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
static Scene? TryGetChildScene(IRef<IDrawOperation>? op) => (op?.Item as BrushDrawOperation)?.Aux as Scene; |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush) |
|
||||
{ |
|
||||
return TryGetChildScene(_currentDraw)?.Size ?? default; |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush) |
|
||||
{ |
|
||||
var childScene = TryGetChildScene(_currentDraw); |
|
||||
|
|
||||
if (childScene != null) |
|
||||
{ |
|
||||
Render(context, (VisualNode)childScene.Root, null, new Rect(childScene.Size)); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
internal void UnitTestUpdateScene() => UpdateScene(); |
|
||||
|
|
||||
internal void UnitTestRender() => Render(false); |
|
||||
|
|
||||
internal Scene? UnitTestScene() => _scene?.Item; |
|
||||
|
|
||||
private void EnsureCanHitTest() |
|
||||
{ |
|
||||
if (_renderLoop == null && (_dirty == null || _dirty.Count > 0)) |
|
||||
{ |
|
||||
// When unit testing the renderLoop may be null, so update the scene manually.
|
|
||||
UpdateScene(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
internal void Render(bool forceComposite) |
|
||||
{ |
|
||||
using (var l = _lock.TryLock()) |
|
||||
{ |
|
||||
if (l == null) |
|
||||
return; |
|
||||
|
|
||||
IDrawingContextImpl? context = null; |
|
||||
try |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
var (scene, updated) = UpdateRenderLayersAndConsumeSceneIfNeeded(ref context); |
|
||||
if (updated) |
|
||||
FpsTick(); |
|
||||
using (scene) |
|
||||
{ |
|
||||
if (scene?.Item != null) |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
var overlay = DrawDirtyRects || DrawFps; |
|
||||
if (DrawDirtyRects) |
|
||||
_dirtyRectsDisplay.Tick(); |
|
||||
if (overlay) |
|
||||
RenderOverlay(scene.Item, ref context); |
|
||||
if (updated || forceComposite || overlay) |
|
||||
RenderComposite(scene.Item, ref context); |
|
||||
} |
|
||||
finally |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
if(scene.Item.RenderThreadJobs!=null) |
|
||||
foreach (var job in scene.Item.RenderThreadJobs) |
|
||||
job(); |
|
||||
} |
|
||||
finally |
|
||||
{ |
|
||||
scene.Item.MarkAsRendered(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
finally |
|
||||
{ |
|
||||
context?.Dispose(); |
|
||||
} |
|
||||
} |
|
||||
catch (RenderTargetCorruptedException ex) |
|
||||
{ |
|
||||
Logger.TryGet(LogEventLevel.Information, LogArea.Animations)?.Log(this, "Render target was corrupted. Exception: {0}", ex); |
|
||||
RenderTarget?.Dispose(); |
|
||||
RenderTarget = null; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private (IRef<Scene>? scene, bool updated) UpdateRenderLayersAndConsumeSceneIfNeeded(ref IDrawingContextImpl? context, |
|
||||
bool recursiveCall = false) |
|
||||
{ |
|
||||
IRef<Scene>? sceneRef; |
|
||||
lock (_sceneLock) |
|
||||
sceneRef = _scene?.Clone(); |
|
||||
if (sceneRef == null) |
|
||||
return (null, false); |
|
||||
using (sceneRef) |
|
||||
{ |
|
||||
var scene = sceneRef.Item; |
|
||||
if (scene.Generation != _lastSceneId) |
|
||||
{ |
|
||||
EnsureDrawingContext(ref context); |
|
||||
|
|
||||
Layers.Update(scene, context); |
|
||||
|
|
||||
RenderToLayers(scene); |
|
||||
|
|
||||
if (DebugFramesPath != null) |
|
||||
{ |
|
||||
SaveDebugFrames(scene.Generation); |
|
||||
} |
|
||||
|
|
||||
lock (_sceneLock) |
|
||||
_lastSceneId = scene.Generation; |
|
||||
|
|
||||
|
|
||||
var isUiThread = Dispatcher.UIThread.CheckAccess(); |
|
||||
// We have consumed the previously available scene, but there might be some dirty
|
|
||||
// rects since the last update. *If* we are on UI thread, we can force immediate scene
|
|
||||
// rebuild before rendering anything on-screen
|
|
||||
// We are calling the same method recursively here
|
|
||||
if (!recursiveCall && isUiThread && NeedsUpdate) |
|
||||
{ |
|
||||
UpdateScene(); |
|
||||
var (rs, _) = UpdateRenderLayersAndConsumeSceneIfNeeded(ref context, true); |
|
||||
return (rs, true); |
|
||||
} |
|
||||
|
|
||||
// We are rendering a new scene version, so it's highly likely
|
|
||||
// that there is already a pending update for animations
|
|
||||
// So we are scheduling an update call so UI thread could prepare a scene before
|
|
||||
// the next render timer tick
|
|
||||
if (!recursiveCall && !isUiThread) |
|
||||
Dispatcher.UIThread.Post(_updateSceneIfNeededDelegate, DispatcherPriority.Render); |
|
||||
|
|
||||
// Indicate that we have updated the layers
|
|
||||
return (sceneRef.Clone(), true); |
|
||||
} |
|
||||
|
|
||||
// Just return scene, layers weren't updated
|
|
||||
return (sceneRef.Clone(), false); |
|
||||
} |
|
||||
|
|
||||
} |
|
||||
|
|
||||
|
|
||||
private void Render(IDrawingContextImpl context, VisualNode node, Visual? layer, Rect clipBounds) |
|
||||
{ |
|
||||
if (layer == null || node.LayerRoot == layer) |
|
||||
{ |
|
||||
clipBounds = node.ClipBounds.Intersect(clipBounds); |
|
||||
|
|
||||
if (!clipBounds.IsDefault && node.Opacity > 0) |
|
||||
{ |
|
||||
var isLayerRoot = node.Visual == layer; |
|
||||
|
|
||||
node.BeginRender(context, isLayerRoot); |
|
||||
|
|
||||
var drawOperations = node.DrawOperations; |
|
||||
var drawOperationsCount = drawOperations.Count; |
|
||||
for (int i = 0; i < drawOperationsCount; i++) |
|
||||
{ |
|
||||
var operation = drawOperations[i]; |
|
||||
_currentDraw = operation; |
|
||||
operation.Item.Render(context); |
|
||||
_currentDraw = null; |
|
||||
} |
|
||||
|
|
||||
var children = node.Children; |
|
||||
var childrenCount = children.Count; |
|
||||
for (int i = 0; i < childrenCount; i++) |
|
||||
{ |
|
||||
var child = children[i]; |
|
||||
Render(context, (VisualNode)child, layer, clipBounds); |
|
||||
} |
|
||||
|
|
||||
node.EndRender(context, isLayerRoot); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void RenderToLayers(Scene scene) |
|
||||
{ |
|
||||
foreach (var layer in scene.Layers) |
|
||||
{ |
|
||||
var renderLayer = Layers[layer.LayerRoot]; |
|
||||
if (layer.Dirty.IsEmpty && !renderLayer.IsEmpty) |
|
||||
continue; |
|
||||
var renderTarget = renderLayer.Bitmap; |
|
||||
var node = (VisualNode?)scene.FindNode(layer.LayerRoot); |
|
||||
|
|
||||
if (node != null) |
|
||||
{ |
|
||||
using (var context = renderTarget.Item.CreateDrawingContext(this)) |
|
||||
{ |
|
||||
if (renderLayer.IsEmpty) |
|
||||
{ |
|
||||
// Render entire layer root node
|
|
||||
context.Clear(Colors.Transparent); |
|
||||
context.Transform = Matrix.Identity; |
|
||||
context.PushClip(node.ClipBounds); |
|
||||
Render(context, node, layer.LayerRoot, node.ClipBounds); |
|
||||
context.PopClip(); |
|
||||
if (DrawDirtyRects) |
|
||||
{ |
|
||||
_dirtyRectsDisplay.Add(node.ClipBounds); |
|
||||
} |
|
||||
|
|
||||
renderLayer.IsEmpty = false; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
var scale = scene.Scaling; |
|
||||
|
|
||||
foreach (var rect in layer.Dirty) |
|
||||
{ |
|
||||
var snappedRect = SnapToDevicePixels(rect, scale); |
|
||||
context.Transform = Matrix.Identity; |
|
||||
context.PushClip(snappedRect); |
|
||||
context.Clear(Colors.Transparent); |
|
||||
Render(context, node, layer.LayerRoot, snappedRect); |
|
||||
context.PopClip(); |
|
||||
|
|
||||
if (DrawDirtyRects) |
|
||||
{ |
|
||||
_dirtyRectsDisplay.Add(snappedRect); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private static Rect SnapToDevicePixels(Rect rect, double scale) |
|
||||
{ |
|
||||
return new Rect( |
|
||||
new Point( |
|
||||
Math.Floor(rect.X * scale) / scale, |
|
||||
Math.Floor(rect.Y * scale) / scale), |
|
||||
new Point( |
|
||||
Math.Ceiling(rect.Right * scale) / scale, |
|
||||
Math.Ceiling(rect.Bottom * scale) / scale)); |
|
||||
} |
|
||||
|
|
||||
private void RenderOverlay(Scene scene, ref IDrawingContextImpl? parentContent) |
|
||||
{ |
|
||||
EnsureDrawingContext(ref parentContent); |
|
||||
|
|
||||
if (DrawDirtyRects) |
|
||||
{ |
|
||||
var overlay = GetOverlay(parentContent, scene.Size, scene.Scaling); |
|
||||
|
|
||||
using (var context = overlay.Item.CreateDrawingContext(this)) |
|
||||
{ |
|
||||
context.Clear(Colors.Transparent); |
|
||||
RenderDirtyRects(context); |
|
||||
} |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
_overlay?.Dispose(); |
|
||||
_overlay = null; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void RenderDirtyRects(IDrawingContextImpl context) |
|
||||
{ |
|
||||
foreach (var r in _dirtyRectsDisplay) |
|
||||
{ |
|
||||
var brush = new ImmutableSolidColorBrush(Colors.Magenta, r.Opacity); |
|
||||
context.DrawRectangle(brush,null, r.Rect); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void RenderComposite(Scene scene, ref IDrawingContextImpl? context) |
|
||||
{ |
|
||||
EnsureDrawingContext(ref context); |
|
||||
|
|
||||
context.Clear(Colors.Transparent); |
|
||||
|
|
||||
var clientRect = new Rect(scene.Size); |
|
||||
|
|
||||
var firstLayer = true; |
|
||||
foreach (var layer in scene.Layers) |
|
||||
{ |
|
||||
var bitmap = Layers[layer.LayerRoot].Bitmap; |
|
||||
var sourceRect = new Rect(0, 0, bitmap.Item.PixelSize.Width, bitmap.Item.PixelSize.Height); |
|
||||
|
|
||||
if (layer.GeometryClip != null) |
|
||||
{ |
|
||||
context.PushGeometryClip(layer.GeometryClip); |
|
||||
} |
|
||||
|
|
||||
if (layer.OpacityMask == null) |
|
||||
{ |
|
||||
if (firstLayer && bitmap.Item.CanBlit) |
|
||||
bitmap.Item.Blit(context); |
|
||||
else |
|
||||
context.DrawBitmap(bitmap, layer.Opacity, sourceRect, clientRect); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
context.DrawBitmap(bitmap, layer.OpacityMask, layer.OpacityMaskRect, sourceRect); |
|
||||
} |
|
||||
|
|
||||
if (layer.GeometryClip != null) |
|
||||
{ |
|
||||
context.PopGeometryClip(); |
|
||||
} |
|
||||
|
|
||||
firstLayer = false; |
|
||||
} |
|
||||
|
|
||||
if (_overlay != null) |
|
||||
{ |
|
||||
var sourceRect = new Rect(0, 0, _overlay.Item.PixelSize.Width, _overlay.Item.PixelSize.Height); |
|
||||
context.DrawBitmap(_overlay, 0.5, sourceRect, clientRect); |
|
||||
} |
|
||||
|
|
||||
if (DrawFps) |
|
||||
{ |
|
||||
using (var c = new DrawingContext(context, false)) |
|
||||
{ |
|
||||
RenderFps(c, clientRect, scene.Layers.Count); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void EnsureDrawingContext([NotNull] ref IDrawingContextImpl? context) |
|
||||
{ |
|
||||
if (context != null) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
if (RenderTarget?.IsCorrupted == true) |
|
||||
{ |
|
||||
RenderTarget!.Dispose(); |
|
||||
RenderTarget = null; |
|
||||
} |
|
||||
|
|
||||
if (RenderTarget == null) |
|
||||
{ |
|
||||
RenderTarget = _renderTargetFactory!(); |
|
||||
} |
|
||||
|
|
||||
context = RenderTarget.CreateDrawingContext(this); |
|
||||
} |
|
||||
|
|
||||
private void UpdateSceneIfNeeded() |
|
||||
{ |
|
||||
if(NeedsUpdate) |
|
||||
UpdateScene(); |
|
||||
} |
|
||||
|
|
||||
private bool? UpdateScene() |
|
||||
{ |
|
||||
Dispatcher.UIThread.VerifyAccess(); |
|
||||
using var noPump = NonPumpingLockHelper.Use(); |
|
||||
lock (_sceneLock) |
|
||||
{ |
|
||||
if (_disposed) |
|
||||
return null; |
|
||||
if (_scene?.Item.Generation > _lastSceneId) |
|
||||
return false; |
|
||||
} |
|
||||
if (_root.IsVisible) |
|
||||
{ |
|
||||
var sceneRef = RefCountable.Create(_scene?.Item.CloneScene() ?? new Scene(_root) |
|
||||
{ |
|
||||
RenderThreadJobs = _pendingRenderThreadJobs |
|
||||
}); |
|
||||
_pendingRenderThreadJobs = null; |
|
||||
var scene = sceneRef.Item; |
|
||||
|
|
||||
if (_dirty == null) |
|
||||
{ |
|
||||
_dirty = new DirtyVisuals(); |
|
||||
_recalculateChildren = new HashSet<Visual>(); |
|
||||
_sceneBuilder.UpdateAll(scene); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
foreach (var visual in _recalculateChildren!) |
|
||||
{ |
|
||||
var node = scene.FindNode(visual); |
|
||||
((VisualNode?)node)?.SortChildren(scene); |
|
||||
} |
|
||||
|
|
||||
_recalculateChildren.Clear(); |
|
||||
|
|
||||
foreach (var visual in _dirty) |
|
||||
{ |
|
||||
_sceneBuilder.Update(scene, visual); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
lock (_sceneLock) |
|
||||
{ |
|
||||
var oldScene = _scene; |
|
||||
_scene = sceneRef; |
|
||||
oldScene?.Dispose(); |
|
||||
} |
|
||||
|
|
||||
_dirty.Clear(); |
|
||||
|
|
||||
if (SceneInvalidated != null) |
|
||||
{ |
|
||||
var rect = new Rect(); |
|
||||
|
|
||||
foreach (var layer in scene.Layers) |
|
||||
{ |
|
||||
foreach (var dirty in layer.Dirty) |
|
||||
{ |
|
||||
rect = rect.Union(dirty); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
SceneInvalidated(this, new SceneInvalidatedEventArgs((IRenderRoot)_root, rect)); |
|
||||
} |
|
||||
|
|
||||
return true; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
lock (_sceneLock) |
|
||||
{ |
|
||||
var oldScene = _scene; |
|
||||
_scene = null; |
|
||||
oldScene?.Dispose(); |
|
||||
} |
|
||||
|
|
||||
return null; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private IRef<IRenderTargetBitmapImpl> GetOverlay( |
|
||||
IDrawingContextImpl parentContext, |
|
||||
Size size, |
|
||||
double scaling) |
|
||||
{ |
|
||||
var pixelSize = size * scaling; |
|
||||
|
|
||||
if (_overlay == null || |
|
||||
_overlay.Item.PixelSize.Width != pixelSize.Width || |
|
||||
_overlay.Item.PixelSize.Height != pixelSize.Height) |
|
||||
{ |
|
||||
_overlay?.Dispose(); |
|
||||
_overlay = RefCountable.Create(parentContext.CreateLayer(size)); |
|
||||
} |
|
||||
|
|
||||
return _overlay; |
|
||||
} |
|
||||
|
|
||||
private void SaveDebugFrames(int id) |
|
||||
{ |
|
||||
var index = 0; |
|
||||
|
|
||||
foreach (var layer in Layers) |
|
||||
{ |
|
||||
var fileName = Path.Combine(DebugFramesPath ?? string.Empty, FormattableString.Invariant($"frame-{id}-layer-{index++}.png")); |
|
||||
layer.Bitmap.Item.Save(fileName); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,16 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using Avalonia.VisualTree; |
||||
|
|
||||
|
namespace Avalonia.Rendering |
||||
|
{ |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Allows customization of hit-testing
|
||||
|
/// </summary>
|
||||
|
public interface ICustomHitTest |
||||
|
{ |
||||
|
/// <param name="point">The point to hit test in global coordinate space.</param>
|
||||
|
bool HitTest(Point point); |
||||
|
} |
||||
|
} |
||||
@ -1,33 +0,0 @@ |
|||||
using System.Collections.Generic; |
|
||||
using System.Linq; |
|
||||
using Avalonia.VisualTree; |
|
||||
|
|
||||
namespace Avalonia.Rendering |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// An interface to allow non-templated controls to customize their hit-testing
|
|
||||
/// when using a renderer with a simple hit-testing algorithm without a scene graph,
|
|
||||
/// such as <see cref="ImmediateRenderer" />
|
|
||||
/// </summary>
|
|
||||
public interface ICustomSimpleHitTest |
|
||||
{ |
|
||||
/// <param name="point">The point to hit test in global coordinate space.</param>
|
|
||||
bool HitTest(Point point); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Allows customization of hit-testing for all renderers.
|
|
||||
/// </summary>
|
|
||||
public interface ICustomHitTest : ICustomSimpleHitTest |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public static class CustomSimpleHitTestExtensions |
|
||||
{ |
|
||||
public static bool HitTestCustom(this Visual visual, Point point) |
|
||||
=> (visual as ICustomSimpleHitTest)?.HitTest(point) ?? visual.TransformedBounds?.Contains(point) == true; |
|
||||
|
|
||||
public static bool HitTestCustom(this IEnumerable<Visual> children, Point point) |
|
||||
=> children.Any(ctrl => ctrl.HitTestCustom(point)); |
|
||||
} |
|
||||
} |
|
||||
@ -1,16 +0,0 @@ |
|||||
|
|
||||
namespace Avalonia.Rendering |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Defines the interface for a renderer factory.
|
|
||||
/// </summary>
|
|
||||
public interface IRendererFactory |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Creates a renderer.
|
|
||||
/// </summary>
|
|
||||
/// <param name="root">The root visual.</param>
|
|
||||
/// <param name="renderLoop">The render loop.</param>
|
|
||||
IRenderer Create(IRenderRoot root, IRenderLoop renderLoop); |
|
||||
} |
|
||||
} |
|
||||
@ -1,48 +0,0 @@ |
|||||
using Avalonia.Media; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.Utilities; |
|
||||
using Avalonia.VisualTree; |
|
||||
|
|
||||
namespace Avalonia.Rendering |
|
||||
{ |
|
||||
public class RenderLayer |
|
||||
{ |
|
||||
public RenderLayer( |
|
||||
IDrawingContextImpl drawingContext, |
|
||||
Size size, |
|
||||
double scaling, |
|
||||
Visual layerRoot) |
|
||||
{ |
|
||||
Bitmap = RefCountable.Create(drawingContext.CreateLayer(size)); |
|
||||
Size = size; |
|
||||
Scaling = scaling; |
|
||||
LayerRoot = layerRoot; |
|
||||
IsEmpty = true; |
|
||||
} |
|
||||
|
|
||||
public IRef<IDrawingContextLayerImpl> Bitmap { get; private set; } |
|
||||
public bool IsEmpty { get; set; } |
|
||||
public double Scaling { get; private set; } |
|
||||
public Size Size { get; private set; } |
|
||||
public Visual LayerRoot { get; } |
|
||||
|
|
||||
public void RecreateBitmap(IDrawingContextImpl drawingContext, Size size, double scaling) |
|
||||
{ |
|
||||
if (Size != size || Scaling != scaling) |
|
||||
{ |
|
||||
var resized = RefCountable.Create(drawingContext.CreateLayer(size)); |
|
||||
|
|
||||
using (var context = resized.Item.CreateDrawingContext(null)) |
|
||||
{ |
|
||||
Bitmap.Dispose(); |
|
||||
context.Clear(default); |
|
||||
|
|
||||
Bitmap = resized; |
|
||||
Scaling = scaling; |
|
||||
Size = size; |
|
||||
IsEmpty = true; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,69 +0,0 @@ |
|||||
using System.Collections; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Diagnostics.CodeAnalysis; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.Rendering.SceneGraph; |
|
||||
|
|
||||
namespace Avalonia.Rendering |
|
||||
{ |
|
||||
public class RenderLayers : IEnumerable<RenderLayer> |
|
||||
{ |
|
||||
private readonly List<RenderLayer> _inner = new List<RenderLayer>(); |
|
||||
private readonly Dictionary<Visual, RenderLayer> _index = new Dictionary<Visual, RenderLayer>(); |
|
||||
|
|
||||
public int Count => _inner.Count; |
|
||||
public RenderLayer this[Visual layerRoot] => _index[layerRoot]; |
|
||||
|
|
||||
public void Update(Scene scene, IDrawingContextImpl context) |
|
||||
{ |
|
||||
for (var i = scene.Layers.Count - 1; i >= 0; --i) |
|
||||
{ |
|
||||
var src = scene.Layers[i]; |
|
||||
|
|
||||
if (!_index.TryGetValue(src.LayerRoot, out var layer)) |
|
||||
{ |
|
||||
layer = new RenderLayer(context, scene.Size, scene.Scaling, src.LayerRoot); |
|
||||
_inner.Add(layer); |
|
||||
_index.Add(src.LayerRoot, layer); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
layer.RecreateBitmap(context, scene.Size, scene.Scaling); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
for (var i = 0; i < _inner.Count;) |
|
||||
{ |
|
||||
var layer = _inner[i]; |
|
||||
|
|
||||
if (!scene.Layers.Exists(layer.LayerRoot)) |
|
||||
{ |
|
||||
layer.Bitmap.Dispose(); |
|
||||
_inner.RemoveAt(i); |
|
||||
_index.Remove(layer.LayerRoot); |
|
||||
} |
|
||||
else |
|
||||
i++; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public void Clear() |
|
||||
{ |
|
||||
foreach (var layer in _index.Values) |
|
||||
{ |
|
||||
layer.Bitmap.Dispose(); |
|
||||
} |
|
||||
|
|
||||
_index.Clear(); |
|
||||
_inner.Clear(); |
|
||||
} |
|
||||
|
|
||||
public bool TryGetValue(Visual layerRoot, [NotNullWhen(true)] out RenderLayer? value) |
|
||||
{ |
|
||||
return _index.TryGetValue(layerRoot, out value); |
|
||||
} |
|
||||
|
|
||||
public IEnumerator<RenderLayer> GetEnumerator() => _inner.GetEnumerator(); |
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|
||||
} |
|
||||
} |
|
||||
@ -1,482 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Numerics; |
|
||||
using Avalonia.Media; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.Utilities; |
|
||||
using Avalonia.Media.Imaging; |
|
||||
using Avalonia.VisualTree; |
|
||||
|
|
||||
namespace Avalonia.Rendering.SceneGraph |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// A drawing context which builds a scene graph.
|
|
||||
/// </summary>
|
|
||||
internal class DeferredDrawingContextImpl : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport |
|
||||
{ |
|
||||
private readonly ISceneBuilder _sceneBuilder; |
|
||||
private VisualNode? _node; |
|
||||
private int _childIndex; |
|
||||
private int _drawOperationindex; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="DeferredDrawingContextImpl"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="sceneBuilder">
|
|
||||
/// A scene builder used for constructing child scenes for visual brushes.
|
|
||||
/// </param>
|
|
||||
/// <param name="layers">The scene layers.</param>
|
|
||||
public DeferredDrawingContextImpl(ISceneBuilder sceneBuilder, SceneLayers layers) |
|
||||
{ |
|
||||
_sceneBuilder = sceneBuilder; |
|
||||
Layers = layers; |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public Matrix Transform { get; set; } = Matrix.Identity; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the layers in the scene being built.
|
|
||||
/// </summary>
|
|
||||
public SceneLayers Layers { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Informs the drawing context of the visual node that is about to be rendered.
|
|
||||
/// </summary>
|
|
||||
/// <param name="node">The visual node.</param>
|
|
||||
/// <returns>
|
|
||||
/// An object which when disposed will commit the changes to visual node.
|
|
||||
/// </returns>
|
|
||||
public UpdateState BeginUpdate(VisualNode node) |
|
||||
{ |
|
||||
_ = node ?? throw new ArgumentNullException(nameof(node)); |
|
||||
|
|
||||
if (_node != null) |
|
||||
{ |
|
||||
if (_childIndex < _node.Children.Count) |
|
||||
{ |
|
||||
_node.ReplaceChild(_childIndex, node); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
_node.AddChild(node); |
|
||||
} |
|
||||
|
|
||||
++_childIndex; |
|
||||
} |
|
||||
|
|
||||
var state = new UpdateState(this, _node, _childIndex, _drawOperationindex); |
|
||||
_node = node; |
|
||||
_childIndex = _drawOperationindex = 0; |
|
||||
return state; |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void Clear(Color color) |
|
||||
{ |
|
||||
// Cannot clear a deferred scene.
|
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
// Nothing to do here since we allocate no unmanaged resources.
|
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Removes any remaining drawing operations from the visual node.
|
|
||||
/// </summary>
|
|
||||
/// <remarks>
|
|
||||
/// Drawing operations are updated in place, overwriting existing drawing operations if
|
|
||||
/// they are different. Once drawing has completed for the current visual node, it is
|
|
||||
/// possible that there are stale drawing operations at the end of the list. This method
|
|
||||
/// trims these stale drawing operations.
|
|
||||
/// </remarks>
|
|
||||
public void TrimChildren() |
|
||||
{ |
|
||||
_node!.TrimChildren(_childIndex); |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry) |
|
||||
{ |
|
||||
var next = NextDrawAs<GeometryNode>(); |
|
||||
|
|
||||
if (next == null || !next.Item.Equals(Transform, brush, pen, geometry)) |
|
||||
{ |
|
||||
Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush))); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
++_drawOperationindex; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) |
|
||||
{ |
|
||||
var next = NextDrawAs<ImageNode>(); |
|
||||
|
|
||||
if (next == null || !next.Item.Equals(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)) |
|
||||
{ |
|
||||
Add(new ImageNode(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
++_drawOperationindex; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect) |
|
||||
{ |
|
||||
// This method is currently only used to composite layers so shouldn't be called here.
|
|
||||
throw new NotSupportedException(); |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void DrawLine(IPen pen, Point p1, Point p2) |
|
||||
{ |
|
||||
var next = NextDrawAs<LineNode>(); |
|
||||
|
|
||||
if (next == null || !next.Item.Equals(Transform, pen, p1, p2)) |
|
||||
{ |
|
||||
Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush))); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
++_drawOperationindex; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, |
|
||||
BoxShadows boxShadows = default) |
|
||||
{ |
|
||||
var next = NextDrawAs<RectangleNode>(); |
|
||||
|
|
||||
if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows)) |
|
||||
{ |
|
||||
Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush))); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
++_drawOperationindex; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) |
|
||||
{ |
|
||||
var next = NextDrawAs<ExperimentalAcrylicNode>(); |
|
||||
|
|
||||
if (next == null || !next.Item.Equals(Transform, material, rect)) |
|
||||
{ |
|
||||
Add(new ExperimentalAcrylicNode(Transform, material, rect)); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
++_drawOperationindex; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) |
|
||||
{ |
|
||||
var next = NextDrawAs<EllipseNode>(); |
|
||||
|
|
||||
if (next == null || !next.Item.Equals(Transform, brush, pen, rect)) |
|
||||
{ |
|
||||
Add(new EllipseNode(Transform, brush, pen, rect, CreateChildScene(brush))); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
++_drawOperationindex; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public void Custom(ICustomDrawOperation custom) |
|
||||
{ |
|
||||
var next = NextDrawAs<CustomDrawOperation>(); |
|
||||
if (next == null || !next.Item.Equals(Transform, custom)) |
|
||||
Add(new CustomDrawOperation(custom, Transform)); |
|
||||
else |
|
||||
++_drawOperationindex; |
|
||||
} |
|
||||
|
|
||||
public object? GetFeature(Type t) => null; |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void DrawGlyphRun(IBrush foreground, IRef<IGlyphRunImpl> glyphRun) |
|
||||
{ |
|
||||
var next = NextDrawAs<GlyphRunNode>(); |
|
||||
|
|
||||
if (next == null || !next.Item.Equals(Transform, foreground, glyphRun)) |
|
||||
{ |
|
||||
Add(new GlyphRunNode(Transform, foreground, glyphRun, CreateChildScene(foreground))); |
|
||||
} |
|
||||
|
|
||||
else |
|
||||
{ |
|
||||
++_drawOperationindex; |
|
||||
} |
|
||||
} |
|
||||
public IDrawingContextLayerImpl CreateLayer(Size size) |
|
||||
{ |
|
||||
throw new NotSupportedException("Creating layers on a deferred drawing context not supported"); |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void PopClip() |
|
||||
{ |
|
||||
var next = NextDrawAs<ClipNode>(); |
|
||||
|
|
||||
if (next == null || !next.Item.Equals(null)) |
|
||||
{ |
|
||||
Add(new ClipNode()); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
++_drawOperationindex; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void PopGeometryClip() |
|
||||
{ |
|
||||
var next = NextDrawAs<GeometryClipNode>(); |
|
||||
|
|
||||
if (next == null || !next.Item.Equals(null)) |
|
||||
{ |
|
||||
Add(new GeometryClipNode()); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
++_drawOperationindex; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void PopBitmapBlendMode() |
|
||||
{ |
|
||||
var next = NextDrawAs<BitmapBlendModeNode>(); |
|
||||
|
|
||||
if (next == null || !next.Item.Equals(null)) |
|
||||
{ |
|
||||
Add(new BitmapBlendModeNode()); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
++_drawOperationindex; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void PopOpacity() |
|
||||
{ |
|
||||
var next = NextDrawAs<OpacityNode>(); |
|
||||
|
|
||||
if (next == null || !next.Item.Equals(null)) |
|
||||
{ |
|
||||
Add(new OpacityNode()); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
++_drawOperationindex; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void PopOpacityMask() |
|
||||
{ |
|
||||
var next = NextDrawAs<OpacityMaskNode>(); |
|
||||
|
|
||||
if (next == null || !next.Item.Equals(null, null)) |
|
||||
{ |
|
||||
Add(new OpacityMaskNode()); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
++_drawOperationindex; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void PushClip(Rect clip) |
|
||||
{ |
|
||||
var next = NextDrawAs<ClipNode>(); |
|
||||
|
|
||||
if (next == null || !next.Item.Equals(Transform, clip)) |
|
||||
{ |
|
||||
Add(new ClipNode(Transform, clip)); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
++_drawOperationindex; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc />
|
|
||||
public void PushClip(RoundedRect clip) |
|
||||
{ |
|
||||
var next = NextDrawAs<ClipNode>(); |
|
||||
|
|
||||
if (next == null || !next.Item.Equals(Transform, clip)) |
|
||||
{ |
|
||||
Add(new ClipNode(Transform, clip)); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
++_drawOperationindex; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void PushGeometryClip(IGeometryImpl? clip) |
|
||||
{ |
|
||||
if (clip is null) |
|
||||
return; |
|
||||
|
|
||||
var next = NextDrawAs<GeometryClipNode>(); |
|
||||
|
|
||||
if (next == null || !next.Item.Equals(Transform, clip)) |
|
||||
{ |
|
||||
Add(new GeometryClipNode(Transform, clip)); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
++_drawOperationindex; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void PushOpacity(double opacity) |
|
||||
{ |
|
||||
var next = NextDrawAs<OpacityNode>(); |
|
||||
|
|
||||
if (next == null || !next.Item.Equals(opacity)) |
|
||||
{ |
|
||||
Add(new OpacityNode(opacity)); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
++_drawOperationindex; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void PushOpacityMask(IBrush mask, Rect bounds) |
|
||||
{ |
|
||||
var next = NextDrawAs<OpacityMaskNode>(); |
|
||||
|
|
||||
if (next == null || !next.Item.Equals(mask, bounds)) |
|
||||
{ |
|
||||
Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask))); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
++_drawOperationindex; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) |
|
||||
{ |
|
||||
var next = NextDrawAs<BitmapBlendModeNode>(); |
|
||||
|
|
||||
if (next == null || !next.Item.Equals(blendingMode)) |
|
||||
{ |
|
||||
Add(new BitmapBlendModeNode(blendingMode)); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
++_drawOperationindex; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public readonly struct UpdateState : IDisposable |
|
||||
{ |
|
||||
public UpdateState( |
|
||||
DeferredDrawingContextImpl owner, |
|
||||
VisualNode? node, |
|
||||
int childIndex, |
|
||||
int drawOperationIndex) |
|
||||
{ |
|
||||
Owner = owner; |
|
||||
Node = node; |
|
||||
ChildIndex = childIndex; |
|
||||
DrawOperationIndex = drawOperationIndex; |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
Owner._node!.TrimDrawOperations(Owner._drawOperationindex); |
|
||||
|
|
||||
var dirty = Owner.Layers.GetOrAdd(Owner._node.LayerRoot!).Dirty; |
|
||||
|
|
||||
var drawOperations = Owner._node.DrawOperations; |
|
||||
var drawOperationsCount = drawOperations.Count; |
|
||||
|
|
||||
for (var i = 0; i < drawOperationsCount; i++) |
|
||||
{ |
|
||||
dirty.Add(drawOperations[i].Item.Bounds); |
|
||||
} |
|
||||
|
|
||||
Owner._node = Node; |
|
||||
Owner._childIndex = ChildIndex; |
|
||||
Owner._drawOperationindex = DrawOperationIndex; |
|
||||
} |
|
||||
|
|
||||
public DeferredDrawingContextImpl Owner { get; } |
|
||||
public VisualNode? Node { get; } |
|
||||
public int ChildIndex { get; } |
|
||||
public int DrawOperationIndex { get; } |
|
||||
} |
|
||||
|
|
||||
private void Add<T>(T node) where T : class, IDrawOperation |
|
||||
{ |
|
||||
using (var refCounted = RefCountable.Create(node)) |
|
||||
{ |
|
||||
Add(refCounted); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private void Add(IRef<IDrawOperation> node) |
|
||||
{ |
|
||||
if (_drawOperationindex < _node!.DrawOperations.Count) |
|
||||
{ |
|
||||
_node.ReplaceDrawOperation(_drawOperationindex, node); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
_node.AddDrawOperation(node); |
|
||||
} |
|
||||
|
|
||||
++_drawOperationindex; |
|
||||
} |
|
||||
|
|
||||
private IRef<T>? NextDrawAs<T>() where T : class, IDrawOperation |
|
||||
{ |
|
||||
return _drawOperationindex < _node!.DrawOperations.Count ? _node.DrawOperations[_drawOperationindex] as IRef<T> : null; |
|
||||
} |
|
||||
|
|
||||
private IDisposable? CreateChildScene(IBrush? brush) |
|
||||
{ |
|
||||
var visualBrush = brush as VisualBrush; |
|
||||
|
|
||||
if (visualBrush != null) |
|
||||
{ |
|
||||
var visual = visualBrush.Visual; |
|
||||
|
|
||||
if (visual != null) |
|
||||
{ |
|
||||
(visual as IVisualBrushInitialize)?.EnsureInitialized(); |
|
||||
var scene = new Scene(visual); |
|
||||
_sceneBuilder.UpdateAll(scene); |
|
||||
return scene; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return null; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,24 +0,0 @@ |
|||||
using Avalonia.VisualTree; |
|
||||
|
|
||||
namespace Avalonia.Rendering.SceneGraph |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Builds a scene graph from a visual tree.
|
|
||||
/// </summary>
|
|
||||
public interface ISceneBuilder |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Builds the initial scene graph for a visual tree.
|
|
||||
/// </summary>
|
|
||||
/// <param name="scene">The scene to build.</param>
|
|
||||
void UpdateAll(Scene scene); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Updates the visual (and potentially its children) in a scene.
|
|
||||
/// </summary>
|
|
||||
/// <param name="scene">The scene.</param>
|
|
||||
/// <param name="visual">The visual to update.</param>
|
|
||||
/// <returns>True if changes were made, otherwise false.</returns>
|
|
||||
bool Update(Scene scene, Visual visual); |
|
||||
} |
|
||||
} |
|
||||
@ -1,105 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.Utilities; |
|
||||
|
|
||||
namespace Avalonia.Rendering.SceneGraph |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Represents a node in the low-level scene graph representing a <see cref="Visual"/>.
|
|
||||
/// </summary>
|
|
||||
public interface IVisualNode : IDisposable |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Gets the visual to which the node relates.
|
|
||||
/// </summary>
|
|
||||
Visual Visual { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the parent scene graph node.
|
|
||||
/// </summary>
|
|
||||
IVisualNode? Parent { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the transform for the node from global to control coordinates.
|
|
||||
/// </summary>
|
|
||||
Matrix Transform { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the corner radius of visual. Contents are clipped to this radius.
|
|
||||
/// </summary>
|
|
||||
CornerRadius ClipToBoundsRadius { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the bounds of the node's geometry in global coordinates.
|
|
||||
/// </summary>
|
|
||||
Rect Bounds { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the clip bounds for the node in global coordinates.
|
|
||||
/// </summary>
|
|
||||
Rect ClipBounds { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the layout bounds for the node in global coordinates.
|
|
||||
/// </summary>
|
|
||||
Rect LayoutBounds { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Whether the node is clipped to <see cref="ClipBounds"/>.
|
|
||||
/// </summary>
|
|
||||
bool ClipToBounds { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the node's clip geometry, if any.
|
|
||||
/// </summary>
|
|
||||
IGeometryImpl? GeometryClip { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets a value indicating whether one of the node's ancestors has a geometry clip.
|
|
||||
/// </summary>
|
|
||||
bool HasAncestorGeometryClip { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the child scene graph nodes.
|
|
||||
/// </summary>
|
|
||||
IReadOnlyList<IVisualNode> Children { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the drawing operations for the visual.
|
|
||||
/// </summary>
|
|
||||
IReadOnlyList<IRef<IDrawOperation>> DrawOperations { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the opacity of the scene graph node.
|
|
||||
/// </summary>
|
|
||||
double Opacity { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Sets up the drawing context for rendering the node's geometry.
|
|
||||
/// </summary>
|
|
||||
/// <param name="context">The drawing context.</param>
|
|
||||
/// <param name="skipOpacity">Whether to skip pushing the control's opacity.</param>
|
|
||||
void BeginRender(IDrawingContextImpl context, bool skipOpacity); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Resets the drawing context after rendering the node's geometry.
|
|
||||
/// </summary>
|
|
||||
/// <param name="context">The drawing context.</param>
|
|
||||
/// <param name="skipOpacity">Whether to skip popping the control's opacity.</param>
|
|
||||
void EndRender(IDrawingContextImpl context, bool skipOpacity); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Hit test the geometry in this node.
|
|
||||
/// </summary>
|
|
||||
/// <param name="p">The point in global coordinates.</param>
|
|
||||
/// <returns>True if the point hits the node's geometry; otherwise false.</returns>
|
|
||||
/// <remarks>
|
|
||||
/// This method does not recurse to child <see cref="IVisualNode"/>s, if you want
|
|
||||
/// to hit test children they must be hit tested manually.
|
|
||||
/// </remarks>
|
|
||||
bool HitTest(Point p); |
|
||||
|
|
||||
bool Disposed { get; } |
|
||||
} |
|
||||
} |
|
||||
@ -1,352 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Linq; |
|
||||
using System.Threading.Tasks; |
|
||||
using Avalonia.Collections.Pooled; |
|
||||
using Avalonia.VisualTree; |
|
||||
|
|
||||
namespace Avalonia.Rendering.SceneGraph |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Represents a scene graph used by the <see cref="DeferredRenderer"/>.
|
|
||||
/// </summary>
|
|
||||
public class Scene : IDisposable |
|
||||
{ |
|
||||
private readonly Dictionary<Visual, IVisualNode> _index; |
|
||||
private readonly TaskCompletionSource<bool> _rendered = new TaskCompletionSource<bool>(); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="Scene"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="rootVisual">The root visual to draw.</param>
|
|
||||
public Scene(Visual rootVisual) |
|
||||
: this( |
|
||||
new VisualNode(rootVisual, null), |
|
||||
new Dictionary<Visual, IVisualNode>(), |
|
||||
new SceneLayers(rootVisual), |
|
||||
0) |
|
||||
{ |
|
||||
_index.Add(rootVisual, Root); |
|
||||
} |
|
||||
|
|
||||
private Scene(VisualNode root, Dictionary<Visual, IVisualNode> index, SceneLayers layers, int generation) |
|
||||
{ |
|
||||
_ = root ?? throw new ArgumentNullException(nameof(root)); |
|
||||
|
|
||||
var renderRoot = root.Visual as IRenderRoot; |
|
||||
|
|
||||
_index = index; |
|
||||
Root = root; |
|
||||
Layers = layers; |
|
||||
Generation = generation; |
|
||||
root.LayerRoot = root.Visual; |
|
||||
} |
|
||||
|
|
||||
public Task Rendered => _rendered.Task; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets a value identifying the scene's generation. This is incremented each time the scene is cloned.
|
|
||||
/// </summary>
|
|
||||
public int Generation { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the layers for the scene.
|
|
||||
/// </summary>
|
|
||||
public SceneLayers Layers { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the root node of the scene graph.
|
|
||||
/// </summary>
|
|
||||
public IVisualNode Root { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the size of the scene in device independent pixels.
|
|
||||
/// </summary>
|
|
||||
public Size Size { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the scene scaling.
|
|
||||
/// </summary>
|
|
||||
public double Scaling { get; set; } = 1; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Adds a node to the scene index.
|
|
||||
/// </summary>
|
|
||||
/// <param name="node">The node.</param>
|
|
||||
public void Add(IVisualNode node) |
|
||||
{ |
|
||||
_ = node ?? throw new ArgumentNullException(nameof(node)); |
|
||||
|
|
||||
_index.Add(node.Visual, node); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Clones the scene.
|
|
||||
/// </summary>
|
|
||||
/// <returns>The cloned scene.</returns>
|
|
||||
public Scene CloneScene() |
|
||||
{ |
|
||||
var index = new Dictionary<Visual, IVisualNode>(_index.Count); |
|
||||
var root = Clone((VisualNode)Root, null, index); |
|
||||
|
|
||||
var result = new Scene(root, index, Layers.Clone(), Generation + 1) |
|
||||
{ |
|
||||
Size = Size, |
|
||||
Scaling = Scaling, |
|
||||
}; |
|
||||
|
|
||||
return result; |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
_rendered.TrySetResult(false); |
|
||||
foreach (var node in _index.Values) |
|
||||
{ |
|
||||
node.Dispose(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Tries to find a node in the scene graph representing the specified visual.
|
|
||||
/// </summary>
|
|
||||
/// <param name="visual">The visual.</param>
|
|
||||
/// <returns>
|
|
||||
/// The node representing the visual or null if it could not be found.
|
|
||||
/// </returns>
|
|
||||
public IVisualNode? FindNode(Visual visual) |
|
||||
{ |
|
||||
_index.TryGetValue(visual, out var node); |
|
||||
return node; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the visuals at a point in the scene.
|
|
||||
/// </summary>
|
|
||||
/// <param name="p">The point.</param>
|
|
||||
/// <param name="root">The root of the subtree to search.</param>
|
|
||||
/// <param name="filter">A filter. May be null.</param>
|
|
||||
/// <returns>The visuals at the specified point.</returns>
|
|
||||
public IEnumerable<Visual> HitTest(Point p, Visual root, Func<Visual, bool>? filter) |
|
||||
{ |
|
||||
var node = FindNode(root); |
|
||||
return (node != null) ? new HitTestEnumerable(node, filter, p, Root) : Enumerable.Empty<Visual>(); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the visual at a point in the scene.
|
|
||||
/// </summary>
|
|
||||
/// <param name="p">The point.</param>
|
|
||||
/// <param name="root">The root of the subtree to search.</param>
|
|
||||
/// <param name="filter">A filter. May be null.</param>
|
|
||||
/// <returns>The visual at the specified point.</returns>
|
|
||||
public Visual? HitTestFirst(Point p, Visual root, Func<Visual, bool>? filter) |
|
||||
{ |
|
||||
var node = FindNode(root); |
|
||||
return (node != null) ? HitTestFirst(node, p, filter) : null; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Removes a node from the scene index.
|
|
||||
/// </summary>
|
|
||||
/// <param name="node">The node.</param>
|
|
||||
public void Remove(IVisualNode node) |
|
||||
{ |
|
||||
_ = node ?? throw new ArgumentNullException(nameof(node)); |
|
||||
|
|
||||
_index.Remove(node.Visual); |
|
||||
|
|
||||
node.Dispose(); |
|
||||
} |
|
||||
|
|
||||
private VisualNode Clone(VisualNode source, IVisualNode? parent, Dictionary<Visual, IVisualNode> index) |
|
||||
{ |
|
||||
var result = source.Clone(parent); |
|
||||
|
|
||||
index.Add(result.Visual, result); |
|
||||
|
|
||||
var children = source.Children; |
|
||||
var childrenCount = children.Count; |
|
||||
|
|
||||
if (childrenCount > 0) |
|
||||
{ |
|
||||
result.TryPreallocateChildren(childrenCount); |
|
||||
|
|
||||
for (var i = 0; i < childrenCount; i++) |
|
||||
{ |
|
||||
var child = children[i]; |
|
||||
|
|
||||
result.AddChild(Clone((VisualNode)child, result, index)); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return result; |
|
||||
} |
|
||||
|
|
||||
private Visual HitTestFirst(IVisualNode root, Point p, Func<Visual, bool>? filter) |
|
||||
{ |
|
||||
using var enumerator = new HitTestEnumerator(root, filter, p, Root); |
|
||||
|
|
||||
enumerator.MoveNext(); |
|
||||
|
|
||||
return enumerator.Current; |
|
||||
} |
|
||||
|
|
||||
private class HitTestEnumerable : IEnumerable<Visual> |
|
||||
{ |
|
||||
private readonly IVisualNode _root; |
|
||||
private readonly Func<Visual, bool>? _filter; |
|
||||
private readonly IVisualNode _sceneRoot; |
|
||||
private readonly Point _point; |
|
||||
|
|
||||
public HitTestEnumerable(IVisualNode root, Func<Visual, bool>? filter, Point point, IVisualNode sceneRoot) |
|
||||
{ |
|
||||
_root = root; |
|
||||
_filter = filter; |
|
||||
_point = point; |
|
||||
_sceneRoot = sceneRoot; |
|
||||
} |
|
||||
|
|
||||
public IEnumerator<Visual> GetEnumerator() |
|
||||
{ |
|
||||
return new HitTestEnumerator(_root, _filter, _point, _sceneRoot); |
|
||||
} |
|
||||
|
|
||||
IEnumerator IEnumerable.GetEnumerator() |
|
||||
{ |
|
||||
return GetEnumerator(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private struct HitTestEnumerator : IEnumerator<Visual> |
|
||||
{ |
|
||||
private readonly PooledStack<Entry> _nodeStack; |
|
||||
private readonly Func<Visual, bool>? _filter; |
|
||||
private readonly IVisualNode _sceneRoot; |
|
||||
private Visual? _current; |
|
||||
private readonly Point _point; |
|
||||
|
|
||||
public HitTestEnumerator(IVisualNode root, Func<Visual, bool>? filter, Point point, IVisualNode sceneRoot) |
|
||||
{ |
|
||||
_nodeStack = new PooledStack<Entry>(); |
|
||||
_nodeStack.Push(new Entry(root, false, null, true)); |
|
||||
|
|
||||
_filter = filter; |
|
||||
_point = point; |
|
||||
_sceneRoot = sceneRoot; |
|
||||
|
|
||||
_current = null; |
|
||||
} |
|
||||
|
|
||||
public bool MoveNext() |
|
||||
{ |
|
||||
while (_nodeStack.Count > 0) |
|
||||
{ |
|
||||
(var wasVisited, var isRoot, IVisualNode node, Rect? clip) = _nodeStack.Pop(); |
|
||||
|
|
||||
if (wasVisited && isRoot) |
|
||||
{ |
|
||||
break; |
|
||||
} |
|
||||
|
|
||||
var children = node.Children; |
|
||||
int childCount = children.Count; |
|
||||
|
|
||||
if (childCount == 0 || wasVisited) |
|
||||
{ |
|
||||
if ((wasVisited || FilterAndClip(node, ref clip)) && |
|
||||
(node.Visual is ICustomHitTest custom ? custom.HitTest(_point) : node.HitTest(_point))) |
|
||||
{ |
|
||||
_current = node.Visual; |
|
||||
|
|
||||
return true; |
|
||||
} |
|
||||
} |
|
||||
else if (FilterAndClip(node, ref clip)) |
|
||||
{ |
|
||||
_nodeStack.Push(new Entry(node, true, null)); |
|
||||
|
|
||||
for (var i = 0; i < childCount; i++) |
|
||||
{ |
|
||||
_nodeStack.Push(new Entry(children[i], false, clip)); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
public void Reset() |
|
||||
{ |
|
||||
throw new NotSupportedException(); |
|
||||
} |
|
||||
|
|
||||
public Visual Current => _current!; |
|
||||
|
|
||||
object IEnumerator.Current => Current; |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
_nodeStack.Dispose(); |
|
||||
} |
|
||||
|
|
||||
private bool FilterAndClip(IVisualNode node, ref Rect? clip) |
|
||||
{ |
|
||||
if (_filter?.Invoke(node.Visual) != false && node.Visual.IsAttachedToVisualTree) |
|
||||
{ |
|
||||
var clipped = false; |
|
||||
|
|
||||
if (node.ClipToBounds) |
|
||||
{ |
|
||||
clip = clip == null ? node.ClipBounds : clip.Value.Intersect(node.ClipBounds); |
|
||||
clipped = !clip.Value.ContainsExclusive(_point); |
|
||||
} |
|
||||
|
|
||||
if (node.GeometryClip != null) |
|
||||
{ |
|
||||
var controlPoint = _sceneRoot.Visual.TranslatePoint(_point, node.Visual); |
|
||||
clipped = !node.GeometryClip.FillContains(controlPoint!.Value); |
|
||||
} |
|
||||
|
|
||||
if (!clipped && node.Visual is ICustomHitTest custom) |
|
||||
{ |
|
||||
clipped = !custom.HitTest(_point); |
|
||||
} |
|
||||
|
|
||||
return !clipped; |
|
||||
} |
|
||||
|
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
private readonly struct Entry |
|
||||
{ |
|
||||
public readonly bool WasVisited; |
|
||||
public readonly bool IsRoot; |
|
||||
public readonly IVisualNode Node; |
|
||||
public readonly Rect? Clip; |
|
||||
|
|
||||
public Entry(IVisualNode node, bool wasVisited, Rect? clip, bool isRoot = false) |
|
||||
{ |
|
||||
Node = node; |
|
||||
WasVisited = wasVisited; |
|
||||
IsRoot = isRoot; |
|
||||
Clip = clip; |
|
||||
} |
|
||||
|
|
||||
public void Deconstruct(out bool wasVisited, out bool isRoot, out IVisualNode node, out Rect? clip) |
|
||||
{ |
|
||||
wasVisited = WasVisited; |
|
||||
isRoot = IsRoot; |
|
||||
node = Node; |
|
||||
clip = Clip; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public void MarkAsRendered() => _rendered.TrySetResult(true); |
|
||||
|
|
||||
public List<Action>? RenderThreadJobs { get; set; } |
|
||||
} |
|
||||
} |
|
||||
@ -1,485 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using Avalonia.Media; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.Threading; |
|
||||
using Avalonia.VisualTree; |
|
||||
|
|
||||
namespace Avalonia.Rendering.SceneGraph |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Builds a scene graph from a visual tree.
|
|
||||
/// </summary>
|
|
||||
public class SceneBuilder : ISceneBuilder |
|
||||
{ |
|
||||
/// <inheritdoc/>
|
|
||||
public void UpdateAll(Scene scene) |
|
||||
{ |
|
||||
_ = scene ?? throw new ArgumentNullException(nameof(scene)); |
|
||||
Dispatcher.UIThread.VerifyAccess(); |
|
||||
|
|
||||
UpdateSize(scene); |
|
||||
scene.Layers.GetOrAdd(scene.Root.Visual); |
|
||||
|
|
||||
using (var impl = new DeferredDrawingContextImpl(this, scene.Layers)) |
|
||||
using (var context = new DrawingContext(impl)) |
|
||||
{ |
|
||||
var clip = new Rect(scene.Root.Visual.Bounds.Size); |
|
||||
Update(context, scene, (VisualNode)scene.Root, clip, true); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public bool Update(Scene scene, Visual visual) |
|
||||
{ |
|
||||
_ = scene ?? throw new ArgumentNullException(nameof(scene)); |
|
||||
_ = visual ?? throw new ArgumentNullException(nameof(visual)); |
|
||||
|
|
||||
Dispatcher.UIThread.VerifyAccess(); |
|
||||
|
|
||||
if (!scene.Root.Visual.IsVisible) |
|
||||
{ |
|
||||
throw new AvaloniaInternalException("Cannot update the scene for an invisible root visual."); |
|
||||
} |
|
||||
|
|
||||
var node = (VisualNode?)scene.FindNode(visual); |
|
||||
|
|
||||
if (visual == scene.Root.Visual) |
|
||||
{ |
|
||||
UpdateSize(scene); |
|
||||
} |
|
||||
|
|
||||
if (visual.VisualRoot == scene.Root.Visual) |
|
||||
{ |
|
||||
if (node?.Parent != null && |
|
||||
visual.VisualParent != null && |
|
||||
node.Parent.Visual != visual.VisualParent) |
|
||||
{ |
|
||||
// The control has changed parents. Remove the node and recurse into the new parent node.
|
|
||||
((VisualNode)node.Parent).RemoveChild(node); |
|
||||
Deindex(scene, node); |
|
||||
node = (VisualNode?)scene.FindNode(visual.VisualParent); |
|
||||
} |
|
||||
|
|
||||
if (visual.IsVisible) |
|
||||
{ |
|
||||
// If the node isn't yet part of the scene, find the nearest ancestor that is.
|
|
||||
node = node ?? FindExistingAncestor(scene, visual); |
|
||||
|
|
||||
// We don't need to do anything if this part of the tree has already been fully
|
|
||||
// updated.
|
|
||||
if (node != null && !node.SubTreeUpdated) |
|
||||
{ |
|
||||
// If the control we've been asked to update isn't part of the scene then
|
|
||||
// we're carrying out an add operation, so recurse and add all the
|
|
||||
// descendents too.
|
|
||||
var recurse = node.Visual != visual; |
|
||||
|
|
||||
using (var impl = new DeferredDrawingContextImpl(this, scene.Layers)) |
|
||||
using (var context = new DrawingContext(impl)) |
|
||||
{ |
|
||||
var clip = new Rect(scene.Root.Visual.Bounds.Size); |
|
||||
|
|
||||
if (node.Parent != null) |
|
||||
{ |
|
||||
context.PushPostTransform(node.Parent.Transform); |
|
||||
clip = node.Parent.ClipBounds; |
|
||||
} |
|
||||
|
|
||||
using (context.PushTransformContainer()) |
|
||||
{ |
|
||||
Update(context, scene, node, clip, recurse); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return true; |
|
||||
} |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
if (node != null) |
|
||||
{ |
|
||||
// The control has been hidden so remove it from its parent and deindex the
|
|
||||
// node and its descendents.
|
|
||||
((VisualNode?)node.Parent)?.RemoveChild(node); |
|
||||
Deindex(scene, node); |
|
||||
return true; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
else if (node != null) |
|
||||
{ |
|
||||
// The control has been removed so remove it from its parent and deindex the
|
|
||||
// node and its descendents.
|
|
||||
var trim = FindFirstDeadAncestor(scene, node); |
|
||||
((VisualNode)trim.Parent!).RemoveChild(trim); |
|
||||
Deindex(scene, trim); |
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
private static VisualNode? FindExistingAncestor(Scene scene, Visual visual) |
|
||||
{ |
|
||||
var node = scene.FindNode(visual); |
|
||||
|
|
||||
while (node == null && visual.IsVisible) |
|
||||
{ |
|
||||
var parent = visual.VisualParent; |
|
||||
|
|
||||
if (parent is null) |
|
||||
return null; |
|
||||
|
|
||||
visual = parent; |
|
||||
node = scene.FindNode(visual); |
|
||||
} |
|
||||
|
|
||||
return visual.IsVisible ? (VisualNode?)node : null; |
|
||||
} |
|
||||
|
|
||||
private static VisualNode FindFirstDeadAncestor(Scene scene, IVisualNode node) |
|
||||
{ |
|
||||
var parent = node.Parent; |
|
||||
|
|
||||
while (parent!.Visual.VisualRoot == null) |
|
||||
{ |
|
||||
node = parent; |
|
||||
parent = node.Parent; |
|
||||
} |
|
||||
|
|
||||
return (VisualNode)node; |
|
||||
} |
|
||||
|
|
||||
private static object GetOrCreateChildNode(Scene scene, Visual child, VisualNode parent) |
|
||||
{ |
|
||||
var result = (VisualNode?)scene.FindNode(child); |
|
||||
|
|
||||
if (result != null && result.Parent != parent) |
|
||||
{ |
|
||||
Deindex(scene, result); |
|
||||
((VisualNode?)result.Parent)?.RemoveChild(result); |
|
||||
result = null; |
|
||||
} |
|
||||
|
|
||||
return result ?? CreateNode(scene, child, parent); |
|
||||
} |
|
||||
|
|
||||
private static void Update(DrawingContext context, Scene scene, VisualNode node, Rect clip, bool forceRecurse) |
|
||||
{ |
|
||||
var visual = node.Visual; |
|
||||
var opacity = visual.Opacity; |
|
||||
var clipToBounds = visual.ClipToBounds; |
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
|
||||
var clipToBoundsRadius = visual is IVisualWithRoundRectClip roundRectClip ? |
|
||||
roundRectClip.ClipToBoundsRadius : |
|
||||
default; |
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
|
||||
|
|
||||
var bounds = new Rect(visual.Bounds.Size); |
|
||||
var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl; |
|
||||
|
|
||||
contextImpl.Layers.Find(node.LayerRoot!)?.Dirty.Add(node.Bounds); |
|
||||
|
|
||||
if (visual.IsVisible) |
|
||||
{ |
|
||||
var m = node != scene.Root ? |
|
||||
Matrix.CreateTranslation(visual.Bounds.Position) : |
|
||||
Matrix.Identity; |
|
||||
|
|
||||
var renderTransform = Matrix.Identity; |
|
||||
|
|
||||
// this should be calculated BEFORE renderTransform
|
|
||||
if (visual.HasMirrorTransform) |
|
||||
{ |
|
||||
var mirrorMatrix = new Matrix(-1.0, 0.0, 0.0, 1.0, visual.Bounds.Width, 0); |
|
||||
renderTransform *= mirrorMatrix; |
|
||||
} |
|
||||
|
|
||||
if (visual.RenderTransform != null) |
|
||||
{ |
|
||||
var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); |
|
||||
var offset = Matrix.CreateTranslation(origin); |
|
||||
var finalTransform = (-offset) * visual.RenderTransform.Value * (offset); |
|
||||
renderTransform *= finalTransform; |
|
||||
} |
|
||||
|
|
||||
m = renderTransform * m; |
|
||||
|
|
||||
using (contextImpl.BeginUpdate(node)) |
|
||||
using (context.PushPostTransform(m)) |
|
||||
using (context.PushTransformContainer()) |
|
||||
{ |
|
||||
var globalBounds = bounds.TransformToAABB(contextImpl.Transform); |
|
||||
var clipBounds = clipToBounds ? |
|
||||
globalBounds.Intersect(clip) : |
|
||||
clip; |
|
||||
|
|
||||
forceRecurse = forceRecurse || |
|
||||
node.ClipBounds != clipBounds || |
|
||||
node.Opacity != opacity || |
|
||||
node.Transform != contextImpl.Transform; |
|
||||
|
|
||||
node.Transform = contextImpl.Transform; |
|
||||
node.ClipBounds = clipBounds; |
|
||||
node.ClipToBounds = clipToBounds; |
|
||||
node.LayoutBounds = globalBounds; |
|
||||
node.ClipToBoundsRadius = clipToBoundsRadius; |
|
||||
node.GeometryClip = visual.Clip?.PlatformImpl; |
|
||||
node.Opacity = opacity; |
|
||||
|
|
||||
// TODO: Check equality between node.OpacityMask and visual.OpacityMask before assigning.
|
|
||||
node.OpacityMask = visual.OpacityMask?.ToImmutable(); |
|
||||
|
|
||||
if (ShouldStartLayer(visual)) |
|
||||
{ |
|
||||
if (node.LayerRoot != visual) |
|
||||
{ |
|
||||
MakeLayer(scene, node); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
UpdateLayer(node, scene.Layers[node.LayerRoot]); |
|
||||
} |
|
||||
} |
|
||||
else if (node.LayerRoot == node.Visual && node.Parent != null) |
|
||||
{ |
|
||||
ClearLayer(scene, node); |
|
||||
} |
|
||||
|
|
||||
if (node.ClipToBounds) |
|
||||
{ |
|
||||
clip = clip.Intersect(node.ClipBounds); |
|
||||
} |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
visual.Render(context); |
|
||||
} |
|
||||
catch { } |
|
||||
|
|
||||
var transformed = new TransformedBounds(new Rect(visual.Bounds.Size), clip, node.Transform); |
|
||||
visual.SetTransformedBounds(transformed); |
|
||||
|
|
||||
if (forceRecurse) |
|
||||
{ |
|
||||
var visualChildren = (IList<Visual>) visual.VisualChildren; |
|
||||
|
|
||||
node.TryPreallocateChildren(visualChildren.Count); |
|
||||
|
|
||||
if (visualChildren.Count == 1) |
|
||||
{ |
|
||||
var childNode = GetOrCreateChildNode(scene, visualChildren[0], node); |
|
||||
Update(context, scene, (VisualNode)childNode, clip, forceRecurse); |
|
||||
} |
|
||||
else if (visualChildren.Count > 1) |
|
||||
{ |
|
||||
var count = visualChildren.Count; |
|
||||
|
|
||||
if (visual.HasNonUniformZIndexChildren) |
|
||||
{ |
|
||||
var sortedChildren = new (Visual visual, int index)[count]; |
|
||||
|
|
||||
for (var i = 0; i < count; i++) |
|
||||
{ |
|
||||
sortedChildren[i] = (visualChildren[i], i); |
|
||||
} |
|
||||
|
|
||||
// Regular Array.Sort is unstable, we need to provide indices as well to avoid reshuffling elements.
|
|
||||
Array.Sort(sortedChildren, (lhs, rhs) => |
|
||||
{ |
|
||||
var result = ZIndexComparer.Instance.Compare(lhs.visual, rhs.visual); |
|
||||
|
|
||||
return result == 0 ? lhs.index.CompareTo(rhs.index) : result; |
|
||||
}); |
|
||||
|
|
||||
foreach (var child in sortedChildren) |
|
||||
{ |
|
||||
var childNode = GetOrCreateChildNode(scene, child.Item1, node); |
|
||||
Update(context, scene, (VisualNode)childNode, clip, forceRecurse); |
|
||||
} |
|
||||
} |
|
||||
else |
|
||||
foreach (var child in visualChildren) |
|
||||
{ |
|
||||
var childNode = GetOrCreateChildNode(scene, child, node); |
|
||||
Update(context, scene, (VisualNode)childNode, clip, forceRecurse); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
node.SubTreeUpdated = true; |
|
||||
contextImpl.TrimChildren(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
contextImpl.BeginUpdate(node).Dispose(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private static void UpdateSize(Scene scene) |
|
||||
{ |
|
||||
var renderRoot = scene.Root.Visual as IRenderRoot; |
|
||||
var newSize = renderRoot?.ClientSize ?? scene.Root.Visual.Bounds.Size; |
|
||||
|
|
||||
scene.Scaling = renderRoot?.RenderScaling ?? 1; |
|
||||
|
|
||||
if (scene.Size != newSize) |
|
||||
{ |
|
||||
var oldSize = scene.Size; |
|
||||
|
|
||||
scene.Size = newSize; |
|
||||
|
|
||||
Rect horizontalDirtyRect = default; |
|
||||
Rect verticalDirtyRect = default; |
|
||||
|
|
||||
if (newSize.Width > oldSize.Width) |
|
||||
{ |
|
||||
horizontalDirtyRect = new Rect(oldSize.Width, 0, newSize.Width - oldSize.Width, oldSize.Height); |
|
||||
} |
|
||||
|
|
||||
if (newSize.Height > oldSize.Height) |
|
||||
{ |
|
||||
verticalDirtyRect = new Rect(0, oldSize.Height, newSize.Width, newSize.Height - oldSize.Height); |
|
||||
} |
|
||||
|
|
||||
foreach (var layer in scene.Layers) |
|
||||
{ |
|
||||
layer.Dirty.Add(horizontalDirtyRect); |
|
||||
layer.Dirty.Add(verticalDirtyRect); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private static VisualNode CreateNode(Scene scene, Visual visual, VisualNode parent) |
|
||||
{ |
|
||||
var node = new VisualNode(visual, parent); |
|
||||
node.LayerRoot = parent.LayerRoot; |
|
||||
scene.Add(node); |
|
||||
return node; |
|
||||
} |
|
||||
|
|
||||
private static void Deindex(Scene scene, VisualNode node) |
|
||||
{ |
|
||||
var nodeChildren = node.Children; |
|
||||
var nodeChildrenCount = nodeChildren.Count; |
|
||||
|
|
||||
for (var i = 0; i < nodeChildrenCount; i++) |
|
||||
{ |
|
||||
if (nodeChildren[i] is VisualNode visual) |
|
||||
{ |
|
||||
Deindex(scene, visual); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
scene.Remove(node); |
|
||||
|
|
||||
node.SubTreeUpdated = true; |
|
||||
|
|
||||
scene.Layers[node.LayerRoot!].Dirty.Add(node.Bounds); |
|
||||
|
|
||||
node.Visual.SetTransformedBounds(null); |
|
||||
|
|
||||
if (node.LayerRoot == node.Visual && node.Visual != scene.Root.Visual) |
|
||||
{ |
|
||||
scene.Layers.Remove(node.LayerRoot); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private static void ClearLayer(Scene scene, VisualNode node) |
|
||||
{ |
|
||||
var parent = (VisualNode)node.Parent!; |
|
||||
var oldLayerRoot = node.LayerRoot; |
|
||||
var newLayerRoot = parent.LayerRoot!; |
|
||||
var existingDirtyRects = scene.Layers[node.LayerRoot!].Dirty; |
|
||||
var newDirtyRects = scene.Layers[newLayerRoot].Dirty; |
|
||||
|
|
||||
existingDirtyRects.Coalesce(); |
|
||||
|
|
||||
foreach (var r in existingDirtyRects) |
|
||||
{ |
|
||||
newDirtyRects.Add(r); |
|
||||
} |
|
||||
|
|
||||
var oldLayer = scene.Layers[oldLayerRoot!]; |
|
||||
PropagateLayer(node, scene.Layers[newLayerRoot], oldLayer); |
|
||||
scene.Layers.Remove(oldLayer); |
|
||||
} |
|
||||
|
|
||||
private static void MakeLayer(Scene scene, VisualNode node) |
|
||||
{ |
|
||||
var oldLayerRoot = node.LayerRoot!; |
|
||||
var layer = scene.Layers.Add(node.Visual); |
|
||||
var oldLayer = scene.Layers[oldLayerRoot!]; |
|
||||
|
|
||||
UpdateLayer(node, layer); |
|
||||
PropagateLayer(node, layer, scene.Layers[oldLayerRoot]); |
|
||||
} |
|
||||
|
|
||||
private static void UpdateLayer(VisualNode node, SceneLayer layer) |
|
||||
{ |
|
||||
layer.Opacity = node.Visual.Opacity; |
|
||||
|
|
||||
if (node.Visual.OpacityMask != null) |
|
||||
{ |
|
||||
layer.OpacityMask = node.Visual.OpacityMask?.ToImmutable(); |
|
||||
layer.OpacityMaskRect = node.ClipBounds; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
layer.OpacityMask = null; |
|
||||
layer.OpacityMaskRect = default; |
|
||||
} |
|
||||
|
|
||||
layer.GeometryClip = node.HasAncestorGeometryClip ? |
|
||||
CreateLayerGeometryClip(node) : |
|
||||
null; |
|
||||
} |
|
||||
|
|
||||
private static void PropagateLayer(VisualNode node, SceneLayer layer, SceneLayer oldLayer) |
|
||||
{ |
|
||||
node.LayerRoot = layer.LayerRoot; |
|
||||
|
|
||||
layer.Dirty.Add(node.Bounds); |
|
||||
oldLayer.Dirty.Add(node.Bounds); |
|
||||
|
|
||||
foreach (VisualNode child in node.Children) |
|
||||
{ |
|
||||
// If the child is not the start of a new layer, recurse.
|
|
||||
if (child.LayerRoot != child.Visual) |
|
||||
{ |
|
||||
PropagateLayer(child, layer, oldLayer); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// HACK: Disabled layers because they're broken in current renderer. See #2244.
|
|
||||
private static bool ShouldStartLayer(Visual visual) => false; |
|
||||
|
|
||||
private static IGeometryImpl? CreateLayerGeometryClip(VisualNode node) |
|
||||
{ |
|
||||
IGeometryImpl? result = null; |
|
||||
VisualNode? n = node; |
|
||||
|
|
||||
for (;;) |
|
||||
{ |
|
||||
n = (VisualNode?)n!.Parent; |
|
||||
|
|
||||
if (n == null || (n.GeometryClip == null && !n.HasAncestorGeometryClip)) |
|
||||
{ |
|
||||
break; |
|
||||
} |
|
||||
|
|
||||
if (n?.GeometryClip != null) |
|
||||
{ |
|
||||
var transformed = n.GeometryClip.WithTransform(n.Transform); |
|
||||
|
|
||||
result = result == null ? transformed : result.Intersect(transformed); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return result; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,73 +0,0 @@ |
|||||
using Avalonia.Media; |
|
||||
using Avalonia.Platform; |
|
||||
|
|
||||
namespace Avalonia.Rendering.SceneGraph |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Represents a layer in a <see cref="Scene"/>.
|
|
||||
/// </summary>
|
|
||||
public class SceneLayer |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="SceneLayer"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="layerRoot">The visual at the root of the layer.</param>
|
|
||||
/// <param name="distanceFromRoot">The distance from the scene root.</param>
|
|
||||
public SceneLayer(Visual layerRoot, int distanceFromRoot) |
|
||||
{ |
|
||||
LayerRoot = layerRoot; |
|
||||
Dirty = new DirtyRects(); |
|
||||
DistanceFromRoot = distanceFromRoot; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Clones the layer.
|
|
||||
/// </summary>
|
|
||||
/// <returns>The cloned layer.</returns>
|
|
||||
public SceneLayer Clone() |
|
||||
{ |
|
||||
return new SceneLayer(LayerRoot, DistanceFromRoot) |
|
||||
{ |
|
||||
Opacity = Opacity, |
|
||||
OpacityMask = OpacityMask, |
|
||||
OpacityMaskRect = OpacityMaskRect, |
|
||||
GeometryClip = GeometryClip, |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the visual at the root of the layer.
|
|
||||
/// </summary>
|
|
||||
public Visual LayerRoot { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the distance of the layer root from the root of the scene.
|
|
||||
/// </summary>
|
|
||||
public int DistanceFromRoot { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the opacity of the layer.
|
|
||||
/// </summary>
|
|
||||
public double Opacity { get; set; } = 1; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the opacity mask for the layer.
|
|
||||
/// </summary>
|
|
||||
public IBrush? OpacityMask { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the target rectangle for the layer opacity mask.
|
|
||||
/// </summary>
|
|
||||
public Rect OpacityMaskRect { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the layer's geometry clip.
|
|
||||
/// </summary>
|
|
||||
public IGeometryImpl? GeometryClip { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the dirty rectangles for the layer.
|
|
||||
/// </summary>
|
|
||||
internal DirtyRects Dirty { get; } |
|
||||
} |
|
||||
} |
|
||||
@ -1,206 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections; |
|
||||
using System.Collections.Generic; |
|
||||
using Avalonia.VisualTree; |
|
||||
|
|
||||
namespace Avalonia.Rendering.SceneGraph |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Holds a collection of layers for a <see cref="Scene"/>.
|
|
||||
/// </summary>
|
|
||||
public class SceneLayers : IEnumerable<SceneLayer> |
|
||||
{ |
|
||||
private readonly Visual _root; |
|
||||
private readonly List<SceneLayer> _inner; |
|
||||
private readonly Dictionary<Visual, SceneLayer> _index; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="SceneLayers"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="root">The scene's root visual.</param>
|
|
||||
public SceneLayers(Visual root) : this(root, 0) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="SceneLayers"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="root">The scene's root visual.</param>
|
|
||||
/// <param name="capacity">Initial layer capacity.</param>
|
|
||||
public SceneLayers(Visual root, int capacity) |
|
||||
{ |
|
||||
_root = root; |
|
||||
|
|
||||
_inner = new List<SceneLayer>(capacity); |
|
||||
_index = new Dictionary<Visual, SceneLayer>(capacity); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the number of layers in the scene.
|
|
||||
/// </summary>
|
|
||||
public int Count => _inner.Count; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets a value indicating whether any of the layers have a dirty region.
|
|
||||
/// </summary>
|
|
||||
public bool HasDirty |
|
||||
{ |
|
||||
get |
|
||||
{ |
|
||||
foreach (var layer in _inner) |
|
||||
{ |
|
||||
if (!layer.Dirty.IsEmpty) |
|
||||
{ |
|
||||
return true; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets a layer by index.
|
|
||||
/// </summary>
|
|
||||
/// <param name="index">The index of the layer.</param>
|
|
||||
/// <returns>The layer.</returns>
|
|
||||
public SceneLayer this[int index] => _inner[index]; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets a layer by its root visual.
|
|
||||
/// </summary>
|
|
||||
/// <param name="visual">The layer's root visual.</param>
|
|
||||
/// <returns>The layer.</returns>
|
|
||||
public SceneLayer this[Visual visual] => _index[visual]; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Adds a layer to the scene.
|
|
||||
/// </summary>
|
|
||||
/// <param name="layerRoot">The root visual of the layer.</param>
|
|
||||
/// <returns>The created layer.</returns>
|
|
||||
public SceneLayer Add(Visual layerRoot) |
|
||||
{ |
|
||||
_ = layerRoot ?? throw new ArgumentNullException(nameof(layerRoot)); |
|
||||
|
|
||||
var distance = layerRoot.CalculateDistanceFromAncestor(_root); |
|
||||
var layer = new SceneLayer(layerRoot, distance); |
|
||||
var insert = FindInsertIndex(layer); |
|
||||
_index.Add(layerRoot, layer); |
|
||||
_inner.Insert(insert, layer); |
|
||||
return layer; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Makes a deep clone of the layers.
|
|
||||
/// </summary>
|
|
||||
/// <returns>The cloned layers.</returns>
|
|
||||
public SceneLayers Clone() |
|
||||
{ |
|
||||
var result = new SceneLayers(_root, Count); |
|
||||
|
|
||||
foreach (var src in _inner) |
|
||||
{ |
|
||||
var dest = src.Clone(); |
|
||||
result._index.Add(dest.LayerRoot, dest); |
|
||||
result._inner.Add(dest); |
|
||||
} |
|
||||
|
|
||||
return result; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Tests whether a layer exists with the specified root visual.
|
|
||||
/// </summary>
|
|
||||
/// <param name="layerRoot">The root visual.</param>
|
|
||||
/// <returns>
|
|
||||
/// True if a layer exists with the specified root visual, otherwise false.
|
|
||||
/// </returns>
|
|
||||
public bool Exists(Visual layerRoot) |
|
||||
{ |
|
||||
_ = layerRoot ?? throw new ArgumentNullException(nameof(layerRoot)); |
|
||||
|
|
||||
return _index.ContainsKey(layerRoot); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Tries to find a layer with the specified root visual.
|
|
||||
/// </summary>
|
|
||||
/// <param name="layerRoot">The root visual.</param>
|
|
||||
/// <returns>The layer if found, otherwise null.</returns>
|
|
||||
public SceneLayer? Find(Visual layerRoot) |
|
||||
{ |
|
||||
_index.TryGetValue(layerRoot, out var result); |
|
||||
return result; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets an existing layer or creates a new one if no existing layer is found.
|
|
||||
/// </summary>
|
|
||||
/// <param name="layerRoot">The root visual.</param>
|
|
||||
/// <returns>The layer.</returns>
|
|
||||
public SceneLayer GetOrAdd(Visual layerRoot) |
|
||||
{ |
|
||||
_ = layerRoot ?? throw new ArgumentNullException(nameof(layerRoot)); |
|
||||
|
|
||||
if (!_index.TryGetValue(layerRoot, out var result)) |
|
||||
{ |
|
||||
result = Add(layerRoot); |
|
||||
} |
|
||||
|
|
||||
return result; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Removes a layer from the scene.
|
|
||||
/// </summary>
|
|
||||
/// <param name="layerRoot">The root visual.</param>
|
|
||||
/// <returns>True if a matching layer was removed, otherwise false.</returns>
|
|
||||
public bool Remove(Visual layerRoot) |
|
||||
{ |
|
||||
_ = layerRoot ?? throw new ArgumentNullException(nameof(layerRoot)); |
|
||||
|
|
||||
if (_index.TryGetValue(layerRoot, out var layer)) |
|
||||
{ |
|
||||
Remove(layer); |
|
||||
} |
|
||||
|
|
||||
return layer != null; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Removes a layer from the scene.
|
|
||||
/// </summary>
|
|
||||
/// <param name="layer">The layer.</param>
|
|
||||
/// <returns>True if the layer was part of the scene, otherwise false.</returns>
|
|
||||
public bool Remove(SceneLayer layer) |
|
||||
{ |
|
||||
_ = layer ?? throw new ArgumentNullException(nameof(layer)); |
|
||||
|
|
||||
_index.Remove(layer.LayerRoot); |
|
||||
return _inner.Remove(layer); |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public IEnumerator<SceneLayer> GetEnumerator() => _inner.GetEnumerator(); |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|
||||
|
|
||||
private int FindInsertIndex(SceneLayer insert) |
|
||||
{ |
|
||||
var index = 0; |
|
||||
|
|
||||
foreach (var layer in _inner) |
|
||||
{ |
|
||||
if (layer.DistanceFromRoot > insert.DistanceFromRoot) |
|
||||
{ |
|
||||
break; |
|
||||
} |
|
||||
|
|
||||
++index; |
|
||||
} |
|
||||
|
|
||||
return index; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,448 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Diagnostics.CodeAnalysis; |
|
||||
using Avalonia.Reactive; |
|
||||
using Avalonia.Media; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.Utilities; |
|
||||
using Avalonia.VisualTree; |
|
||||
|
|
||||
namespace Avalonia.Rendering.SceneGraph |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// A node in the low-level scene graph representing an <see cref="Visual"/>.
|
|
||||
/// </summary>
|
|
||||
internal class VisualNode : IVisualNode |
|
||||
{ |
|
||||
private static readonly IReadOnlyList<IVisualNode> EmptyChildren = Array.Empty<IVisualNode>(); |
|
||||
private static readonly IReadOnlyList<IRef<IDrawOperation>> EmptyDrawOperations = Array.Empty<IRef<IDrawOperation>>(); |
|
||||
|
|
||||
private Rect? _bounds; |
|
||||
private double _opacity; |
|
||||
private List<IVisualNode>? _children; |
|
||||
private List<IRef<IDrawOperation>>? _drawOperations; |
|
||||
private IRef<IDisposable>? _drawOperationsRefCounter; |
|
||||
private bool _drawOperationsCloned; |
|
||||
private Matrix transformRestore; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of the <see cref="VisualNode"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="visual">The visual that this node represents.</param>
|
|
||||
/// <param name="parent">The parent scene graph node, if any.</param>
|
|
||||
public VisualNode(Visual visual, IVisualNode? parent) |
|
||||
{ |
|
||||
Visual = visual ?? throw new ArgumentNullException(nameof(visual)); |
|
||||
Parent = parent; |
|
||||
HasAncestorGeometryClip = parent != null && |
|
||||
(parent.HasAncestorGeometryClip || parent.GeometryClip != null); |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public Visual Visual { get; } |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public IVisualNode? Parent { get; } |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public CornerRadius ClipToBoundsRadius { get; set; } |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public Matrix Transform { get; set; } |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public Rect Bounds => _bounds ?? CalculateBounds(); |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public Rect ClipBounds { get; set; } |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public Rect LayoutBounds { get; set; } |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public bool ClipToBounds { get; set; } |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public IGeometryImpl? GeometryClip { get; set; } |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public bool HasAncestorGeometryClip { get; } |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public double Opacity |
|
||||
{ |
|
||||
get { return _opacity; } |
|
||||
set |
|
||||
{ |
|
||||
if (_opacity != value) |
|
||||
{ |
|
||||
_opacity = value; |
|
||||
OpacityChanged = true; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the opacity mask for the scene graph node.
|
|
||||
/// </summary>
|
|
||||
public IBrush? OpacityMask { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets a value indicating whether this node in the scene graph has already
|
|
||||
/// been updated in the current update pass.
|
|
||||
/// </summary>
|
|
||||
public bool SubTreeUpdated { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets a value indicating whether the <see cref="Opacity"/> property has changed.
|
|
||||
/// </summary>
|
|
||||
public bool OpacityChanged { get; private set; } |
|
||||
|
|
||||
public Visual? LayerRoot { get; set; } |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public IReadOnlyList<IVisualNode> Children => _children ?? EmptyChildren; |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public IReadOnlyList<IRef<IDrawOperation>> DrawOperations => _drawOperations ?? EmptyDrawOperations; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Adds a child to the <see cref="Children"/> collection.
|
|
||||
/// </summary>
|
|
||||
/// <param name="child">The child to add.</param>
|
|
||||
public void AddChild(IVisualNode child) |
|
||||
{ |
|
||||
if (child.Disposed) |
|
||||
{ |
|
||||
throw new ObjectDisposedException("Visual node for {node.Visual}"); |
|
||||
} |
|
||||
|
|
||||
if (child.Parent != this) |
|
||||
{ |
|
||||
throw new AvaloniaInternalException("VisualNode added to wrong parent."); |
|
||||
} |
|
||||
|
|
||||
EnsureChildrenCreated(); |
|
||||
_children.Add(child); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Adds an operation to the <see cref="DrawOperations"/> collection.
|
|
||||
/// </summary>
|
|
||||
/// <param name="operation">The operation to add.</param>
|
|
||||
public void AddDrawOperation(IRef<IDrawOperation> operation) |
|
||||
{ |
|
||||
EnsureDrawOperationsCreated(); |
|
||||
_drawOperations.Add(operation.Clone()); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Removes a child from the <see cref="Children"/> collection.
|
|
||||
/// </summary>
|
|
||||
/// <param name="child">The child to remove.</param>
|
|
||||
public void RemoveChild(IVisualNode child) |
|
||||
{ |
|
||||
EnsureChildrenCreated(); |
|
||||
_children.Remove(child); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Replaces a child in the <see cref="Children"/> collection.
|
|
||||
/// </summary>
|
|
||||
/// <param name="index">The child to be replaced.</param>
|
|
||||
/// <param name="node">The child to add.</param>
|
|
||||
public void ReplaceChild(int index, IVisualNode node) |
|
||||
{ |
|
||||
if (node.Disposed) |
|
||||
{ |
|
||||
throw new ObjectDisposedException("Visual node for {node.Visual}"); |
|
||||
} |
|
||||
|
|
||||
if (node.Parent != this) |
|
||||
{ |
|
||||
throw new AvaloniaInternalException("VisualNode added to wrong parent."); |
|
||||
} |
|
||||
|
|
||||
EnsureChildrenCreated(); |
|
||||
_children[index] = node; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Replaces an item in the <see cref="DrawOperations"/> collection.
|
|
||||
/// </summary>
|
|
||||
/// <param name="index">The operation to be replaced.</param>
|
|
||||
/// <param name="operation">The operation to add.</param>
|
|
||||
public void ReplaceDrawOperation(int index, IRef<IDrawOperation> operation) |
|
||||
{ |
|
||||
EnsureDrawOperationsCreated(); |
|
||||
var old = _drawOperations[index]; |
|
||||
_drawOperations[index] = operation.Clone(); |
|
||||
old.Dispose(); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Sorts the <see cref="Children"/> collection according to the order of the visual's
|
|
||||
/// children and their z-index.
|
|
||||
/// </summary>
|
|
||||
/// <param name="scene">The scene that the node is a part of.</param>
|
|
||||
public void SortChildren(Scene scene) |
|
||||
{ |
|
||||
if (_children == null || _children.Count <= 1) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
var keys = new List<long>(Visual.VisualChildren.Count); |
|
||||
|
|
||||
for (var i = 0; i < Visual.VisualChildren.Count; ++i) |
|
||||
{ |
|
||||
var child = Visual.VisualChildren[i]; |
|
||||
var zIndex = child.ZIndex; |
|
||||
keys.Add(((long)zIndex << 32) + i); |
|
||||
} |
|
||||
|
|
||||
keys.Sort(); |
|
||||
_children.Clear(); |
|
||||
|
|
||||
foreach (var i in keys) |
|
||||
{ |
|
||||
var child = Visual.VisualChildren[(int)(i & 0xffffffff)]; |
|
||||
var node = scene.FindNode(child); |
|
||||
|
|
||||
if (node != null) |
|
||||
{ |
|
||||
_children.Add(node); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Removes items in the <see cref="Children"/> collection from the specified index
|
|
||||
/// to the end.
|
|
||||
/// </summary>
|
|
||||
/// <param name="first">The index of the first child to be removed.</param>
|
|
||||
public void TrimChildren(int first) |
|
||||
{ |
|
||||
if (first < _children?.Count) |
|
||||
{ |
|
||||
EnsureChildrenCreated(); |
|
||||
for (int i = first; i < _children.Count; i++) |
|
||||
{ |
|
||||
_children[i].Dispose(); |
|
||||
} |
|
||||
_children.RemoveRange(first, _children.Count - first); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Removes items in the <see cref="DrawOperations"/> collection from the specified index
|
|
||||
/// to the end.
|
|
||||
/// </summary>
|
|
||||
/// <param name="first">The index of the first operation to be removed.</param>
|
|
||||
public void TrimDrawOperations(int first) |
|
||||
{ |
|
||||
if (first < _drawOperations?.Count) |
|
||||
{ |
|
||||
EnsureDrawOperationsCreated(); |
|
||||
for (int i = first; i < _drawOperations.Count; i++) |
|
||||
{ |
|
||||
_drawOperations[i].Dispose(); |
|
||||
} |
|
||||
_drawOperations.RemoveRange(first, _drawOperations.Count - first); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Makes a copy of the node
|
|
||||
/// </summary>
|
|
||||
/// <param name="parent">The new parent node.</param>
|
|
||||
/// <returns>A cloned node.</returns>
|
|
||||
public VisualNode Clone(IVisualNode? parent) |
|
||||
{ |
|
||||
return new VisualNode(Visual, parent) |
|
||||
{ |
|
||||
Transform = Transform, |
|
||||
ClipBounds = ClipBounds, |
|
||||
ClipToBoundsRadius = ClipToBoundsRadius, |
|
||||
ClipToBounds = ClipToBounds, |
|
||||
LayoutBounds = LayoutBounds, |
|
||||
GeometryClip = GeometryClip, |
|
||||
_opacity = Opacity, |
|
||||
OpacityMask = OpacityMask, |
|
||||
_drawOperations = _drawOperations, |
|
||||
_drawOperationsRefCounter = _drawOperationsRefCounter?.Clone(), |
|
||||
_drawOperationsCloned = true, |
|
||||
LayerRoot= LayerRoot, |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public bool HitTest(Point p) |
|
||||
{ |
|
||||
var drawOperations = DrawOperations; |
|
||||
var drawOperationsCount = drawOperations.Count; |
|
||||
|
|
||||
for (var i = 0; i < drawOperationsCount; i++) |
|
||||
{ |
|
||||
var operation = drawOperations[i]; |
|
||||
|
|
||||
if (operation?.Item?.HitTest(p) == true) |
|
||||
{ |
|
||||
return true; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void BeginRender(IDrawingContextImpl context, bool skipOpacity) |
|
||||
{ |
|
||||
transformRestore = context.Transform; |
|
||||
|
|
||||
if (ClipToBounds) |
|
||||
{ |
|
||||
context.Transform = Matrix.Identity; |
|
||||
if (ClipToBoundsRadius.IsDefault) |
|
||||
context.PushClip(ClipBounds); |
|
||||
else |
|
||||
context.PushClip(new RoundedRect(ClipBounds, ClipToBoundsRadius)); |
|
||||
} |
|
||||
|
|
||||
context.Transform = Transform; |
|
||||
|
|
||||
if (Opacity != 1 && !skipOpacity) |
|
||||
{ |
|
||||
context.PushOpacity(Opacity); |
|
||||
} |
|
||||
|
|
||||
if (GeometryClip != null) |
|
||||
{ |
|
||||
context.PushGeometryClip(GeometryClip); |
|
||||
} |
|
||||
|
|
||||
if (OpacityMask != null) |
|
||||
{ |
|
||||
context.PushOpacityMask(OpacityMask, LayoutBounds); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void EndRender(IDrawingContextImpl context, bool skipOpacity) |
|
||||
{ |
|
||||
if (OpacityMask != null) |
|
||||
{ |
|
||||
context.PopOpacityMask(); |
|
||||
} |
|
||||
|
|
||||
if (GeometryClip != null) |
|
||||
{ |
|
||||
context.PopGeometryClip(); |
|
||||
} |
|
||||
|
|
||||
if (Opacity != 1 && !skipOpacity) |
|
||||
{ |
|
||||
context.PopOpacity(); |
|
||||
} |
|
||||
|
|
||||
if (ClipToBounds) |
|
||||
{ |
|
||||
context.Transform = Matrix.Identity; |
|
||||
context.PopClip(); |
|
||||
} |
|
||||
|
|
||||
context.Transform = transformRestore; |
|
||||
} |
|
||||
|
|
||||
internal void TryPreallocateChildren(int count) |
|
||||
{ |
|
||||
if (count == 0) |
|
||||
{ |
|
||||
return; |
|
||||
} |
|
||||
|
|
||||
EnsureChildrenCreated(count); |
|
||||
} |
|
||||
|
|
||||
private Rect CalculateBounds() |
|
||||
{ |
|
||||
var result = new Rect(); |
|
||||
|
|
||||
if (_drawOperations != null) |
|
||||
{ |
|
||||
foreach (var operation in _drawOperations) |
|
||||
{ |
|
||||
result = result.Union(operation.Item.Bounds); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
_bounds = result; |
|
||||
return result; |
|
||||
} |
|
||||
|
|
||||
[MemberNotNull(nameof(_children))] |
|
||||
private void EnsureChildrenCreated(int capacity = 0) |
|
||||
{ |
|
||||
if (_children == null) |
|
||||
{ |
|
||||
_children = new List<IVisualNode>(capacity); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Ensures that this node draw operations have been created and are mutable (in case we are using cloned operations).
|
|
||||
/// </summary>
|
|
||||
[MemberNotNull(nameof(_drawOperations))] |
|
||||
private void EnsureDrawOperationsCreated() |
|
||||
{ |
|
||||
if (_drawOperations == null) |
|
||||
{ |
|
||||
_drawOperations = new List<IRef<IDrawOperation>>(); |
|
||||
_drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations)); |
|
||||
_drawOperationsCloned = false; |
|
||||
} |
|
||||
else if (_drawOperationsCloned) |
|
||||
{ |
|
||||
var oldDrawOperations = _drawOperations; |
|
||||
|
|
||||
_drawOperations = new List<IRef<IDrawOperation>>(oldDrawOperations.Count); |
|
||||
|
|
||||
foreach (var drawOperation in oldDrawOperations) |
|
||||
{ |
|
||||
_drawOperations.Add(drawOperation.Clone()); |
|
||||
} |
|
||||
|
|
||||
_drawOperationsRefCounter?.Dispose(); |
|
||||
_drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations)); |
|
||||
_drawOperationsCloned = false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Creates disposable that will dispose all items in passed draw operations after being disposed.
|
|
||||
/// It is crucial that we don't capture current <see cref="VisualNode"/> instance
|
|
||||
/// as draw operations can be cloned and may persist across subsequent scenes.
|
|
||||
/// </summary>
|
|
||||
/// <param name="drawOperations">Draw operations that need to be disposed.</param>
|
|
||||
/// <returns>Disposable for given draw operations.</returns>
|
|
||||
private static IDisposable CreateDisposeDrawOperations(List<IRef<IDrawOperation>> drawOperations) |
|
||||
{ |
|
||||
return Disposable.Create(drawOperations, operations => |
|
||||
{ |
|
||||
foreach (var operation in operations) |
|
||||
{ |
|
||||
operation.Dispose(); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
public bool Disposed { get; private set; } |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
_drawOperationsRefCounter?.Dispose(); |
|
||||
|
|
||||
Disposed = true; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,120 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Threading.Tasks; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.Rendering; |
|
||||
using Avalonia.Threading; |
|
||||
using Avalonia.VisualTree; |
|
||||
|
|
||||
namespace Avalonia.X11 |
|
||||
{ |
|
||||
public class X11ImmediateRendererProxy : IRenderer, IRenderLoopTask |
|
||||
{ |
|
||||
private readonly IRenderLoop _loop; |
|
||||
private ImmediateRenderer _renderer; |
|
||||
private bool _invalidated; |
|
||||
private bool _running; |
|
||||
private object _lock = new object(); |
|
||||
|
|
||||
public X11ImmediateRendererProxy(Visual root, IRenderLoop loop, Func<IRenderTarget> renderTargetFactory, |
|
||||
PlatformRenderInterfaceContextManager renderContext) |
|
||||
{ |
|
||||
_loop = loop; |
|
||||
_renderer = new ImmediateRenderer(root, renderTargetFactory, renderContext); |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
_running = false; |
|
||||
_renderer.Dispose(); |
|
||||
} |
|
||||
|
|
||||
public bool DrawFps |
|
||||
{ |
|
||||
get => _renderer.DrawFps; |
|
||||
set => _renderer.DrawFps = value; |
|
||||
} |
|
||||
|
|
||||
public bool DrawDirtyRects |
|
||||
{ |
|
||||
get => _renderer.DrawDirtyRects; |
|
||||
set => _renderer.DrawDirtyRects = value; |
|
||||
} |
|
||||
|
|
||||
public event EventHandler<SceneInvalidatedEventArgs> SceneInvalidated |
|
||||
{ |
|
||||
add => _renderer.SceneInvalidated += value; |
|
||||
remove => _renderer.SceneInvalidated -= value; |
|
||||
} |
|
||||
|
|
||||
public void AddDirty(Visual visual) |
|
||||
{ |
|
||||
lock (_lock) |
|
||||
_invalidated = true; |
|
||||
_renderer.AddDirty(visual); |
|
||||
} |
|
||||
|
|
||||
public IEnumerable<Visual> HitTest(Point p, Visual root, Func<Visual, bool> filter) |
|
||||
{ |
|
||||
return _renderer.HitTest(p, root, filter); |
|
||||
} |
|
||||
|
|
||||
public Visual HitTestFirst(Point p, Visual root, Func<Visual, bool> filter) |
|
||||
{ |
|
||||
return _renderer.HitTestFirst(p, root, filter); |
|
||||
} |
|
||||
|
|
||||
public void RecalculateChildren(Visual visual) |
|
||||
{ |
|
||||
_renderer.RecalculateChildren(visual); |
|
||||
} |
|
||||
|
|
||||
public void Resized(Size size) |
|
||||
{ |
|
||||
_renderer.Resized(size); |
|
||||
} |
|
||||
|
|
||||
public void Paint(Rect rect) |
|
||||
{ |
|
||||
_invalidated = false; |
|
||||
_renderer.Paint(rect); |
|
||||
} |
|
||||
|
|
||||
public void Start() |
|
||||
{ |
|
||||
_running = true; |
|
||||
_loop.Add(this); |
|
||||
_renderer.Start(); |
|
||||
} |
|
||||
|
|
||||
public void Stop() |
|
||||
{ |
|
||||
_running = false; |
|
||||
_loop.Remove(this); |
|
||||
_renderer.Stop(); |
|
||||
} |
|
||||
|
|
||||
public ValueTask<object> TryGetRenderInterfaceFeature(Type featureType) => |
|
||||
_renderer.TryGetRenderInterfaceFeature(featureType); |
|
||||
|
|
||||
public bool NeedsUpdate => false; |
|
||||
public void Update(TimeSpan time) |
|
||||
{ |
|
||||
|
|
||||
} |
|
||||
|
|
||||
public void Render() |
|
||||
{ |
|
||||
if (_invalidated) |
|
||||
{ |
|
||||
lock (_lock) |
|
||||
_invalidated = false; |
|
||||
Dispatcher.UIThread.Post(() => |
|
||||
{ |
|
||||
if (_running) |
|
||||
Paint(new Rect(0, 0, 100000, 100000)); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,790 +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, |
|
||||
renderTargetFactory: root.CreateRenderTarget, |
|
||||
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, |
|
||||
renderTargetFactory: root.CreateRenderTarget, |
|
||||
dispatcher: dispatcher); |
|
||||
|
|
||||
target.Start(); |
|
||||
IgnoreFirstFrame(target, sceneBuilder); |
|
||||
target.AddDirty(border); |
|
||||
target.AddDirty(canvas); |
|
||||
target.AddDirty(root); |
|
||||
target.AddDirty(decorator); |
|
||||
|
|
||||
var result = new List<Visual>(); |
|
||||
|
|
||||
sceneBuilder.Setup(x => x.Update(It.IsAny<Scene>(), It.IsAny<Visual>())) |
|
||||
.Callback<Scene, Visual>((_, v) => result.Add(v)); |
|
||||
|
|
||||
RunFrame(target); |
|
||||
|
|
||||
Assert.Equal(new List<Visual> { 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, |
|
||||
renderTargetFactory: root.CreateRenderTarget, |
|
||||
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, |
|
||||
renderTargetFactory: root.CreateRenderTarget, |
|
||||
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, |
|
||||
renderTargetFactory: root.CreateRenderTarget, |
|
||||
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, |
|
||||
renderTargetFactory: root.CreateRenderTarget, |
|
||||
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, |
|
||||
renderTargetFactory: root.CreateRenderTarget, |
|
||||
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, |
|
||||
renderTargetFactory: root.CreateRenderTarget, |
|
||||
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, |
|
||||
renderTargetFactory: root.CreateRenderTarget, |
|
||||
dispatcher: dispatcher); |
|
||||
|
|
||||
var otherSceneBuilder = new SceneBuilder(); |
|
||||
|
|
||||
var otherTarget = new DeferredRenderer( |
|
||||
otherRoot, |
|
||||
loop.Object, |
|
||||
sceneBuilder: otherSceneBuilder, |
|
||||
renderTargetFactory: root.CreateRenderTarget, |
|
||||
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 Visual[] { 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, |
|
||||
renderTargetFactory: root.CreateRenderTarget, |
|
||||
dispatcher: dispatcher); |
|
||||
root.Renderer = target; |
|
||||
target.Start(); |
|
||||
RunFrame(target); |
|
||||
return target; |
|
||||
} |
|
||||
|
|
||||
private Mock<IDrawingContextImpl> GetLayerContext(DeferredRenderer renderer, Control 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((Visual)root).Dirty.Add(new Rect(root.ClientSize))); |
|
||||
return result; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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((IRenderRoot)root, null, root.CreateRenderTarget); |
|
||||
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((IRenderRoot)root, null, root.CreateRenderTarget); |
|
||||
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((IRenderRoot)root, null, root.CreateRenderTarget); |
|
||||
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((IRenderRoot)root, null, root.CreateRenderTarget); |
|
||||
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((IRenderRoot)root, null, root.CreateRenderTarget); |
|
||||
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((IRenderRoot)root, null, root.CreateRenderTarget); |
|
||||
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((IRenderRoot)root, null, root.CreateRenderTarget); |
|
||||
container.Measure(Size.Infinity); |
|
||||
container.Arrange(new Rect(container.DesiredSize)); |
|
||||
|
|
||||
var result = root.Renderer.HitTest(new Point(120, 120), root, null); |
|
||||
|
|
||||
Assert.Equal(new Visual[] { 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((IRenderRoot)root, null, root.CreateRenderTarget); |
|
||||
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((IRenderRoot)root, null, root.CreateRenderTarget); |
|
||||
root.Measure(Size.Infinity); |
|
||||
root.Arrange(new Rect(container.DesiredSize)); |
|
||||
|
|
||||
root.Renderer.Paint(default); |
|
||||
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(default); |
|
||||
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((IRenderRoot)root, null, root.CreateRenderTarget); |
|
||||
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((IRenderRoot)root, null, root.CreateRenderTarget); |
|
||||
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 Visual[] { 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((IRenderRoot)root, null, root.CreateRenderTarget); |
|
||||
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((IRenderRoot)root, null, root.CreateRenderTarget); |
|
||||
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 static IDisposable TestApplication() |
|
||||
{ |
|
||||
return UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,273 +0,0 @@ |
|||||
using System.Collections.Generic; |
|
||||
using Avalonia.Controls; |
|
||||
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 ImmediateRendererTests |
|
||||
{ |
|
||||
[Fact] |
|
||||
public void AddDirty_Call_RenderRoot_Invalidate() |
|
||||
{ |
|
||||
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) |
|
||||
{ |
|
||||
var child = new Border |
|
||||
{ |
|
||||
Width = 100, |
|
||||
Height = 100, |
|
||||
Margin = new(10), |
|
||||
HorizontalAlignment = HorizontalAlignment.Left, |
|
||||
VerticalAlignment = VerticalAlignment.Top, |
|
||||
}; |
|
||||
|
|
||||
var root = new RenderRoot |
|
||||
{ |
|
||||
Child = child, |
|
||||
Width = 400, |
|
||||
Height = 400, |
|
||||
}; |
|
||||
|
|
||||
root.LayoutManager.ExecuteInitialLayoutPass(); |
|
||||
|
|
||||
var target = new ImmediateRenderer(root, root.CreateRenderTarget); |
|
||||
|
|
||||
target.AddDirty(child); |
|
||||
|
|
||||
Assert.Equal(new[] { new Rect(10, 10, 100, 100) }, root.Invalidations); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void AddDirty_With_RenderTransform_Call_RenderRoot_Invalidate() |
|
||||
{ |
|
||||
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) |
|
||||
{ |
|
||||
var child = new Border |
|
||||
{ |
|
||||
Width = 100, |
|
||||
Height = 100, |
|
||||
Margin = new(100), |
|
||||
HorizontalAlignment = HorizontalAlignment.Left, |
|
||||
VerticalAlignment = VerticalAlignment.Top, |
|
||||
}; |
|
||||
|
|
||||
var root = new RenderRoot |
|
||||
{ |
|
||||
Child = child, |
|
||||
Width = 400, |
|
||||
Height = 400, |
|
||||
}; |
|
||||
|
|
||||
root.LayoutManager.ExecuteInitialLayoutPass(); |
|
||||
|
|
||||
child.RenderTransform = new ScaleTransform() { ScaleX = 2, ScaleY = 2 }; |
|
||||
|
|
||||
var target = new ImmediateRenderer(root, root.CreateRenderTarget); |
|
||||
|
|
||||
target.AddDirty(child); |
|
||||
|
|
||||
Assert.Equal(new[] { new Rect(50, 50, 200, 200) }, root.Invalidations); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void AddDirty_For_Child_Moved_Should_Invalidate_Previous_Bounds() |
|
||||
{ |
|
||||
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) |
|
||||
{ |
|
||||
var child = new Border |
|
||||
{ |
|
||||
Width = 100, |
|
||||
Height = 100, |
|
||||
HorizontalAlignment = HorizontalAlignment.Left, |
|
||||
VerticalAlignment = VerticalAlignment.Top, |
|
||||
}; |
|
||||
|
|
||||
var root = new RenderRoot |
|
||||
{ |
|
||||
Child = child, |
|
||||
Width = 400, |
|
||||
Height = 400, |
|
||||
}; |
|
||||
|
|
||||
var target = new ImmediateRenderer(root, root.CreateRenderTarget); |
|
||||
|
|
||||
root.LayoutManager.ExecuteInitialLayoutPass(); |
|
||||
target.AddDirty(child); |
|
||||
|
|
||||
Assert.Equal(new Rect(0, 0, 100, 100), root.Invalidations[0]); |
|
||||
|
|
||||
target.Paint(new Rect(0, 0, 100, 100)); |
|
||||
|
|
||||
//move child 100 pixels bottom/right
|
|
||||
child.Margin = new(100, 100); |
|
||||
root.LayoutManager.ExecuteLayoutPass(); |
|
||||
|
|
||||
//renderer should invalidate old child bounds with new one
|
|
||||
//as on old area there can be artifacts
|
|
||||
target.AddDirty(child); |
|
||||
|
|
||||
//invalidate first old position
|
|
||||
Assert.Equal(new Rect(0, 0, 100, 100), root.Invalidations[1]); |
|
||||
|
|
||||
//then new position
|
|
||||
Assert.Equal(new Rect(100, 100, 100, 100), root.Invalidations[2]); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_Not_Clip_Children_With_RenderTransform_When_In_Bounds() |
|
||||
{ |
|
||||
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) |
|
||||
{ |
|
||||
const int RootWidth = 300; |
|
||||
const int RootHeight = 300; |
|
||||
|
|
||||
var rootGrid = new Grid { Width = RootWidth, Height = RootHeight, ClipToBounds = true }; |
|
||||
|
|
||||
var stackPanel = new StackPanel |
|
||||
{ |
|
||||
Orientation = Orientation.Horizontal, |
|
||||
VerticalAlignment = VerticalAlignment.Top, |
|
||||
HorizontalAlignment = HorizontalAlignment.Right, |
|
||||
Margin = new Thickness(0, 10, 0, 0), |
|
||||
RenderTransformOrigin = new RelativePoint(new Point(0, 0), RelativeUnit.Relative), |
|
||||
RenderTransform = new TransformGroup |
|
||||
{ |
|
||||
Children = { new RotateTransform { Angle = 90 }, new TranslateTransform { X = 240 } } |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
rootGrid.Children.Add(stackPanel); |
|
||||
|
|
||||
TestControl CreateControl() |
|
||||
=> new TestControl |
|
||||
{ |
|
||||
Width = 80, |
|
||||
Height = 40, |
|
||||
Margin = new Thickness(0, 0, 5, 0), |
|
||||
ClipToBounds = true |
|
||||
}; |
|
||||
|
|
||||
var control1 = CreateControl(); |
|
||||
var control2 = CreateControl(); |
|
||||
var control3 = CreateControl(); |
|
||||
|
|
||||
stackPanel.Children.Add(control1); |
|
||||
stackPanel.Children.Add(control2); |
|
||||
stackPanel.Children.Add(control3); |
|
||||
|
|
||||
var root = new TestRoot(rootGrid); |
|
||||
root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); |
|
||||
root.LayoutManager.ExecuteInitialLayoutPass(); |
|
||||
|
|
||||
var rootSize = new Size(RootWidth, RootHeight); |
|
||||
root.Measure(rootSize); |
|
||||
root.Arrange(new Rect(rootSize)); |
|
||||
|
|
||||
root.Renderer.Paint(root.Bounds); |
|
||||
|
|
||||
Assert.True(control1.Rendered); |
|
||||
Assert.True(control2.Rendered); |
|
||||
Assert.True(control3.Rendered); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Should_Not_Render_Clipped_Child_With_RenderTransform_When_Not_In_Bounds() |
|
||||
{ |
|
||||
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) |
|
||||
{ |
|
||||
const int RootWidth = 300; |
|
||||
const int RootHeight = 300; |
|
||||
|
|
||||
var rootGrid = new Grid { Width = RootWidth, Height = RootHeight, ClipToBounds = true }; |
|
||||
|
|
||||
var stackPanel = new StackPanel |
|
||||
{ |
|
||||
Orientation = Orientation.Horizontal, |
|
||||
VerticalAlignment = VerticalAlignment.Top, |
|
||||
HorizontalAlignment = HorizontalAlignment.Right, |
|
||||
Margin = new Thickness(0, 10, 0, 0), |
|
||||
RenderTransformOrigin = new RelativePoint(new Point(0, 0), RelativeUnit.Relative), |
|
||||
RenderTransform = new TransformGroup |
|
||||
{ |
|
||||
Children = { new RotateTransform { Angle = 90 }, new TranslateTransform { X = 280 } } |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
rootGrid.Children.Add(stackPanel); |
|
||||
|
|
||||
TestControl CreateControl() |
|
||||
=> new TestControl |
|
||||
{ |
|
||||
Width = 160, |
|
||||
Height = 40, |
|
||||
Margin = new Thickness(0, 0, 5, 0), |
|
||||
ClipToBounds = true |
|
||||
}; |
|
||||
|
|
||||
var control1 = CreateControl(); |
|
||||
var control2 = CreateControl(); |
|
||||
var control3 = CreateControl(); |
|
||||
|
|
||||
stackPanel.Children.Add(control1); |
|
||||
stackPanel.Children.Add(control2); |
|
||||
stackPanel.Children.Add(control3); |
|
||||
|
|
||||
var root = new TestRoot(rootGrid); |
|
||||
root.Renderer = new ImmediateRenderer(root, root.CreateRenderTarget); |
|
||||
root.LayoutManager.ExecuteInitialLayoutPass(); |
|
||||
|
|
||||
var rootSize = new Size(RootWidth, RootHeight); |
|
||||
root.Measure(rootSize); |
|
||||
root.Arrange(new Rect(rootSize)); |
|
||||
|
|
||||
root.Renderer.Paint(root.Bounds); |
|
||||
|
|
||||
Assert.True(control1.Rendered); |
|
||||
Assert.True(control2.Rendered); |
|
||||
Assert.False(control3.Rendered); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Static_Render_Method_Does_Not_Update_TransformedBounds() |
|
||||
{ |
|
||||
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) |
|
||||
{ |
|
||||
var target = new Border(); |
|
||||
var expected = new TransformedBounds(new Rect(1, 2, 3, 4), new Rect(4, 5, 6, 7), Matrix.CreateRotation(0.8)); |
|
||||
|
|
||||
target.SetTransformedBounds(expected); |
|
||||
|
|
||||
var renderTarget = Mock.Of<IRenderTarget>(x => |
|
||||
x.CreateDrawingContext(It.IsAny<IVisualBrushRenderer>()) == Mock.Of<IDrawingContextImpl>()); |
|
||||
ImmediateRenderer.Render(target, renderTarget); |
|
||||
|
|
||||
Assert.Equal(expected, target.TransformedBounds); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private class RenderRoot : TestRoot, IRenderRoot |
|
||||
{ |
|
||||
public List<Rect> Invalidations { get; } = new(); |
|
||||
void IRenderRoot.Invalidate(Rect rect) => Invalidations.Add(rect); |
|
||||
} |
|
||||
|
|
||||
private class TestControl : Control |
|
||||
{ |
|
||||
public bool Rendered { get; private set; } |
|
||||
|
|
||||
public override void Render(DrawingContext context) |
|
||||
=> Rendered = true; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,458 +0,0 @@ |
|||||
using System; |
|
||||
using System.Linq; |
|
||||
using Avalonia.Controls; |
|
||||
using Avalonia.Controls.Presenters; |
|
||||
using Avalonia.Layout; |
|
||||
using Avalonia.Media; |
|
||||
using Avalonia.Rendering; |
|
||||
using Avalonia.UnitTests; |
|
||||
using Avalonia.VisualTree; |
|
||||
using Xunit; |
|
||||
|
|
||||
namespace Avalonia.Base.UnitTests.Rendering |
|
||||
{ |
|
||||
public class ImmediateRendererTests_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 ImmediateRenderer(root, root.CreateRenderTarget); |
|
||||
root.Measure(Size.Infinity); |
|
||||
root.Arrange(new Rect(root.DesiredSize)); |
|
||||
root.Renderer.Paint(new Rect(root.ClientSize)); |
|
||||
|
|
||||
var result = root.Renderer.HitTest(new Point(100, 100), root, null); |
|
||||
|
|
||||
Assert.Equal(new[] { root.Child, root }, 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 ImmediateRenderer(root, root.CreateRenderTarget); |
|
||||
root.Measure(Size.Infinity); |
|
||||
root.Arrange(new Rect(root.DesiredSize)); |
|
||||
root.Renderer.Paint(new Rect(root.ClientSize)); |
|
||||
|
|
||||
var result = root.Renderer.HitTest(new Point(100, 100), root, null); |
|
||||
|
|
||||
Assert.Equal(new[] { root }, 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 ImmediateRenderer(root, root.CreateRenderTarget); |
|
||||
root.Measure(Size.Infinity); |
|
||||
root.Arrange(new Rect(root.DesiredSize)); |
|
||||
root.Renderer.Paint(new Rect(root.ClientSize)); |
|
||||
|
|
||||
var result = root.Renderer.HitTest(new Point(10, 10), root, null); |
|
||||
|
|
||||
Assert.Equal(new[] { root }, 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 ImmediateRenderer(root, root.CreateRenderTarget); |
|
||||
root.Measure(Size.Infinity); |
|
||||
root.Arrange(new Rect(container.DesiredSize)); |
|
||||
root.Renderer.Paint(new Rect(root.ClientSize)); |
|
||||
|
|
||||
var result = root.Renderer.HitTest(new Point(100, 100), root, null); |
|
||||
|
|
||||
Assert.Equal(new[] { container.Children[1], container.Children[0], container, root }, 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 ImmediateRenderer(root, root.CreateRenderTarget); |
|
||||
root.Measure(Size.Infinity); |
|
||||
root.Arrange(new Rect(container.DesiredSize)); |
|
||||
root.Renderer.Paint(new Rect(root.ClientSize)); |
|
||||
|
|
||||
var result = root.Renderer.HitTest(new Point(100, 100), root, null); |
|
||||
|
|
||||
Assert.Equal( |
|
||||
new[] |
|
||||
{ |
|
||||
container.Children[2], |
|
||||
container.Children[0], |
|
||||
container.Children[1], |
|
||||
container, |
|
||||
root |
|
||||
}, |
|
||||
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 ImmediateRenderer(root, root.CreateRenderTarget); |
|
||||
container.Measure(Size.Infinity); |
|
||||
container.Arrange(new Rect(container.DesiredSize)); |
|
||||
root.Renderer.Paint(new Rect(root.ClientSize)); |
|
||||
|
|
||||
var result = root.Renderer.HitTest(new Point(120, 120), root, null); |
|
||||
|
|
||||
Assert.Equal(new Visual[] { 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 ImmediateRenderer(root, root.CreateRenderTarget); |
|
||||
root.Measure(Size.Infinity); |
|
||||
root.Arrange(new Rect(container.DesiredSize)); |
|
||||
root.Renderer.Paint(new Rect(root.ClientSize)); |
|
||||
|
|
||||
var result = root.Renderer.HitTest(new Point(50, 50), root, null); |
|
||||
|
|
||||
Assert.Equal(new Visual[] { container, root }, 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 ImmediateRenderer(root, root.CreateRenderTarget); |
|
||||
root.Measure(Size.Infinity); |
|
||||
root.Arrange(new Rect(container.DesiredSize)); |
|
||||
root.Renderer.Paint(new Rect(root.ClientSize)); |
|
||||
|
|
||||
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(new Rect(root.ClientSize)); |
|
||||
|
|
||||
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_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 ImmediateRenderer(root, root.CreateRenderTarget); |
|
||||
root.Measure(Size.Infinity); |
|
||||
root.Arrange(new Rect(root.DesiredSize)); |
|
||||
root.Renderer.Paint(new Rect(root.ClientSize)); |
|
||||
|
|
||||
var result = root.Renderer.HitTest(new Point(75, 100), root, null).First(); |
|
||||
Assert.Equal(border, result); |
|
||||
|
|
||||
result = root.Renderer.HitTest(new Point(125, 100), root, null).First(); |
|
||||
Assert.Equal(border, result); |
|
||||
|
|
||||
result = root.Renderer.HitTest(new Point(175, 100), root, null).First(); |
|
||||
Assert.Equal(root, result); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private static IDisposable TestApplication() |
|
||||
{ |
|
||||
return UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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<Visual>(), 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<Visual>(), 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<Visual>(), parent); |
|
||||
var child2 = new VisualNode(Mock.Of<Visual>(), 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<Visual>(), node) { LayerRoot = root }); |
|
||||
node.AddChild(new VisualNode(Mock.Of<Visual>(), node) { LayerRoot = root }); |
|
||||
node.AddChild(new VisualNode(Mock.Of<Visual>(), node) { LayerRoot = root }); |
|
||||
node.AddChild(new VisualNode(Mock.Of<Visual>(), node) { LayerRoot = root }); |
|
||||
|
|
||||
var layers = new SceneLayers(root); |
|
||||
var target = new DeferredDrawingContextImpl(null, layers); |
|
||||
var child1 = new VisualNode(Mock.Of<Visual>(), node) { LayerRoot = root }; |
|
||||
var child2 = new VisualNode(Mock.Of<Visual>(), 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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
File diff suppressed because it is too large
@ -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 Visual[] { 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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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 Visual[] { root, border, decorator }, result); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,123 +0,0 @@ |
|||||
using Avalonia.Controls; |
|
||||
using Avalonia.Rendering.SceneGraph; |
|
||||
using Avalonia.Utilities; |
|
||||
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(new Control(), null); |
|
||||
var node2 = new VisualNode(new Control(), null); |
|
||||
|
|
||||
Assert.Same(node1.Children, node2.Children); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Adding_Child_Should_Create_Collection() |
|
||||
{ |
|
||||
var node = new VisualNode(new Control(), null); |
|
||||
var collection = node.Children; |
|
||||
|
|
||||
node.AddChild(new VisualNode(new Border(), node)); |
|
||||
|
|
||||
Assert.NotSame(collection, node.Children); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Empty_DrawOperations_Collections_Should_Be_Shared() |
|
||||
{ |
|
||||
var node1 = new VisualNode(new Control(), null); |
|
||||
var node2 = new VisualNode(new Control(), null); |
|
||||
|
|
||||
Assert.Same(node1.DrawOperations, node2.DrawOperations); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Adding_DrawOperation_Should_Create_Collection() |
|
||||
{ |
|
||||
var node = new VisualNode(new Control(), 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(new Control(), 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(new Control(), 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(new Control(), 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(new Control(), null); |
|
||||
var scene = new Scene(new Control()); |
|
||||
|
|
||||
node.SortChildren(scene); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void TrimChildren_Should_Work_Correctly() |
|
||||
{ |
|
||||
var parent = new VisualNode(new Control(), null); |
|
||||
var child1 = new VisualNode(new Control(), parent); |
|
||||
var child2 = new VisualNode(new Control(), parent); |
|
||||
var child3 = new VisualNode(new Control(), 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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,52 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using Avalonia.Controls; |
|
||||
using Avalonia.Controls.Shapes; |
|
||||
using Avalonia.Media; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.Rendering; |
|
||||
using Avalonia.UnitTests; |
|
||||
using Avalonia.VisualTree; |
|
||||
using Moq; |
|
||||
using Xunit; |
|
||||
|
|
||||
namespace Avalonia.Base.UnitTests.VisualTree |
|
||||
{ |
|
||||
public class TransformedBoundsTests |
|
||||
{ |
|
||||
[Fact] |
|
||||
public void Should_Track_Bounds() |
|
||||
{ |
|
||||
using (UnitTestApplication.Start(TestServices.StyledWindow)) |
|
||||
{ |
|
||||
var control = default(Rectangle); |
|
||||
var tree = new Decorator |
|
||||
{ |
|
||||
Padding = new Thickness(10), |
|
||||
Child = new Decorator |
|
||||
{ |
|
||||
Padding = new Thickness(5), |
|
||||
Child = control = new Rectangle |
|
||||
{ |
|
||||
Width = 15, |
|
||||
Height = 15, |
|
||||
}, |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
var context = new DrawingContext(Mock.Of<IDrawingContextImpl>()); |
|
||||
|
|
||||
tree.Measure(Size.Infinity); |
|
||||
tree.Arrange(new Rect(0, 0, 100, 100)); |
|
||||
ImmediateRenderer.Render(tree, context, true); |
|
||||
|
|
||||
var track = control.GetObservable(Visual.TransformedBoundsProperty); |
|
||||
var results = new List<TransformedBounds?>(); |
|
||||
track.Subscribe(results.Add); |
|
||||
|
|
||||
Assert.Equal(new Rect(0, 0, 15, 15), results[0].Value.Bounds); |
|
||||
Assert.Equal(Matrix.CreateTranslation(42, 42), results[0].Value.Transform); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,207 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Controls.Embedding; |
||||
|
using Avalonia.Controls.Platform.Surfaces; |
||||
|
using Avalonia.Controls.Presenters; |
||||
|
using Avalonia.Controls.Templates; |
||||
|
using Avalonia.Data; |
||||
|
using Avalonia.Input; |
||||
|
using Avalonia.Input.Raw; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Rendering; |
||||
|
using Avalonia.Rendering.Composition; |
||||
|
using Avalonia.Threading; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.UnitTests; |
||||
|
public class CompositorTestServices : IDisposable |
||||
|
{ |
||||
|
private readonly IDisposable _app; |
||||
|
public Compositor Compositor { get; } |
||||
|
public ManualRenderTimer Timer { get; } = new(); |
||||
|
public EmbeddableControlRoot TopLevel { get; } |
||||
|
public CompositingRenderer Renderer { get; } = null!; |
||||
|
public DebugEvents Events { get; } = new(); |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
TopLevel.Renderer.Stop(); |
||||
|
TopLevel.Dispose(); |
||||
|
_app.Dispose(); |
||||
|
} |
||||
|
|
||||
|
public CompositorTestServices(Size? size = null, IPlatformRenderInterface renderInterface = null) |
||||
|
{ |
||||
|
var services = TestServices.MockPlatformRenderInterface; |
||||
|
if (renderInterface != null) |
||||
|
services = services.With(renderInterface: renderInterface); |
||||
|
|
||||
|
_app = UnitTestApplication.Start(services); |
||||
|
try |
||||
|
{ |
||||
|
AvaloniaLocator.CurrentMutable.Bind<IRenderTimer>().ToConstant(Timer); |
||||
|
AvaloniaLocator.CurrentMutable.Bind<IRenderLoop>() |
||||
|
.ToConstant(new RenderLoop(Timer, Dispatcher.UIThread)); |
||||
|
|
||||
|
Compositor = new Compositor(AvaloniaLocator.Current.GetRequiredService<IRenderLoop>(), null); |
||||
|
var impl = new TopLevelImpl(Compositor, size ?? new Size(1000, 1000)); |
||||
|
TopLevel = new EmbeddableControlRoot(impl) |
||||
|
{ |
||||
|
Template = new FuncControlTemplate((parent, scope) => |
||||
|
{ |
||||
|
var presenter = new ContentPresenter |
||||
|
{ |
||||
|
[~ContentPresenter.ContentProperty] = new TemplateBinding(ContentControl.ContentProperty) |
||||
|
}; |
||||
|
scope.Register("PART_ContentPresenter", presenter); |
||||
|
return presenter; |
||||
|
}) |
||||
|
}; |
||||
|
Renderer = impl.Renderer; |
||||
|
TopLevel.Prepare(); |
||||
|
TopLevel.Renderer.Start(); |
||||
|
RunJobs(); |
||||
|
Renderer.CompositionTarget.Server.DebugEvents = Events; |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
_app.Dispose(); |
||||
|
throw; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void RunJobs() |
||||
|
{ |
||||
|
Dispatcher.UIThread.RunJobs(); |
||||
|
Timer.TriggerTick(); |
||||
|
Dispatcher.UIThread.RunJobs(); |
||||
|
} |
||||
|
|
||||
|
public void AssertRects(params Rect[] rects) |
||||
|
{ |
||||
|
RunJobs(); |
||||
|
var toAssert = rects.Select(x => x.ToString()).Distinct().OrderBy(x => x); |
||||
|
var invalidated = Events.Rects.Select(x => x.ToString()).Distinct().OrderBy(x => x); |
||||
|
Assert.Equal(toAssert, invalidated); |
||||
|
Events.Rects.Clear(); |
||||
|
} |
||||
|
|
||||
|
public void AssertHitTest(double x, double y, Func<Visual, bool> filter, params object[] expected) |
||||
|
=> AssertHitTest(new Point(x, y), filter, expected); |
||||
|
|
||||
|
public void AssertHitTest(Point pt, Func<Visual, bool> filter, params object[] expected) |
||||
|
{ |
||||
|
RunJobs(); |
||||
|
var tested = Renderer.HitTest(pt, TopLevel, filter); |
||||
|
Assert.Equal(expected, tested); |
||||
|
} |
||||
|
|
||||
|
public void AssertHitTestFirst(Point pt, Func<Visual, bool> filter, object expected) |
||||
|
{ |
||||
|
RunJobs(); |
||||
|
var tested = Renderer.HitTest(pt, TopLevel, filter).First(); |
||||
|
Assert.Equal(expected, tested); |
||||
|
} |
||||
|
|
||||
|
public class DebugEvents : ICompositionTargetDebugEvents |
||||
|
{ |
||||
|
public List<Rect> Rects = new(); |
||||
|
|
||||
|
public void RectInvalidated(Rect rc) |
||||
|
{ |
||||
|
Rects.Add(rc); |
||||
|
} |
||||
|
|
||||
|
public void Reset() |
||||
|
{ |
||||
|
Rects.Clear(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class ManualRenderTimer : IRenderTimer |
||||
|
{ |
||||
|
public event Action<TimeSpan> Tick; |
||||
|
public bool RunsInBackground => false; |
||||
|
public void TriggerTick() => Tick?.Invoke(TimeSpan.Zero); |
||||
|
public Task TriggerBackgroundTick() => Task.Run(TriggerTick); |
||||
|
} |
||||
|
|
||||
|
class TopLevelImpl : ITopLevelImpl |
||||
|
{ |
||||
|
private readonly Compositor _compositor; |
||||
|
public CompositingRenderer Renderer { get; private set; } |
||||
|
|
||||
|
public TopLevelImpl(Compositor compositor, Size clientSize) |
||||
|
{ |
||||
|
ClientSize = clientSize; |
||||
|
_compositor = compositor; |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public Size ClientSize { get; } |
||||
|
public Size? FrameSize { get; } |
||||
|
public double RenderScaling => 1; |
||||
|
public IEnumerable<object> Surfaces { get; } = new[] { new DummyFramebufferSurface() }; |
||||
|
public Action<RawInputEventArgs> Input { get; set; } |
||||
|
public Action<Rect> Paint { get; set; } |
||||
|
public Action<Size, PlatformResizeReason> Resized { get; set; } |
||||
|
public Action<double> ScalingChanged { get; set; } |
||||
|
public Action<WindowTransparencyLevel> TransparencyLevelChanged { get; set; } |
||||
|
|
||||
|
class DummyFramebufferSurface : IFramebufferPlatformSurface |
||||
|
{ |
||||
|
public ILockedFramebuffer Lock() |
||||
|
{ |
||||
|
var ptr = Marshal.AllocHGlobal(128); |
||||
|
return new LockedFramebuffer(ptr, new PixelSize(1, 1), 4, new Vector(96, 96), |
||||
|
PixelFormat.Rgba8888, () => Marshal.FreeHGlobal(ptr)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public IRenderer CreateRenderer(IRenderRoot root) |
||||
|
{ |
||||
|
return Renderer = new CompositingRenderer(root, _compositor, () => Surfaces); |
||||
|
} |
||||
|
|
||||
|
public void Invalidate(Rect rect) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public void SetInputRoot(IInputRoot inputRoot) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public Point PointToClient(PixelPoint point) => default; |
||||
|
|
||||
|
public PixelPoint PointToScreen(Point point) => new(); |
||||
|
|
||||
|
public void SetCursor(ICursorImpl cursor) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public Action Closed { get; set; } |
||||
|
public Action LostFocus { get; set; } |
||||
|
public IMouseDevice MouseDevice { get; } = new MouseDevice(); |
||||
|
public IPopupImpl CreatePopup() => throw new NotImplementedException(); |
||||
|
|
||||
|
public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public WindowTransparencyLevel TransparencyLevel { get; } |
||||
|
|
||||
|
public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } |
||||
|
} |
||||
|
} |
||||
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
Loading…
Reference in new issue