From ada15eba002f8266a9ed339fd2a4f10cccfa123e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 4 Nov 2016 18:54:52 +0100 Subject: [PATCH 001/175] Initial implementation of low-level scene graph. Whole tree is currently being updated and rendered still though and lots doesn't even build. --- samples/RenderTest/MainWindow.xaml.cs | 2 +- src/Avalonia.Controls/Shapes/Shape.cs | 2 +- src/Avalonia.Visuals/Avalonia.Visuals.csproj | 28 ++- src/Avalonia.Visuals/Media/DrawingContext.cs | 41 ++-- ...awingContext.cs => IDrawingContextImpl.cs} | 7 +- .../Media/Imaging/RenderTargetBitmap.cs | 2 +- .../Platform/IFormattedTextImpl.cs | 5 + .../Platform/IRenderTarget.cs | 2 +- .../Rendering/DeferredRenderer.cs | 128 ++++++++++ src/Avalonia.Visuals/Rendering/IRenderer.cs | 3 +- src/Avalonia.Visuals/Rendering/Renderer.cs | 3 + .../Rendering/RendererMixin.cs | 56 ++--- .../SceneGraph/DeferredDrawingContextImpl.cs | 222 ++++++++++++++++++ .../Rendering/SceneGraph/GeometryNode.cs | 39 +++ .../Rendering/SceneGraph/ISceneNode.cs | 14 ++ .../Rendering/SceneGraph/IVisualNode.cs | 15 ++ .../Rendering/SceneGraph/ImageNode.cs | 42 ++++ .../Rendering/SceneGraph/LineNode.cs | 35 +++ .../Rendering/SceneGraph/RectangleNode.cs | 50 ++++ .../Rendering/SceneGraph/Scene.cs | 72 ++++++ .../Rendering/SceneGraph/SceneBuilder.cs | 90 +++++++ .../Rendering/SceneGraph/TextNode.cs | 39 +++ .../Rendering/SceneGraph/VisualNode.cs | 65 +++++ .../Avalonia.Cairo.v2.ncrunchproject | 6 +- .../Avalonia.Direct2D1.csproj | 2 +- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 6 +- .../Media/AvaloniaTextRenderer.cs | 4 +- ...rawingContext.cs => DrawingContextImpl.cs} | 21 +- .../Media/FormattedTextImpl.cs | 4 + .../Media/Imaging/RenderTargetBitmapImpl.cs | 2 +- .../Avalonia.Direct2D1/Media/TileBrushImpl.cs | 2 +- .../Avalonia.Direct2D1/RenderTarget.cs | 6 +- ...ia.Direct2D1.RenderTests.v2.ncrunchproject | 9 +- .../Avalonia.Visuals.UnitTests.csproj | 1 + .../Rendering/SceneGraph/SceneBuilderTests.cs | 130 ++++++++++ tests/Avalonia.Visuals.UnitTests/TestRoot.cs | 2 +- 36 files changed, 1064 insertions(+), 93 deletions(-) rename src/Avalonia.Visuals/Media/{IDrawingContext.cs => IDrawingContextImpl.cs} (92%) create mode 100644 src/Avalonia.Visuals/Rendering/DeferredRenderer.cs create mode 100644 src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs create mode 100644 src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs create mode 100644 src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs create mode 100644 src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs create mode 100644 src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs create mode 100644 src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs create mode 100644 src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs create mode 100644 src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs create mode 100644 src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs create mode 100644 src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs create mode 100644 src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs rename src/Windows/Avalonia.Direct2D1/Media/{DrawingContext.cs => DrawingContextImpl.cs} (95%) create mode 100644 tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs diff --git a/samples/RenderTest/MainWindow.xaml.cs b/samples/RenderTest/MainWindow.xaml.cs index feb121186fd..8fe306df7d3 100644 --- a/samples/RenderTest/MainWindow.xaml.cs +++ b/samples/RenderTest/MainWindow.xaml.cs @@ -22,7 +22,7 @@ public MainWindow() this.InitializeComponent(); this.CreateAnimations(); this.AttachDevTools(); - RendererMixin.DrawFpsCounter = true; + Renderer.DrawFps = true; } private void InitializeComponent() diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index 427749263a4..688d507c8ee 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -64,7 +64,7 @@ public Geometry RenderedGeometry if (DefiningGeometry != null) { _renderedGeometry = DefiningGeometry.Clone(); - _renderedGeometry.Transform = new MatrixTransform(_transform); + ////_renderedGeometry.Transform = new MatrixTransform(_transform); } } diff --git a/src/Avalonia.Visuals/Avalonia.Visuals.csproj b/src/Avalonia.Visuals/Avalonia.Visuals.csproj index d9736e4c6ec..aeffde81edc 100644 --- a/src/Avalonia.Visuals/Avalonia.Visuals.csproj +++ b/src/Avalonia.Visuals/Avalonia.Visuals.csproj @@ -100,15 +100,34 @@ - + + + + + + + + + + + + + + + + + + + + @@ -131,7 +150,6 @@ - @@ -140,14 +158,8 @@ - - - - - - diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index ddaa681bab7..9a5e83dbcb9 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -1,16 +1,11 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Avalonia.Media.Imaging; namespace Avalonia.Media { public sealed class DrawingContext : IDisposable { - private readonly IDrawingContextImpl _impl; private int _currentLevel; //Internal tranformation that is applied but not exposed anywhere //To be used for DPI scaling, etc @@ -41,10 +36,11 @@ public TransformContainer(Matrix localTransform, Matrix containerTransform) public DrawingContext(IDrawingContextImpl impl, Matrix? hiddenPostTransform = null) { - _impl = impl; + PlatformImpl = impl; _hiddenPostTransform = hiddenPostTransform; } + public IDrawingContextImpl PlatformImpl { get; } private Matrix _currentTransform = Matrix.Identity; @@ -62,7 +58,7 @@ private set var transform = _currentTransform*_currentContainerTransform; if (_hiddenPostTransform.HasValue) transform = transform*_hiddenPostTransform.Value; - _impl.Transform = transform; + PlatformImpl.Transform = transform; } } @@ -79,7 +75,7 @@ private set /// The rect in the image to draw. /// The rect in the output to draw to. public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect) - => _impl.DrawImage(source, opacity, sourceRect, destRect); + => PlatformImpl.DrawImage(source.PlatformImpl, opacity, sourceRect, destRect); /// /// Draws a line. @@ -87,7 +83,7 @@ public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect dest /// The stroke pen. /// The first point of the line. /// The second point of the line. - public void DrawLine(Pen pen, Point p1, Point p2) => _impl.DrawLine(pen, p1, p2); + public void DrawLine(Pen pen, Point p1, Point p2) => PlatformImpl.DrawLine(pen, p1, p2); /// /// Draws a geometry. @@ -95,7 +91,8 @@ public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect dest /// The fill brush. /// The stroke pen. /// The geometry. - public void DrawGeometry(IBrush brush, Pen pen, Geometry geometry) => _impl.DrawGeometry(brush, pen, geometry); + public void DrawGeometry(IBrush brush, Pen pen, Geometry geometry) + => PlatformImpl.DrawGeometry(brush, pen, geometry.PlatformImpl); /// /// Draws the outline of a rectangle. @@ -104,7 +101,7 @@ public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect dest /// The rectangle bounds. /// The corner radius. public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0.0f) - => _impl.DrawRectangle(pen, rect, cornerRadius); + => PlatformImpl.DrawRectangle(pen, rect, cornerRadius); /// /// Draws text. @@ -113,7 +110,7 @@ public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0.0f) /// The upper-left corner of the text. /// The text. public void DrawText(IBrush foreground, Point origin, FormattedText text) - => _impl.DrawText(foreground, origin, text); + => PlatformImpl.DrawText(foreground, origin, text.PlatformImpl); /// /// Draws a filled rectangle. @@ -122,7 +119,7 @@ public void DrawText(IBrush foreground, Point origin, FormattedText text) /// The rectangle bounds. /// The corner radius. public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f) - => _impl.FillRectangle(brush, rect, cornerRadius); + => PlatformImpl.FillRectangle(brush, rect, cornerRadius); public struct PushedState : IDisposable { @@ -162,13 +159,13 @@ public void Dispose() if (_type == PushedStateType.Matrix) _context.CurrentTransform = _matrix; else if (_type == PushedStateType.Clip) - _context._impl.PopClip(); + _context.PlatformImpl.PopClip(); else if (_type == PushedStateType.Opacity) - _context._impl.PopOpacity(); + _context.PlatformImpl.PopOpacity(); else if (_type == PushedStateType.GeometryClip) - _context._impl.PopGeometryClip(); + _context.PlatformImpl.PopGeometryClip(); else if (_type == PushedStateType.OpacityMask) - _context._impl.PopOpacityMask(); + _context.PlatformImpl.PopOpacityMask(); else if (_type == PushedStateType.MatrixContainer) { var cont = _context._transformContainers.Pop(); @@ -186,7 +183,7 @@ public void Dispose() /// A disposable used to undo the clip rectangle. public PushedState PushClip(Rect clip) { - _impl.PushClip(clip); + PlatformImpl.PushClip(clip); return new PushedState(this, PushedState.PushedStateType.Clip); } @@ -198,7 +195,7 @@ public PushedState PushClip(Rect clip) public PushedState PushGeometryClip(Geometry clip) { Contract.Requires(clip != null); - _impl.PushGeometryClip(clip); + PlatformImpl.PushGeometryClip(clip); return new PushedState(this, PushedState.PushedStateType.GeometryClip); } @@ -210,7 +207,7 @@ public PushedState PushGeometryClip(Geometry clip) public PushedState PushOpacity(double opacity) //TODO: Eliminate platform-specific push opacity call { - _impl.PushOpacity(opacity); + PlatformImpl.PushOpacity(opacity); return new PushedState(this, PushedState.PushedStateType.Opacity); } @@ -224,7 +221,7 @@ public PushedState PushOpacity(double opacity) /// A disposable to undo the opacity mask. public PushedState PushOpacityMask(IBrush mask, Rect bounds) { - _impl.PushOpacityMask(mask, bounds); + PlatformImpl.PushOpacityMask(mask, bounds); return new PushedState(this, PushedState.PushedStateType.OpacityMask); } @@ -278,7 +275,7 @@ public void Dispose() _states = null; TransformStackPool.Push(_transformContainers); _transformContainers = null; - _impl.Dispose(); + PlatformImpl.Dispose(); } } } diff --git a/src/Avalonia.Visuals/Media/IDrawingContext.cs b/src/Avalonia.Visuals/Media/IDrawingContextImpl.cs similarity index 92% rename from src/Avalonia.Visuals/Media/IDrawingContext.cs rename to src/Avalonia.Visuals/Media/IDrawingContextImpl.cs index c63abc160fb..0fb57923dd8 100644 --- a/src/Avalonia.Visuals/Media/IDrawingContext.cs +++ b/src/Avalonia.Visuals/Media/IDrawingContextImpl.cs @@ -3,6 +3,7 @@ using System; using Avalonia.Media.Imaging; +using Avalonia.Platform; namespace Avalonia.Media { @@ -23,7 +24,7 @@ public interface IDrawingContextImpl : IDisposable /// The opacity to draw with. /// The rect in the image to draw. /// The rect in the output to draw to. - void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect); + void DrawImage(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect); /// /// Draws a line. @@ -39,7 +40,7 @@ public interface IDrawingContextImpl : IDisposable /// The fill brush. /// The stroke pen. /// The geometry. - void DrawGeometry(IBrush brush, Pen pen, Geometry geometry); + void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry); /// /// Draws the outline of a rectangle. @@ -55,7 +56,7 @@ public interface IDrawingContextImpl : IDisposable /// The foreground brush. /// The upper-left corner of the text. /// The text. - void DrawText(IBrush foreground, Point origin, FormattedText text); + void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text); /// /// Draws a filled rectangle. diff --git a/src/Avalonia.Visuals/Media/Imaging/RenderTargetBitmap.cs b/src/Avalonia.Visuals/Media/Imaging/RenderTargetBitmap.cs index 4cc9c3fdf25..749d690d487 100644 --- a/src/Avalonia.Visuals/Media/Imaging/RenderTargetBitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/RenderTargetBitmap.cs @@ -48,6 +48,6 @@ private static IBitmapImpl CreateImpl(int width, int height) return factory.CreateRenderTargetBitmap(width, height); } - public DrawingContext CreateDrawingContext() => PlatformImpl.CreateDrawingContext(); + public IDrawingContextImpl CreateDrawingContext() => PlatformImpl.CreateDrawingContext(); } } diff --git a/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs b/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs index 14f9e987bdc..e3a44c437ee 100644 --- a/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs @@ -17,6 +17,11 @@ public interface IFormattedTextImpl : IDisposable /// Size Constraint { get; set; } + /// + /// Gets the text. + /// + string Text { get; } + /// /// Gets the lines in the text. /// diff --git a/src/Avalonia.Visuals/Platform/IRenderTarget.cs b/src/Avalonia.Visuals/Platform/IRenderTarget.cs index 75ba9d5a560..fcde1948841 100644 --- a/src/Avalonia.Visuals/Platform/IRenderTarget.cs +++ b/src/Avalonia.Visuals/Platform/IRenderTarget.cs @@ -17,6 +17,6 @@ public interface IRenderTarget : IDisposable /// /// Creates an for a rendering session. /// - DrawingContext CreateDrawingContext(); + IDrawingContextImpl CreateDrawingContext(); } } diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs new file mode 100644 index 00000000000..37215442345 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -0,0 +1,128 @@ +using System; +using System.Diagnostics; +using System.Linq; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Rendering +{ + public class DeferredRenderer : IRenderer + { + private readonly IRenderLoop _renderLoop; + private readonly IRenderRoot _root; + private Scene _scene; + private IRenderTarget _renderTarget; + private bool _needsUpdate; + private bool _needsRender; + + private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); + private int _totalFrames; + private int _framesThisSecond; + private int _fps; + private TimeSpan _lastFpsUpdate; + + public DeferredRenderer(IRenderRoot root, IRenderLoop renderLoop) + { + Contract.Requires(root != null); + + _root = root; + _scene = new Scene(root); + _renderLoop = renderLoop; + _renderLoop.Tick += OnRenderLoopTick; + } + + public bool DrawFps { get; set; } + + public void AddDirty(IVisual visual) + { + if (!_needsUpdate) + { + _needsUpdate = true; + Dispatcher.UIThread.InvokeAsync(UpdateScene, DispatcherPriority.Render); + } + } + + public void Dispose() + { + _renderLoop.Tick -= OnRenderLoopTick; + } + + public void Render(Rect rect) + { + if (_renderTarget == null) + { + _renderTarget = _root.CreateRenderTarget(); + } + + try + { + _totalFrames++; + + using (var context = _renderTarget.CreateDrawingContext()) + { + _scene.Root.Render(context); + + if (DrawFps) + { + RenderFps(context); + } + } + } + catch (RenderTargetCorruptedException ex) + { + Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex); + _renderTarget.Dispose(); + _renderTarget = null; + } + } + + private void RenderFps(IDrawingContextImpl context) + { + var now = _stopwatch.Elapsed; + var elapsed = now - _lastFpsUpdate; + + _framesThisSecond++; + + if (elapsed.TotalSeconds > 1) + { + _fps = (int)(_framesThisSecond / elapsed.TotalSeconds); + _framesThisSecond = 0; + _lastFpsUpdate = now; + } + + var pt = new Point(40, 40); + using ( + var txt = new FormattedText("Frame #" + _totalFrames + " FPS: " + _fps, "Arial", 18, + FontStyle.Normal, + TextAlignment.Left, + FontWeight.Normal, + TextWrapping.NoWrap)) + { + context.Transform = Matrix.Identity; + context.FillRectangle(Brushes.White, new Rect(pt, txt.Measure())); + context.DrawText(Brushes.Black, pt, txt.PlatformImpl); + } + } + + private void UpdateScene() + { + Dispatcher.UIThread.VerifyAccess(); + + _scene = SceneBuilder.Update(_scene); + _needsUpdate = false; + _needsRender = true; + _root.Invalidate(new Rect(_root.ClientSize)); + } + + private void OnRenderLoopTick(object sender, EventArgs e) + { + //if (_needsRender) + //{ + // _needsRender = false; + //} + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/IRenderer.cs b/src/Avalonia.Visuals/Rendering/IRenderer.cs index c643662179b..119bb4c8d1f 100644 --- a/src/Avalonia.Visuals/Rendering/IRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/IRenderer.cs @@ -8,8 +8,9 @@ namespace Avalonia.Rendering { public interface IRenderer : IDisposable { - void AddDirty(IVisual visual); + bool DrawFps { get; set; } + void AddDirty(IVisual visual); void Render(Rect rect); } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Rendering/Renderer.cs b/src/Avalonia.Visuals/Rendering/Renderer.cs index ed58d129aff..3b2fe19be40 100644 --- a/src/Avalonia.Visuals/Rendering/Renderer.cs +++ b/src/Avalonia.Visuals/Rendering/Renderer.cs @@ -23,6 +23,8 @@ public Renderer(IRenderRoot root, IRenderLoop renderLoop) _renderLoop.Tick += OnRenderLoopTick; } + public bool DrawFps { get; set; } + public void AddDirty(IVisual visual) { _dirty = true; @@ -42,6 +44,7 @@ public void Render(Rect rect) try { + RendererMixin.DrawFpsCounter = DrawFps; _renderTarget.Render(_root); } catch (RenderTargetCorruptedException ex) diff --git a/src/Avalonia.Visuals/Rendering/RendererMixin.cs b/src/Avalonia.Visuals/Rendering/RendererMixin.cs index 3d13501edd4..3a24e1a22f6 100644 --- a/src/Avalonia.Visuals/Rendering/RendererMixin.cs +++ b/src/Avalonia.Visuals/Rendering/RendererMixin.cs @@ -42,34 +42,34 @@ public static class RendererMixin /// The visual to render. public static void Render(this IRenderTarget renderTarget, IVisual visual) { - using (var ctx = renderTarget.CreateDrawingContext()) - { - ctx.Render(visual); - s_frameNum++; - if (DrawFpsCounter) - { - s_currentFrames++; - var now = s_stopwatch.Elapsed; - var elapsed = now - s_lastMeasure; - if (elapsed.TotalSeconds > 1) - { - s_fps = (int) (s_currentFrames/elapsed.TotalSeconds); - s_currentFrames = 0; - s_lastMeasure = now; - } - var pt = new Point(40, 40); - using ( - var txt = new FormattedText("Frame #" + s_frameNum + " FPS: " + s_fps, "Arial", 18, - FontStyle.Normal, - TextAlignment.Left, - FontWeight.Normal, - TextWrapping.NoWrap)) - { - ctx.FillRectangle(Brushes.White, new Rect(pt, txt.Measure())); - ctx.DrawText(Brushes.Black, pt, txt); - } - } - } + ////using (var ctx = renderTarget.CreateDrawingContext()) + ////{ + //// ctx.Render(visual); + //// s_frameNum++; + //// if (DrawFpsCounter) + //// { + //// s_currentFrames++; + //// var now = s_stopwatch.Elapsed; + //// var elapsed = now - s_lastMeasure; + //// if (elapsed.TotalSeconds > 1) + //// { + //// s_fps = (int) (s_currentFrames/elapsed.TotalSeconds); + //// s_currentFrames = 0; + //// s_lastMeasure = now; + //// } + //// var pt = new Point(40, 40); + //// using ( + //// var txt = new FormattedText("Frame #" + s_frameNum + " FPS: " + s_fps, "Arial", 18, + //// FontStyle.Normal, + //// TextAlignment.Left, + //// FontWeight.Normal, + //// TextWrapping.NoWrap)) + //// { + //// ctx.FillRectangle(Brushes.White, new Rect(pt, txt.Measure())); + //// ctx.DrawText(Brushes.Black, pt, txt); + //// } + //// } + ////} } /// diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs new file mode 100644 index 00000000000..5717ccdf0eb --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -0,0 +1,222 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Reactive.Disposables; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.VisualTree; + +namespace Avalonia.Rendering.SceneGraph +{ + public class DeferredDrawingContextImpl : IDrawingContextImpl + { + private Stack _stack = new Stack(); + + public Matrix Transform { get; set; } + + private VisualNode Node => _stack.Peek().Node; + + private int Index + { + get { return _stack.Peek().Index; } + set { _stack.Peek().Index = value; } + } + + public IDisposable Begin(VisualNode node) + { + _stack.Push(new Frame(node)); + return Disposable.Create(Pop); + } + + public void Dispose() + { + } + + public void AddChild(IVisualNode visualNode) + { + if (_stack.Count > 0) + { + var next = NextNodeAs(); + + if (next == null || next != visualNode) + { + Add(visualNode); + } + else + { + ++Index; + } + } + } + + public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry) + { + var next = NextNodeAs(); + + if (next == null || !next.Equals(Transform, brush, pen, geometry)) + { + Add(new GeometryNode(Transform, brush, pen, geometry)); + } + else + { + ++Index; + } + } + + public void DrawImage(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect) + { + var next = NextNodeAs(); + + if (next == null || !next.Equals(Transform, source, opacity, sourceRect, destRect)) + { + Add(new ImageNode(Transform, source, opacity, sourceRect, destRect)); + } + else + { + ++Index; + } + } + + public void DrawLine(Pen pen, Point p1, Point p2) + { + var next = NextNodeAs(); + + if (next == null || !next.Equals(Transform, pen, p1, p2)) + { + Add(new LineNode(Transform, pen, p1, p2)); + } + else + { + ++Index; + } + } + + public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0) + { + var next = NextNodeAs(); + + if (next == null || !next.Equals(Transform, null, pen, rect, cornerRadius)) + { + Add(new RectangleNode(Transform, null, pen, rect, cornerRadius)); + } + else + { + ++Index; + } + } + + public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) + { + var next = NextNodeAs(); + + if (next == null || !next.Equals(Transform, foreground, origin, text)) + { + Add(new TextNode(Transform, foreground, origin, text)); + } + else + { + ++Index; + } + } + + public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0) + { + var next = NextNodeAs(); + + if (next == null || !next.Equals(Transform, brush, null, rect, cornerRadius)) + { + Add(new RectangleNode(Transform, brush, null, rect, cornerRadius)); + } + else + { + ++Index; + } + } + + public void PopClip() + { + // TODO: Implement + } + + public void PopGeometryClip() + { + // TODO: Implement + } + + public void PopOpacity() + { + // TODO: Implement + } + + public void PopOpacityMask() + { + // TODO: Implement + } + + public void PushClip(Rect clip) + { + // TODO: Implement + } + + public void PushGeometryClip(Geometry clip) + { + // TODO: Implement + } + + public void PushOpacity(double opacity) + { + // TODO: Implement + } + + public void PushOpacityMask(IBrush mask, Rect bounds) + { + // TODO: Implement + } + + private void Add(ISceneNode node) + { + var index = Index; + + if (index < Node.Children.Count) + { + Node.Children[index] = node; + } + else + { + Node.Children.Add(node); + } + + ++Index; + } + + private T NextNodeAs() where T : class, ISceneNode + { + return Index < Node.Children.Count ? Node.Children[Index] as T : null; + } + + private void Pop() + { + var frame = _stack.Pop(); + var children = frame.Node.Children; + var index = frame.Index; + + if (children.Count > index) + { + children.RemoveRange(index, children.Count - index); + } + } + + class Frame + { + public Frame(VisualNode node) + { + Node = node; + } + + public VisualNode Node { get; } + public int Index { get; set; } + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs new file mode 100644 index 00000000000..89850f0c0e3 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs @@ -0,0 +1,39 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Media; +using Avalonia.Platform; + +namespace Avalonia.Rendering.SceneGraph +{ + public class GeometryNode : ISceneNode + { + public GeometryNode(Matrix transform, IBrush brush, Pen pen, IGeometryImpl geometry) + { + Transform = transform; + Brush = brush; + Pen = pen; + Geometry = geometry; + } + + public Matrix Transform { get; } + public IBrush Brush { get; } + public Pen Pen { get; } + public IGeometryImpl Geometry { get; } + + public bool Equals(Matrix transform, IBrush brush, Pen pen, IGeometryImpl geometry) + { + return transform == Transform && + Equals(brush, Brush) && + pen == Pen && + Equals(geometry, Geometry); + } + + public void Render(IDrawingContextImpl context) + { + context.Transform = Transform; + context.DrawGeometry(Brush, Pen, Geometry); + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs new file mode 100644 index 00000000000..073d9716d77 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs @@ -0,0 +1,14 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using Avalonia.Media; + +namespace Avalonia.Rendering.SceneGraph +{ + public interface ISceneNode + { + void Render(IDrawingContextImpl context); + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs new file mode 100644 index 00000000000..17fcb7e219c --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs @@ -0,0 +1,15 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using Avalonia.VisualTree; + +namespace Avalonia.Rendering.SceneGraph +{ + public interface IVisualNode : ISceneNode + { + IReadOnlyList Children { get; } + IVisual Visual { get; } + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs new file mode 100644 index 00000000000..f8aef2aed99 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs @@ -0,0 +1,42 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Media; +using Avalonia.Platform; + +namespace Avalonia.Rendering.SceneGraph +{ + public class ImageNode : ISceneNode + { + public ImageNode(Matrix transform, IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect) + { + Transform = transform; + Source = source; + Opacity = opacity; + SourceRect = sourceRect; + DestRect = destRect; + } + + public Matrix Transform { get; } + public IBitmapImpl Source { get; } + public double Opacity { get; } + public Rect SourceRect { get; } + public Rect DestRect { get; } + + public bool Equals(Matrix transform, IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect) + { + return transform == Transform && + Equals(source, Source) && + opacity == Opacity && + sourceRect == SourceRect && + destRect == DestRect; + } + + public void Render(IDrawingContextImpl context) + { + context.Transform = Transform; + context.DrawImage(Source, Opacity, SourceRect, DestRect); + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs new file mode 100644 index 00000000000..9ff0fbde4ea --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs @@ -0,0 +1,35 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Media; + +namespace Avalonia.Rendering.SceneGraph +{ + public class LineNode : ISceneNode + { + public LineNode(Matrix transform, Pen pen, Point p1, Point p2) + { + Transform = transform; + Pen = pen; + P1 = p1; + P2 = p2; + } + + public Matrix Transform { get; } + public Pen Pen { get; } + public Point P1 { get; } + public Point P2 { get; } + + public bool Equals(Matrix transform, Pen pen, Point p1, Point p2) + { + return transform == Transform && pen == Pen && p1 == P1 && p2 == P2; + } + + public void Render(IDrawingContextImpl context) + { + context.Transform = Transform; + context.DrawLine(Pen, P1, P2); + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs new file mode 100644 index 00000000000..738cd189adc --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs @@ -0,0 +1,50 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Media; + +namespace Avalonia.Rendering.SceneGraph +{ + public class RectangleNode : ISceneNode + { + public RectangleNode(Matrix transform, IBrush brush, Pen pen, Rect rect, float cornerRadius) + { + Transform = transform; + Brush = brush; + Pen = pen; + Rect = rect; + CornerRadius = cornerRadius; + } + + public Matrix Transform { get; } + public IBrush Brush { get; } + public Pen Pen { get; } + public Rect Rect { get; } + public float CornerRadius { get; } + + public bool Equals(Matrix transform, IBrush brush, Pen pen, Rect rect, float cornerRadius) + { + return transform == Transform && + Equals(brush, Brush) && + pen == Pen && + rect == Rect && + cornerRadius == CornerRadius; + } + + public void Render(IDrawingContextImpl context) + { + context.Transform = Transform; + + if (Brush != null) + { + context.FillRectangle(Brush, Rect, CornerRadius); + } + + if (Pen != null) + { + context.DrawRectangle(Pen, Rect, CornerRadius); + } + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs new file mode 100644 index 00000000000..f0212d23c3d --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs @@ -0,0 +1,72 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using Avalonia.VisualTree; + +namespace Avalonia.Rendering.SceneGraph +{ + public class Scene + { + private Dictionary _index; + + public Scene(IVisual rootVisual) + : this(new VisualNode(rootVisual), new Dictionary()) + { + } + + internal Scene(VisualNode root, Dictionary index) + { + Contract.Requires(root != null); + + _index = index; + Root = root; + } + + public IVisualNode Root { get; } + + public void Add(IVisualNode node) + { + _index.Add(node.Visual, node); + } + + public IVisualNode FindNode(IVisual visual) + { + IVisualNode node; + _index.TryGetValue(visual, out node); + return node; + } + + public Scene Clone() + { + var index = new Dictionary(); + var root = (VisualNode)Clone((VisualNode)Root, null, index); + var result = new Scene(root, index); + return result; + } + + private VisualNode Clone(VisualNode source, ISceneNode parent, Dictionary index) + { + var result = source.Clone(); + + index.Add(result.Visual, result); + + foreach (var child in source.Children) + { + var visualNode = child as VisualNode; + + if (visualNode != null) + { + result.Children.Add(Clone(visualNode, result, index)); + } + else + { + result.Children.Add(child); + } + } + + return result; + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs new file mode 100644 index 00000000000..b3ac55fe6ab --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -0,0 +1,90 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Media; +using Avalonia.Threading; +using Avalonia.VisualTree; + +namespace Avalonia.Rendering.SceneGraph +{ + public static class SceneBuilder + { + public static Scene Update(Scene scene) + { + Dispatcher.UIThread.VerifyAccess(); + + scene = scene.Clone(); + + using (var impl = new DeferredDrawingContextImpl()) + using (var context = new DrawingContext(impl)) + { + Update(context, scene, scene.Root.Visual, null); + } + + return scene; + } + + private static void Update(DrawingContext context, Scene scene, IVisual visual, VisualNode parent) + { + var opacity = visual.Opacity; + var clipToBounds = visual.ClipToBounds; + var bounds = new Rect(visual.Bounds.Size); + var node = (VisualNode)scene.FindNode(visual) ?? CreateNode(visual, scene, parent); + var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl; + + contextImpl.AddChild(node); + + if (visual.IsVisible && opacity > 0) + { + var m = Matrix.CreateTranslation(visual.Bounds.Position); + + var renderTransform = Matrix.Identity; + + if (visual.RenderTransform != null) + { + var origin = visual.RenderTransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); + var offset = Matrix.CreateTranslation(origin); + renderTransform = (-offset) * visual.RenderTransform.Value * (offset); + } + + m = renderTransform * m; + + using (contextImpl.Begin(node)) + using (context.PushPostTransform(m)) + using (context.PushTransformContainer()) + { + node.Transform = contextImpl.Transform; + node.Bounds = bounds; + node.ClipToBounds = clipToBounds; + node.GeometryClip = visual.Clip; + node.Opacity = opacity; + node.OpacityMask = visual.OpacityMask; + + visual.Render(context); + +#pragma warning disable 0618 + var transformed = new TransformedBounds(bounds, new Rect(), context.CurrentContainerTransform); +#pragma warning restore 0618 + + if (visual is Visual) + { + BoundsTracker.SetTransformedBounds((Visual)visual, transformed); + } + + foreach (var child in visual.VisualChildren) + { + Update(context, scene, child, node); + } + } + } + } + + private static VisualNode CreateNode(IVisual visual, Scene scene, VisualNode parent) + { + var node = new VisualNode(visual); + scene.Add(node); + return node; + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs new file mode 100644 index 00000000000..3718f16af41 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs @@ -0,0 +1,39 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Media; +using Avalonia.Platform; + +namespace Avalonia.Rendering.SceneGraph +{ + public class TextNode : ISceneNode + { + public TextNode(Matrix transform, IBrush foreground, Point origin, IFormattedTextImpl text) + { + Transform = transform; + Foreground = foreground; + Origin = origin; + Text = text; + } + + public Matrix Transform { get; } + public IBrush Foreground { get; } + public Point Origin { get; } + public IFormattedTextImpl Text { get; } + + public void Render(IDrawingContextImpl context) + { + context.Transform = Transform; + context.DrawText(Foreground, Origin, Text); + } + + internal bool Equals(Matrix transform, IBrush foreground, Point origin, IFormattedTextImpl text) + { + return transform == Transform && + Equals(foreground, Foreground) && + origin == Origin && + Equals(text, Text); + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs new file mode 100644 index 00000000000..3f4cea5c30d --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -0,0 +1,65 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using Avalonia.Media; +using Avalonia.VisualTree; + +namespace Avalonia.Rendering.SceneGraph +{ + public class VisualNode : IVisualNode + { + public VisualNode(IVisual visual) + { + Children = new List(); + Visual = visual; + } + + public IVisual Visual { get; } + public Matrix Transform { get; set; } + public Rect Bounds { get; set; } + public bool ClipToBounds { get; set; } + public Geometry GeometryClip { get; set; } + public double Opacity { get; set; } + public IBrush OpacityMask { get; set; } + public List Children { get; } + + IReadOnlyList IVisualNode.Children => Children; + + public VisualNode Clone() + { + return new VisualNode(Visual); + } + + public void Render(IDrawingContextImpl context) + { + context.Transform = Transform; + + if (Opacity != 1) + { + context.PushOpacity(Opacity); + } + + if (ClipToBounds) + { + context.PushClip(Bounds); + } + + foreach (var child in Children) + { + child.Render(context); + } + + if (ClipToBounds) + { + context.PopClip(); + } + + if (Opacity != 1) + { + context.PopOpacity(); + } + } + } +} diff --git a/src/Gtk/Avalonia.Cairo/Avalonia.Cairo.v2.ncrunchproject b/src/Gtk/Avalonia.Cairo/Avalonia.Cairo.v2.ncrunchproject index 30815b1937d..1f00ab7c749 100644 --- a/src/Gtk/Avalonia.Cairo/Avalonia.Cairo.v2.ncrunchproject +++ b/src/Gtk/Avalonia.Cairo/Avalonia.Cairo.v2.ncrunchproject @@ -17,9 +17,9 @@ true true 60000 - - - + + + AutoDetect STA x86 diff --git a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj index 9f9558ff76c..eeae2bff1cd 100644 --- a/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj +++ b/src/Windows/Avalonia.Direct2D1/Avalonia.Direct2D1.csproj @@ -64,7 +64,7 @@ - + diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index b43eef2fa9f..2b465619ef5 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -29,9 +29,9 @@ public class Direct2D1Platform : IPlatformRenderInterface, IRendererFactory private static readonly SharpDX.Direct2D1.Factory s_d2D1Factory = #if DEBUG - new SharpDX.Direct2D1.Factory(SharpDX.Direct2D1.FactoryType.SingleThreaded, SharpDX.Direct2D1.DebugLevel.Error); + new SharpDX.Direct2D1.Factory(SharpDX.Direct2D1.FactoryType.MultiThreaded, SharpDX.Direct2D1.DebugLevel.Error); #else - new SharpDX.Direct2D1.Factory(SharpDX.Direct2D1.FactoryType.SingleThreaded, SharpDX.Direct2D1.DebugLevel.None); + new SharpDX.Direct2D1.Factory(SharpDX.Direct2D1.FactoryType.MultiThreaded, SharpDX.Direct2D1.DebugLevel.None); #endif private static readonly SharpDX.DirectWrite.Factory s_dwfactory = new SharpDX.DirectWrite.Factory(); @@ -63,7 +63,7 @@ public IFormattedTextImpl CreateFormattedText( public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop) { - return new Renderer(root, renderLoop); + return new DeferredRenderer(root, renderLoop); } public IRenderTarget CreateRenderTarget(IPlatformHandle handle) diff --git a/src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs b/src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs index a1b9f857830..51b4eef62d5 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs @@ -11,14 +11,14 @@ namespace Avalonia.Direct2D1.Media { internal class AvaloniaTextRenderer : TextRenderer { - private readonly DrawingContext _context; + private readonly DrawingContextImpl _context; private readonly SharpDX.Direct2D1.RenderTarget _renderTarget; private readonly Brush _foreground; public AvaloniaTextRenderer( - DrawingContext context, + DrawingContextImpl context, SharpDX.Direct2D1.RenderTarget target, Brush foreground) { diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContext.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs similarity index 95% rename from src/Windows/Avalonia.Direct2D1/Media/DrawingContext.cs rename to src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 75a0f43d9fa..2747ad7ca06 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContext.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -5,6 +5,7 @@ using System.Collections; using System.Collections.Generic; using Avalonia.Media; +using Avalonia.Platform; using SharpDX; using SharpDX.Direct2D1; using SharpDX.Mathematics.Interop; @@ -15,7 +16,7 @@ namespace Avalonia.Direct2D1.Media /// /// Draws using Direct2D1. /// - public class DrawingContext : IDrawingContextImpl, IDisposable + public class DrawingContextImpl : IDrawingContextImpl, IDisposable { /// /// The Direct2D1 render target. @@ -28,11 +29,11 @@ public class DrawingContext : IDrawingContextImpl, IDisposable private SharpDX.DirectWrite.Factory _directWriteFactory; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The render target to draw to. /// The DirectWrite factory. - public DrawingContext( + public DrawingContextImpl( SharpDX.Direct2D1.RenderTarget renderTarget, SharpDX.DirectWrite.Factory directWriteFactory) { @@ -74,9 +75,9 @@ public void Dispose() /// The opacity to draw with. /// The rect in the image to draw. /// The rect in the output to draw to. - public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect) + public void DrawImage(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect) { - BitmapImpl impl = (BitmapImpl)source.PlatformImpl; + BitmapImpl impl = (BitmapImpl)source; Bitmap d2d = impl.GetDirect2DBitmap(_renderTarget); _renderTarget.DrawBitmap( d2d, @@ -120,7 +121,7 @@ public void DrawLine(Pen pen, Point p1, Point p2) /// The fill brush. /// The stroke pen. /// The geometry. - public void DrawGeometry(IBrush brush, Pen pen, Avalonia.Media.Geometry geometry) + public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry) { if (brush != null) { @@ -128,7 +129,7 @@ public void DrawGeometry(IBrush brush, Pen pen, Avalonia.Media.Geometry geometry { if (d2dBrush.PlatformBrush != null) { - var impl = (GeometryImpl)geometry.PlatformImpl; + var impl = (GeometryImpl)geometry; _renderTarget.FillGeometry(impl.Geometry, d2dBrush.PlatformBrush); } } @@ -141,7 +142,7 @@ public void DrawGeometry(IBrush brush, Pen pen, Avalonia.Media.Geometry geometry { if (d2dBrush.PlatformBrush != null) { - var impl = (GeometryImpl)geometry.PlatformImpl; + var impl = (GeometryImpl)geometry; _renderTarget.DrawGeometry(impl.Geometry, d2dBrush.PlatformBrush, (float)pen.Thickness, d2dStroke); } } @@ -187,11 +188,11 @@ public void DrawRectangle(Pen pen, Rect rect, float cornerRadius) /// The foreground brush. /// The upper-left corner of the text. /// The text. - public void DrawText(IBrush foreground, Point origin, FormattedText text) + public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) { if (!string.IsNullOrEmpty(text.Text)) { - var impl = (FormattedTextImpl)text.PlatformImpl; + var impl = (FormattedTextImpl)text; using (var brush = CreateBrush(foreground, impl.Measure())) using (var renderer = new AvaloniaTextRenderer(this, _renderTarget, brush.PlatformBrush)) diff --git a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs index 45d06b5e279..b8e295e8ab2 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs @@ -23,6 +23,8 @@ public FormattedTextImpl( { var factory = AvaloniaLocator.Current.GetService(); + Text = text; + using (var format = new DWrite.TextFormat( factory, fontFamily, @@ -58,6 +60,8 @@ public Size Constraint } } + public string Text { get; } + public DWrite.TextLayout TextLayout { get; } public void Dispose() diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs index eff832407ee..219693ca237 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs @@ -39,7 +39,7 @@ public override void Dispose() base.Dispose(); } - public Avalonia.Media.DrawingContext CreateDrawingContext() => new RenderTarget(_target).CreateDrawingContext(); + public IDrawingContextImpl CreateDrawingContext() => new RenderTarget(_target).CreateDrawingContext(); } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/TileBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TileBrushImpl.cs index dd1aac37765..87f886008c3 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TileBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TileBrushImpl.cs @@ -23,7 +23,7 @@ public TileBrushImpl( using (var ctx = new RenderTarget(intermediate).CreateDrawingContext()) { intermediate.Clear(null); - helper.DrawIntermediate(ctx); + helper.DrawIntermediate(new DrawingContext(ctx)); } PlatformBrush = new BitmapBrush( diff --git a/src/Windows/Avalonia.Direct2D1/RenderTarget.cs b/src/Windows/Avalonia.Direct2D1/RenderTarget.cs index 180e1a7472d..32dd4ac794e 100644 --- a/src/Windows/Avalonia.Direct2D1/RenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/RenderTarget.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Avalonia.Direct2D1.Media; +using Avalonia.Media; using Avalonia.Platform; using Avalonia.Win32.Interop; using SharpDX; @@ -80,7 +82,7 @@ public DwFactory DirectWriteFactory /// Creates a drawing context for a rendering session. /// /// An . - public DrawingContext CreateDrawingContext() + public IDrawingContextImpl CreateDrawingContext() { var window = _renderTarget as WindowRenderTarget; @@ -100,7 +102,7 @@ public DrawingContext CreateDrawingContext() } } - return new DrawingContext(new Media.DrawingContext(_renderTarget, DirectWriteFactory)); + return new DrawingContextImpl(_renderTarget, DirectWriteFactory); } public void Dispose() diff --git a/tests/Avalonia.RenderTests/Avalonia.Direct2D1.RenderTests.v2.ncrunchproject b/tests/Avalonia.RenderTests/Avalonia.Direct2D1.RenderTests.v2.ncrunchproject index 30815b1937d..3f48b966ba0 100644 --- a/tests/Avalonia.RenderTests/Avalonia.Direct2D1.RenderTests.v2.ncrunchproject +++ b/tests/Avalonia.RenderTests/Avalonia.Direct2D1.RenderTests.v2.ncrunchproject @@ -17,10 +17,13 @@ true true 60000 - - - + + + AutoDetect STA x86 + + + \ No newline at end of file diff --git a/tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.csproj b/tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.csproj index 9e7f739a1c0..c885914279c 100644 --- a/tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.csproj +++ b/tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.csproj @@ -81,6 +81,7 @@ + diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs new file mode 100644 index 00000000000..1ccd8655bce --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs @@ -0,0 +1,130 @@ +using System; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Rendering.SceneGraph; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph +{ + public class SceneBuilderTests + { + [Fact] + public void Should_Build_Initial_Scene() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + Border border; + TextBlock textBlock; + var tree = new TestRoot + { + Child = border = new Border + { + Background = Brushes.Red, + Child = textBlock = new TextBlock + { + Text = "Hello World", + } + } + }; + + var initial = new Scene(tree); + var result = SceneBuilder.Update(initial); + + Assert.NotSame(initial, result); + Assert.Equal(1, result.Root.Children.Count); + + var borderNode = (VisualNode)result.Root.Children[0]; + Assert.Same(borderNode, result.FindNode(border)); + Assert.Same(border, borderNode.Visual); + Assert.Equal(2, borderNode.Children.Count); + + var backgroundNode = (RectangleNode)borderNode.Children[0]; + Assert.Equal(Brushes.Red, backgroundNode.Brush); + + var textBlockNode = (VisualNode)borderNode.Children[1]; + Assert.Same(textBlockNode, result.FindNode(textBlock)); + Assert.Same(textBlock, textBlockNode.Visual); + Assert.Equal(1, textBlockNode.Children.Count); + + var textNode = (TextNode)textBlockNode.Children[0]; + Assert.NotNull(textNode.Text); + } + } + + [Fact] + public void Should_Update_Border_Background_Node() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + Border border; + TextBlock textBlock; + var tree = new TestRoot + { + Child = border = new Border + { + Background = Brushes.Red, + Child = textBlock = new TextBlock + { + Text = "Hello World", + } + } + }; + + var initial = SceneBuilder.Update(new Scene(tree)); + var initialBackgroundNode = initial.FindNode(border).Children[0]; + var initialTextNode = initial.FindNode(textBlock).Children[0]; + + Assert.NotNull(initialBackgroundNode); + Assert.NotNull(initialTextNode); + + border.Background = Brushes.Green; + var result = SceneBuilder.Update(initial); + + Assert.NotSame(initial, result); + var borderNode = (VisualNode)result.Root.Children[0]; + Assert.Same(border, borderNode.Visual); + + var backgroundNode = (RectangleNode)borderNode.Children[0]; + Assert.NotSame(initialBackgroundNode, backgroundNode); + Assert.Equal(Brushes.Green, backgroundNode.Brush); + + var textBlockNode = (VisualNode)borderNode.Children[1]; + Assert.Same(textBlock, textBlockNode.Visual); + + var textNode = (TextNode)textBlockNode.Children[0]; + Assert.Same(initialTextNode, textNode); + } + } + + [Fact] + public void Should_Update_When_Control_Removed() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + Border border; + TextBlock textBlock; + var tree = new TestRoot + { + Child = border = new Border + { + Background = Brushes.Red, + Child = textBlock = new TextBlock + { + Text = "Hello World", + } + } + }; + + var initial = SceneBuilder.Update(new Scene(tree)); + + border.Child = null; + var result = SceneBuilder.Update(initial); + + Assert.NotSame(initial, result); + var borderNode = (VisualNode)result.Root.Children[0]; + Assert.Equal(1, borderNode.Children.Count); + } + } + } +} diff --git a/tests/Avalonia.Visuals.UnitTests/TestRoot.cs b/tests/Avalonia.Visuals.UnitTests/TestRoot.cs index bdcbae59ce3..b291afb6d3c 100644 --- a/tests/Avalonia.Visuals.UnitTests/TestRoot.cs +++ b/tests/Avalonia.Visuals.UnitTests/TestRoot.cs @@ -23,7 +23,7 @@ public void Invalidate(Rect rect) public IRenderer Renderer { - get { throw new NotImplementedException(); } + get { return AvaloniaLocator.Current.GetService(); } } public Point PointToClient(Point p) From 8aae51513007603fc109d7db1807c62edb3bcd3d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 6 Nov 2016 17:43:34 +0100 Subject: [PATCH 002/175] Removed duplicate TestRoot. Use the one in Avalonia.UnitTests. --- tests/Avalonia.Visuals.UnitTests/TestRoot.cs | 39 ------------------- .../Avalonia.Visuals.UnitTests/VisualTests.cs | 15 +++---- 2 files changed, 8 insertions(+), 46 deletions(-) delete mode 100644 tests/Avalonia.Visuals.UnitTests/TestRoot.cs diff --git a/tests/Avalonia.Visuals.UnitTests/TestRoot.cs b/tests/Avalonia.Visuals.UnitTests/TestRoot.cs deleted file mode 100644 index bdcbae59ce3..00000000000 --- a/tests/Avalonia.Visuals.UnitTests/TestRoot.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using Avalonia.Platform; -using Avalonia.Rendering; - -namespace Avalonia.Visuals.UnitTests -{ - public class TestRoot : TestVisual, IRenderRoot - { - public Size ClientSize { get; } - - public IRenderTarget CreateRenderTarget() - { - throw new NotImplementedException(); - } - - public void Invalidate(Rect rect) - { - throw new NotImplementedException(); - } - - public IRenderer Renderer - { - get { throw new NotImplementedException(); } - } - - public Point PointToClient(Point p) - { - throw new NotImplementedException(); - } - - public Point PointToScreen(Point p) - { - throw new NotImplementedException(); - } - } -} diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTests.cs b/tests/Avalonia.Visuals.UnitTests/VisualTests.cs index 8daaacd0511..9cec9e434b2 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTests.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Controls; +using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; @@ -66,8 +67,8 @@ public void Clearing_Children_Should_Clear_VisualParent() [Fact] public void Adding_Children_Should_Fire_OnAttachedToVisualTree() { - var child2 = new TestVisual(); - var child1 = new TestVisual { Child = child2 }; + var child2 = new Decorator(); + var child1 = new Decorator { Child = child2 }; var root = new TestRoot(); var called1 = false; var called2 = false; @@ -84,8 +85,8 @@ public void Adding_Children_Should_Fire_OnAttachedToVisualTree() [Fact] public void Removing_Children_Should_Fire_OnDetachedFromVisualTree() { - var child2 = new TestVisual(); - var child1 = new TestVisual { Child = child2 }; + var child2 = new Decorator(); + var child1 = new Decorator { Child = child2 }; var root = new TestRoot(); var called1 = false; var called2 = false; @@ -104,11 +105,11 @@ public void Adding_Already_Parented_Control_Should_Throw() { var root1 = new TestRoot(); var root2 = new TestRoot(); - var child = new TestVisual(); + var child = new Canvas(); - root1.AddChild(child); + root1.Child = child; - Assert.Throws(() => root2.AddChild(child)); + Assert.Throws(() => root2.Child = child); Assert.Equal(0, root2.GetVisualChildren().Count()); } } From d91d1829ac00a7a77e132bbe5f5905dbf25eade5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 6 Nov 2016 18:34:26 +0100 Subject: [PATCH 003/175] Initial implementation of scenegraph hit testing. Based solely on control bounds as before. --- .../RenderTest/RenderTest.v2.ncrunchproject | 7 +- src/Avalonia.Visuals/Avalonia.Visuals.csproj | 1 + .../Rendering/DeferredRenderer.cs | 12 + src/Avalonia.Visuals/Rendering/IRenderer.cs | 2 + src/Avalonia.Visuals/Rendering/Renderer.cs | 6 + .../Rendering/SceneGraph/GeometryNode.cs | 5 + .../Rendering/SceneGraph/ISceneNode.cs | 3 +- .../Rendering/SceneGraph/IVisualNode.cs | 4 +- .../Rendering/SceneGraph/ImageNode.cs | 5 + .../Rendering/SceneGraph/LineNode.cs | 5 + .../Rendering/SceneGraph/RectangleNode.cs | 5 + .../Rendering/SceneGraph/Scene.cs | 48 +- .../Rendering/SceneGraph/SceneBuilder.cs | 14 +- .../Rendering/SceneGraph/TextNode.cs | 5 + .../Rendering/SceneGraph/VisualNode.cs | 9 +- .../Rendering/ZIndexComparer.cs | 13 + .../VisualTree/VisualExtensions.cs | 28 +- .../Avalonia.Input.UnitTests.csproj | 1 - .../InputElement_HitTesting.cs | 484 ------------------ tests/Avalonia.LeakTests/ControlTests.cs | 14 - ...alonia.Cairo.RenderTests.v2.ncrunchproject | 8 +- tests/Avalonia.UnitTests/TestRoot.cs | 10 +- tests/Avalonia.UnitTests/TestServices.cs | 11 +- .../Avalonia.UnitTests/UnitTestApplication.cs | 17 +- ...alonia.Visuals.UnitTests.v2.ncrunchproject | 7 +- .../Rendering/SceneGraph/SceneBuilderTests.cs | 66 +++ .../VisualExtensionsTests_GetVisualsAt.cs | 250 +++++---- 27 files changed, 371 insertions(+), 669 deletions(-) create mode 100644 src/Avalonia.Visuals/Rendering/ZIndexComparer.cs delete mode 100644 tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs diff --git a/samples/RenderTest/RenderTest.v2.ncrunchproject b/samples/RenderTest/RenderTest.v2.ncrunchproject index 30815b1937d..8e69ee9b4a9 100644 --- a/samples/RenderTest/RenderTest.v2.ncrunchproject +++ b/samples/RenderTest/RenderTest.v2.ncrunchproject @@ -17,10 +17,11 @@ true true 60000 - - - + + + AutoDetect STA x86 + MissingOrIgnoredProjectReference \ No newline at end of file diff --git a/src/Avalonia.Visuals/Avalonia.Visuals.csproj b/src/Avalonia.Visuals/Avalonia.Visuals.csproj index aeffde81edc..69caba17380 100644 --- a/src/Avalonia.Visuals/Avalonia.Visuals.csproj +++ b/src/Avalonia.Visuals/Avalonia.Visuals.csproj @@ -124,6 +124,7 @@ + diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 37215442345..d9fd8e50122 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -6,6 +6,7 @@ using Avalonia.Rendering.SceneGraph; using Avalonia.Threading; using Avalonia.VisualTree; +using System.Collections.Generic; namespace Avalonia.Rendering { @@ -30,6 +31,7 @@ public DeferredRenderer(IRenderRoot root, IRenderLoop renderLoop) _root = root; _scene = new Scene(root); + _needsUpdate = true; _renderLoop = renderLoop; _renderLoop.Tick += OnRenderLoopTick; } @@ -50,6 +52,16 @@ public void Dispose() _renderLoop.Tick -= OnRenderLoopTick; } + public IEnumerable HitTest(Point p, Func filter) + { + if (_needsUpdate) + { + UpdateScene(); + } + + return _scene.HitTest(p, filter); + } + public void Render(Rect rect) { if (_renderTarget == null) diff --git a/src/Avalonia.Visuals/Rendering/IRenderer.cs b/src/Avalonia.Visuals/Rendering/IRenderer.cs index 119bb4c8d1f..a297e82203f 100644 --- a/src/Avalonia.Visuals/Rendering/IRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/IRenderer.cs @@ -3,6 +3,7 @@ using System; using Avalonia.VisualTree; +using System.Collections.Generic; namespace Avalonia.Rendering { @@ -11,6 +12,7 @@ public interface IRenderer : IDisposable bool DrawFps { get; set; } void AddDirty(IVisual visual); + IEnumerable HitTest(Point p, Func filter); void Render(Rect rect); } } \ No newline at end of file diff --git a/src/Avalonia.Visuals/Rendering/Renderer.cs b/src/Avalonia.Visuals/Rendering/Renderer.cs index 3b2fe19be40..504fd403960 100644 --- a/src/Avalonia.Visuals/Rendering/Renderer.cs +++ b/src/Avalonia.Visuals/Rendering/Renderer.cs @@ -4,6 +4,7 @@ using System; using Avalonia.Platform; using Avalonia.VisualTree; +using System.Collections.Generic; namespace Avalonia.Rendering { @@ -35,6 +36,11 @@ public void Dispose() _renderLoop.Tick -= OnRenderLoopTick; } + public IEnumerable HitTest(Point p, Func filter) + { + throw new NotImplementedException(); + } + public void Render(Rect rect) { if (_renderTarget == null) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs index 89850f0c0e3..db05533d4a3 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs @@ -35,5 +35,10 @@ public void Render(IDrawingContextImpl context) context.Transform = Transform; context.DrawGeometry(Brush, Pen, Geometry); } + + public bool HitTest(Point p) + { + throw new NotImplementedException(); + } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs index 073d9716d77..e022970c1e0 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs @@ -2,13 +2,14 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; using Avalonia.Media; namespace Avalonia.Rendering.SceneGraph { public interface ISceneNode { + bool HitTest(Point p); + void Render(IDrawingContextImpl context); } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs index 17fcb7e219c..ddaf3a4e9ca 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs @@ -9,7 +9,9 @@ namespace Avalonia.Rendering.SceneGraph { public interface IVisualNode : ISceneNode { - IReadOnlyList Children { get; } IVisual Visual { get; } + Rect ClipBounds { get; set; } + bool ClipToBounds { get; set; } + IReadOnlyList Children { get; } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs index f8aef2aed99..6d3e5c6a565 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs @@ -38,5 +38,10 @@ public void Render(IDrawingContextImpl context) context.Transform = Transform; context.DrawImage(Source, Opacity, SourceRect, DestRect); } + + public bool HitTest(Point p) + { + throw new NotImplementedException(); + } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs index 9ff0fbde4ea..4b30e94b7db 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs @@ -31,5 +31,10 @@ public void Render(IDrawingContextImpl context) context.Transform = Transform; context.DrawLine(Pen, P1, P2); } + + public bool HitTest(Point p) + { + throw new NotImplementedException(); + } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs index 738cd189adc..d4fed636a1b 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs @@ -46,5 +46,10 @@ public void Render(IDrawingContextImpl context) context.DrawRectangle(Pen, Rect, CornerRadius); } } + + public bool HitTest(Point p) + { + throw new NotImplementedException(); + } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs index f0212d23c3d..77c1a45c905 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs @@ -31,6 +31,14 @@ public void Add(IVisualNode node) _index.Add(node.Visual, node); } + public Scene Clone() + { + var index = new Dictionary(); + var root = (VisualNode)Clone((VisualNode)Root, null, index); + var result = new Scene(root, index); + return result; + } + public IVisualNode FindNode(IVisual visual) { IVisualNode node; @@ -38,12 +46,9 @@ public IVisualNode FindNode(IVisual visual) return node; } - public Scene Clone() + public IEnumerable HitTest(Point p, Func filter) { - var index = new Dictionary(); - var root = (VisualNode)Clone((VisualNode)Root, null, index); - var result = new Scene(root, index); - return result; + return HitTest(Root, p, null, filter); } private VisualNode Clone(VisualNode source, ISceneNode parent, Dictionary index) @@ -68,5 +73,38 @@ private VisualNode Clone(VisualNode source, ISceneNode parent, Dictionary HitTest(IVisualNode node, Point p, Rect? clip, Func filter) + { + if (filter?.Invoke(node.Visual) != false) + { + if (node.ClipToBounds) + { + // TODO: Handle geometry clip. + clip = clip == null ? node.ClipBounds : clip.Value.Intersect(node.ClipBounds); + } + + if (!clip.HasValue || clip.Value.Contains(p)) + { + for (var i = node.Children.Count - 1; i >= 0; --i) + { + var visualChild = node.Children[i] as IVisualNode; + + if (visualChild != null) + { + foreach (var h in HitTest(visualChild, p, clip, filter)) + { + yield return h; + } + } + } + + if (node.HitTest(p)) + { + yield return node.Visual; + } + } + } + } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index b3ac55fe6ab..4749c999259 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Linq; using Avalonia.Media; using Avalonia.Threading; using Avalonia.VisualTree; @@ -55,7 +56,7 @@ private static void Update(DrawingContext context, Scene scene, IVisual visual, using (context.PushTransformContainer()) { node.Transform = contextImpl.Transform; - node.Bounds = bounds; + node.ClipBounds = bounds * node.Transform; node.ClipToBounds = clipToBounds; node.GeometryClip = visual.Clip; node.Opacity = opacity; @@ -63,16 +64,7 @@ private static void Update(DrawingContext context, Scene scene, IVisual visual, visual.Render(context); -#pragma warning disable 0618 - var transformed = new TransformedBounds(bounds, new Rect(), context.CurrentContainerTransform); -#pragma warning restore 0618 - - if (visual is Visual) - { - BoundsTracker.SetTransformedBounds((Visual)visual, transformed); - } - - foreach (var child in visual.VisualChildren) + foreach (var child in visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance)) { Update(context, scene, child, node); } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs index 3718f16af41..f9bc16c807f 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs @@ -35,5 +35,10 @@ internal bool Equals(Matrix transform, IBrush foreground, Point origin, IFormatt origin == Origin && Equals(text, Text); } + + public bool HitTest(Point p) + { + throw new NotImplementedException(); + } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 3f4cea5c30d..1482d7d468e 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -18,7 +18,7 @@ public VisualNode(IVisual visual) public IVisual Visual { get; } public Matrix Transform { get; set; } - public Rect Bounds { get; set; } + public Rect ClipBounds { get; set; } public bool ClipToBounds { get; set; } public Geometry GeometryClip { get; set; } public double Opacity { get; set; } @@ -32,6 +32,11 @@ public VisualNode Clone() return new VisualNode(Visual); } + public bool HitTest(Point p) + { + return ClipBounds.Contains(p); + } + public void Render(IDrawingContextImpl context) { context.Transform = Transform; @@ -43,7 +48,7 @@ public void Render(IDrawingContextImpl context) if (ClipToBounds) { - context.PushClip(Bounds); + context.PushClip(ClipBounds); } foreach (var child in Children) diff --git a/src/Avalonia.Visuals/Rendering/ZIndexComparer.cs b/src/Avalonia.Visuals/Rendering/ZIndexComparer.cs new file mode 100644 index 00000000000..b9c43bcbc36 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/ZIndexComparer.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using Avalonia.VisualTree; + +namespace Avalonia.Rendering +{ + public class ZIndexComparer : IComparer + { + public static readonly ZIndexComparer Instance = new ZIndexComparer(); + + public int Compare(IVisual x, IVisual y) => x.ZIndex.CompareTo(y.ZIndex); + } +} diff --git a/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs b/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs index e7876b762f3..6eaef3363d3 100644 --- a/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs +++ b/src/Avalonia.Visuals/VisualTree/VisualExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Rendering; using System; using System.Collections.Generic; using System.Linq; @@ -102,26 +103,9 @@ public static IEnumerable GetVisualsAt( { Contract.Requires(visual != null); - if (filter?.Invoke(visual) != false) - { - bool containsPoint = BoundsTracker.GetTransformedBounds((Visual)visual)?.Contains(p) == true; - - if ((containsPoint || !visual.ClipToBounds) && visual.VisualChildren.Any()) - { - foreach (var child in visual.VisualChildren.SortByZIndex()) - { - foreach (var result in child.GetVisualsAt(p, filter)) - { - yield return result; - } - } - } - - if (containsPoint) - { - yield return visual; - } - } + var root = visual.GetVisualRoot(); + p = visual.TranslatePoint(p, root); + return root.Renderer.HitTest(p, filter); } /// @@ -197,11 +181,11 @@ public static T GetVisualParent(this IVisual visual) where T : class /// /// The root visual or null if the visual is not rooted. /// - public static IVisual GetVisualRoot(this IVisual visual) + public static IRenderRoot GetVisualRoot(this IVisual visual) { Contract.Requires(visual != null); - return visual.VisualRoot as IVisual; + return visual as IRenderRoot ?? visual.VisualRoot; } /// diff --git a/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj b/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj index 5ed05e9e338..26c73ad00a0 100644 --- a/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj +++ b/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj @@ -86,7 +86,6 @@ - diff --git a/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs b/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs deleted file mode 100644 index e4e7551f5c9..00000000000 --- a/tests/Avalonia.Input.UnitTests/InputElement_HitTesting.cs +++ /dev/null @@ -1,484 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using Avalonia.Controls; -using Avalonia.Controls.Presenters; -using Avalonia.Layout; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.Rendering; -using Avalonia.UnitTests; -using Moq; -using System; -using System.Collections.Generic; -using System.IO; -using Xunit; - -namespace Avalonia.Input.UnitTests -{ - public class InputElement_HitTesting - { - [Fact] - public void InputHitTest_Should_Find_Control_At_Point() - { - using (var application = UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) - { - var container = new Decorator - { - Width = 200, - Height = 200, - Child = new Border - { - Width = 100, - Height = 100, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - } - }; - - container.Measure(Size.Infinity); - container.Arrange(new Rect(container.DesiredSize)); - - var context = new DrawingContext(Mock.Of()); - context.Render(container); - - var result = container.InputHitTest(new Point(100, 100)); - - Assert.Equal(container.Child, result); - } - } - - [Fact] - public void InputHitTest_Should_Not_Find_Control_Outside_Point() - { - using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) - { - var container = new Decorator - { - Width = 200, - Height = 200, - Child = new Border - { - Width = 100, - Height = 100, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - } - }; - - container.Measure(Size.Infinity); - container.Arrange(new Rect(container.DesiredSize)); - - var context = new DrawingContext(Mock.Of()); - context.Render(container); - - var result = container.InputHitTest(new Point(10, 10)); - - Assert.Equal(container, result); - } - } - - [Fact] - public void InputHitTest_Should_Find_Top_Control_At_Point() - { - using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) - { - var container = new Panel - { - Width = 200, - Height = 200, - Children = new Controls.Controls - { - new Border - { - Width = 100, - Height = 100, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - }, - new Border - { - Width = 50, - Height = 50, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - } - } - }; - - container.Measure(Size.Infinity); - container.Arrange(new Rect(container.DesiredSize)); - - var context = new DrawingContext(Mock.Of()); - context.Render(container); - - var result = container.InputHitTest(new Point(100, 100)); - - Assert.Equal(container.Children[1], result); - } - } - - [Fact] - public void InputHitTest_Should_Find_Top_Control_At_Point_With_ZOrder() - { - using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) - { - var container = new Panel - { - Width = 200, - Height = 200, - Children = new Controls.Controls - { - new Border - { - Width = 100, - Height = 100, - ZIndex = 1, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - }, - new Border - { - Width = 50, - Height = 50, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - } - } - }; - - container.Measure(Size.Infinity); - container.Arrange(new Rect(container.DesiredSize)); - - var context = new DrawingContext(Mock.Of()); - context.Render(container); - - var result = container.InputHitTest(new Point(100, 100)); - - Assert.Equal(container.Children[0], result); - } - } - - [Fact] - public void InputHitTest_Should_Find_Control_Translated_Outside_Parent_Bounds() - { - using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) - { - Border target; - var container = new Panel - { - Width = 200, - Height = 200, - ClipToBounds = false, - Children = new Controls.Controls - { - new Border - { - Width = 100, - Height = 100, - ZIndex = 1, - HorizontalAlignment = HorizontalAlignment.Left, - VerticalAlignment = VerticalAlignment.Top, - Child = target = new Border - { - Width = 50, - Height = 50, - HorizontalAlignment = HorizontalAlignment.Left, - VerticalAlignment = VerticalAlignment.Top, - RenderTransform = new TranslateTransform(110, 110), - } - }, - } - }; - - container.Measure(Size.Infinity); - container.Arrange(new Rect(container.DesiredSize)); - - var context = new DrawingContext(Mock.Of()); - context.Render(container); - - var result = container.InputHitTest(new Point(120, 120)); - - Assert.Equal(target, result); - } - } - - [Fact] - public void InputHitTest_Should_Not_Find_Control_Outside_Parent_Bounds_When_Clipped() - { - using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) - { - Border target; - - var container = new Panel - { - Width = 100, - Height = 200, - Children = new Controls.Controls - { - new Panel() - { - Width = 100, - Height = 100, - Margin = new Thickness(0, 100, 0, 0), - ClipToBounds = true, - Children = new Controls.Controls - { - (target = new Border() - { - Width = 100, - Height = 100, - Margin = new Thickness(0, -100, 0, 0) - }) - } - } - } - }; - - container.Measure(Size.Infinity); - container.Arrange(new Rect(container.DesiredSize)); - - var context = new DrawingContext(Mock.Of()); - context.Render(container); - - var result = container.InputHitTest(new Point(50, 50)); - - Assert.NotEqual(target, result); - Assert.Equal(container, result); - } - } - - [Fact] - public void InputHitTest_Should_Not_Find_Control_Outside_Scroll_ViewPort() - { - using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) - { - Border target; - Border item1; - Border item2; - ScrollContentPresenter scroll; - - var container = new Panel - { - Width = 100, - Height = 200, - Children = new Controls.Controls - { - (target = new Border() - { - Width = 100, - Height = 100 - }), - new Border() - { - Width = 100, - Height = 100, - Margin = new Thickness(0, 100, 0, 0), - Child = scroll = new ScrollContentPresenter() - { - Content = new StackPanel() - { - Children = new Controls.Controls - { - (item1 = new Border() - { - Width = 100, - Height = 100, - }), - (item2 = new Border() - { - Width = 100, - Height = 100, - }), - } - } - } - } - } - }; - - scroll.UpdateChild(); - - container.Measure(Size.Infinity); - container.Arrange(new Rect(container.DesiredSize)); - - var context = new DrawingContext(Mock.Of()); - context.Render(container); - - var result = container.InputHitTest(new Point(50, 150)); - - Assert.Equal(item1, result); - - result = container.InputHitTest(new Point(50, 50)); - - Assert.Equal(target, result); - - scroll.Offset = new Vector(0, 100); - - //we don't have setup LayoutManager so we will make it manually - scroll.Parent.InvalidateArrange(); - container.InvalidateArrange(); - - container.Arrange(new Rect(container.DesiredSize)); - context.Render(container); - - result = container.InputHitTest(new Point(50, 150)); - - Assert.Equal(item2, result); - - result = container.InputHitTest(new Point(50, 50)); - - Assert.NotEqual(item1, result); - Assert.Equal(target, result); - } - } - - class MockRenderInterface : IPlatformRenderInterface - { - public IFormattedTextImpl CreateFormattedText(string text, string fontFamilyName, double fontSize, FontStyle fontStyle, TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping) - { - throw new NotImplementedException(); - } - - public IRenderTarget CreateRenderTarget(IPlatformHandle handle) - { - throw new NotImplementedException(); - } - - public IRenderTargetBitmapImpl CreateRenderTargetBitmap(int width, int height) - { - throw new NotImplementedException(); - } - - public IStreamGeometryImpl CreateStreamGeometry() - { - return new MockStreamGeometry(); - } - - public IBitmapImpl LoadBitmap(Stream stream) - { - throw new NotImplementedException(); - } - - public IBitmapImpl LoadBitmap(string fileName) - { - throw new NotImplementedException(); - } - - class MockStreamGeometry : Avalonia.Platform.IStreamGeometryImpl - { - private MockStreamGeometryContext _impl = new MockStreamGeometryContext(); - public Rect Bounds - { - get - { - throw new NotImplementedException(); - } - } - - public Matrix Transform - { - get - { - throw new NotImplementedException(); - } - - set - { - throw new NotImplementedException(); - } - } - - public IStreamGeometryImpl Clone() - { - return this; - } - - public bool FillContains(Point point) - { - return _impl.FillContains(point); - } - - public Rect GetRenderBounds(double strokeThickness) - { - throw new NotImplementedException(); - } - - public IStreamGeometryContextImpl Open() - { - return _impl; - } - - class MockStreamGeometryContext : IStreamGeometryContextImpl - { - private List points = new List(); - public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) - { - throw new NotImplementedException(); - } - - public void BeginFigure(Point startPoint, bool isFilled) - { - points.Add(startPoint); - } - - public void CubicBezierTo(Point point1, Point point2, Point point3) - { - throw new NotImplementedException(); - } - - public void Dispose() - { - } - - public void EndFigure(bool isClosed) - { - } - - public void LineTo(Point point) - { - points.Add(point); - } - - public void QuadraticBezierTo(Point control, Point endPoint) - { - throw new NotImplementedException(); - } - - public void SetFillRule(FillRule fillRule) - { - } - - public bool FillContains(Point point) - { - // Use the algorithm from http://www.blackpawn.com/texts/pointinpoly/default.html - // to determine if the point is in the geometry (since it will always be convex in this situation) - for (int i = 0; i < points.Count; i++) - { - var a = points[i]; - var b = points[(i + 1) % points.Count]; - var c = points[(i + 2) % points.Count]; - - Vector v0 = c - a; - Vector v1 = b - a; - Vector v2 = point - a; - - var dot00 = v0 * v0; - var dot01 = v0 * v1; - var dot02 = v0 * v2; - var dot11 = v1 * v1; - var dot12 = v1 * v2; - - - var invDenom = 1 / (dot00 * dot11 - dot01 * dot01); - var u = (dot11 * dot02 - dot01 * dot12) * invDenom; - var v = (dot00 * dot12 - dot01 * dot02) * invDenom; - if ((u >= 0) && (v >= 0) && (u + v < 1)) return true; - } - return false; - } - } - } - } - } -} diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 1e257a698c5..3f9b2602255 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -54,7 +54,6 @@ public void Canvas_Is_Freed() }; var result = run(); - PurgeMoqReferences(); dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); @@ -90,7 +89,6 @@ public void Named_Canvas_Is_Freed() }; var result = run(); - PurgeMoqReferences(); dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); @@ -127,7 +125,6 @@ public void ScrollViewer_With_Content_Is_Freed() }; var result = run(); - PurgeMoqReferences(); dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); @@ -163,7 +160,6 @@ public void TextBox_Is_Freed() }; var result = run(); - PurgeMoqReferences(); dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); @@ -207,7 +203,6 @@ public void TextBox_With_Xaml_Binding_Is_Freed() }; var result = run(); - PurgeMoqReferences(); dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); @@ -294,21 +289,12 @@ public void TreeView_Is_Freed() }; var result = run(); - PurgeMoqReferences(); dotMemory.Check(memory => Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); } } - private static void PurgeMoqReferences() - { - // Moq holds onto references in its mock of IRenderer in case we want to check if a method has been called; - // clear these. - var renderer = Mock.Get(AvaloniaLocator.Current.GetService()); - renderer.ResetCalls(); - } - private class Node { public string Name { get; set; } diff --git a/tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v2.ncrunchproject b/tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v2.ncrunchproject index ed3fd080b1a..7c25da7d157 100644 --- a/tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v2.ncrunchproject +++ b/tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v2.ncrunchproject @@ -7,7 +7,7 @@ true false false - false + true false false true @@ -17,9 +17,9 @@ true true 60000 - - - + + + AutoDetect STA x86 diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index 064d5d36d3c..8d20ed66e68 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -15,6 +15,13 @@ public class TestRoot : Decorator, IFocusScope, ILayoutRoot, INameScope, IRender { private readonly NameScope _nameScope = new NameScope(); + public TestRoot() + { + var rendererFactory = AvaloniaLocator.Current.GetService(); + var renderLoop = AvaloniaLocator.Current.GetService(); + Renderer = rendererFactory?.CreateRenderer(this, renderLoop); + } + event EventHandler INameScope.Registered { add { _nameScope.Registered += value; ++NameScopeRegisteredSubscribers; } @@ -41,7 +48,7 @@ public event EventHandler Unregistered public IRenderTarget RenderTarget => null; - public IRenderer Renderer => null; + public IRenderer Renderer { get; } public IRenderTarget CreateRenderTarget() { @@ -50,7 +57,6 @@ public IRenderTarget CreateRenderTarget() public void Invalidate(Rect rect) { - throw new NotImplementedException(); } public Point PointToClient(Point p) => p; diff --git a/tests/Avalonia.UnitTests/TestServices.cs b/tests/Avalonia.UnitTests/TestServices.cs index 8dc838163f4..7e0437c7f34 100644 --- a/tests/Avalonia.UnitTests/TestServices.cs +++ b/tests/Avalonia.UnitTests/TestServices.cs @@ -21,7 +21,7 @@ public class TestServices assetLoader: new AssetLoader(), layoutManager: new LayoutManager(), platform: new AppBuilder().RuntimePlatform, - renderer: Mock.Of(), + renderer: (_, __) => Mock.Of(), renderInterface: CreateRenderInterfaceMock(), renderLoop: Mock.Of(), standardCursorFactory: Mock.Of(), @@ -42,6 +42,9 @@ public class TestServices public static readonly TestServices MockThreadingInterface = new TestServices( threadingInterface: Mock.Of(x => x.CurrentThreadIsLoopThread == true)); + public static readonly TestServices RealDeferredRenderer = new TestServices( + renderer: (root, loop) => new DeferredRenderer(root, loop)); + public static readonly TestServices RealFocus = new TestServices( focusManager: new FocusManager(), keyboardDevice: () => new KeyboardDevice(), @@ -60,7 +63,7 @@ public TestServices( Func keyboardDevice = null, ILayoutManager layoutManager = null, IRuntimePlatform platform = null, - IRenderer renderer = null, + Func renderer = null, IPlatformRenderInterface renderInterface = null, IRenderLoop renderLoop = null, IStandardCursorFactory standardCursorFactory = null, @@ -93,7 +96,7 @@ public TestServices( public Func KeyboardDevice { get; } public ILayoutManager LayoutManager { get; } public IRuntimePlatform Platform { get; } - public IRenderer Renderer { get; } + public Func Renderer { get; } public IPlatformRenderInterface RenderInterface { get; } public IRenderLoop RenderLoop { get; } public IStandardCursorFactory StandardCursorFactory { get; } @@ -110,7 +113,7 @@ public TestServices With( Func keyboardDevice = null, ILayoutManager layoutManager = null, IRuntimePlatform platform = null, - IRenderer renderer = null, + Func renderer = null, IPlatformRenderInterface renderInterface = null, IRenderLoop renderLoop = null, IStandardCursorFactory standardCursorFactory = null, diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs index 0dec2184323..ab71aa95cc5 100644 --- a/tests/Avalonia.UnitTests/UnitTestApplication.cs +++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs @@ -43,7 +43,7 @@ public override void RegisterServices() .Bind().ToConstant(Services.KeyboardDevice?.Invoke()) .Bind().ToConstant(Services.LayoutManager) .Bind().ToConstant(Services.Platform) - .Bind().ToConstant(Services.Renderer) + .Bind().ToConstant(new RendererFactory(Services.Renderer)) .Bind().ToConstant(Services.RenderInterface) .Bind().ToConstant(Services.RenderLoop) .Bind().ToConstant(Services.ThreadingInterface) @@ -58,5 +58,20 @@ public override void RegisterServices() Styles.AddRange(styles); } } + + private class RendererFactory : IRendererFactory + { + Func _func; + + public RendererFactory(Func func) + { + _func = func; + } + + public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop) + { + return _func?.Invoke(root, renderLoop); + } + } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.v2.ncrunchproject b/tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.v2.ncrunchproject index 30815b1937d..621f6269595 100644 --- a/tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.v2.ncrunchproject +++ b/tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.v2.ncrunchproject @@ -17,10 +17,11 @@ true true 60000 - - - + + + AutoDetect STA x86 + AbnormalReferenceResolution;LongTestTimesWithoutParallelExecution \ No newline at end of file diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs index 1ccd8655bce..36ff61ed6da 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs @@ -1,8 +1,10 @@ using System; +using System.Linq; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Rendering.SceneGraph; using Avalonia.UnitTests; +using Avalonia.VisualTree; using Xunit; namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph @@ -52,6 +54,70 @@ public void Should_Build_Initial_Scene() } } + [Fact] + public void Should_Respect_ZIndex() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + Border front; + Border back; + var tree = new TestRoot + { + Child = new Panel + { + Children = + { + (front = new Border + { + ZIndex = 1, + }), + (back = new Border + { + ZIndex = 0, + }), + } + } + }; + + var result = SceneBuilder.Update(new Scene(tree)); + + var panelNode = result.FindNode(tree.Child); + var expected = new IVisual[] { back, front }; + var actual = panelNode.Children.OfType().Select(x => x.Visual).ToArray(); + Assert.Equal(expected, actual); + } + } + + [Fact] + public void ClipBounds_Should_Be_In_Global_Coordinates() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + Border target; + var tree = new TestRoot + { + Child = new Decorator + { + Margin = new Thickness(24, 26), + Child = target = new Border + { + Margin = new Thickness(26, 24), + Width = 100, + Height = 100, + } + } + }; + + tree.Measure(Size.Infinity); + tree.Arrange(new Rect(tree.DesiredSize)); + + var result = SceneBuilder.Update(new Scene(tree)); + var targetNode = result.FindNode(target); + + Assert.Equal(new Rect(50, 50, 100, 100), targetNode.ClipBounds); + } + } + [Fact] public void Should_Update_Border_Background_Node() { diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs index 28ca7cdd417..39f90107551 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs @@ -11,6 +11,7 @@ using Avalonia.VisualTree; using Moq; using Xunit; +using System; namespace Avalonia.Visuals.UnitTests.VisualTree { @@ -19,9 +20,9 @@ public class VisualExtensionsTests_GetVisualsAt [Fact] public void GetVisualsAt_Should_Find_Controls_At_Point() { - using (var application = UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) + using (TestApplication()) { - var container = new Decorator + var container = new TestRoot { Width = 200, Height = 200, @@ -49,9 +50,9 @@ public void GetVisualsAt_Should_Find_Controls_At_Point() [Fact] public void GetVisualsAt_Should_Not_Find_Invisible_Controls_At_Point() { - using (var application = UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) + using (TestApplication()) { - var container = new Decorator + var container = new TestRoot { Width = 200, Height = 200, @@ -85,9 +86,9 @@ public void GetVisualsAt_Should_Not_Find_Invisible_Controls_At_Point() [Fact] public void GetVisualsAt_Should_Not_Find_Control_Outside_Point() { - using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) + using (TestApplication()) { - var container = new Decorator + var container = new TestRoot { Width = 200, Height = 200, @@ -115,27 +116,31 @@ public void GetVisualsAt_Should_Not_Find_Control_Outside_Point() [Fact] public void GetVisualsAt_Should_Return_Top_Controls_First() { - using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) + using (TestApplication()) { - var container = new Panel + Panel container; + var root = new TestRoot { - Width = 200, - Height = 200, - Children = new Controls.Controls + Child = container = new Panel { - new Border - { - Width = 100, - Height = 100, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - }, - new Border + Width = 200, + Height = 200, + Children = new Controls.Controls { - Width = 50, - Height = 50, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center + new Border + { + Width = 100, + Height = 100, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }, + new Border + { + Width = 50, + Height = 50, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } } } }; @@ -155,36 +160,40 @@ public void GetVisualsAt_Should_Return_Top_Controls_First() [Fact] public void GetVisualsAt_Should_Return_Top_Controls_First_With_ZIndex() { - using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) + using (TestApplication()) { - var container = new Panel + Panel container; + var root = new TestRoot { - Width = 200, - Height = 200, - Children = new Controls.Controls + Child = container = new Panel { - new Border + Width = 200, + Height = 200, + Children = new Controls.Controls { - Width = 100, - Height = 100, - ZIndex = 1, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - }, - new Border - { - Width = 50, - Height = 50, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - }, - new Border - { - Width = 75, - Height = 75, - ZIndex = 2, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center + new Border + { + Width = 100, + Height = 100, + ZIndex = 1, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }, + new Border + { + Width = 50, + Height = 50, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }, + new Border + { + Width = 75, + Height = 75, + ZIndex = 2, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } } } }; @@ -204,32 +213,36 @@ public void GetVisualsAt_Should_Return_Top_Controls_First_With_ZIndex() [Fact] public void GetVisualsAt_Should_Find_Control_Translated_Outside_Parent_Bounds() { - using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) + using (TestApplication()) { Border target; - var container = new Panel + Panel container; + var root = new TestRoot { - Width = 200, - Height = 200, - ClipToBounds = false, - Children = new Controls.Controls + Child = container = new Panel { - new Border + Width = 200, + Height = 200, + ClipToBounds = false, + Children = new Controls.Controls { - Width = 100, - Height = 100, - ZIndex = 1, - HorizontalAlignment = HorizontalAlignment.Left, - VerticalAlignment = VerticalAlignment.Top, - Child = target = new Border + new Border { - Width = 50, - Height = 50, + Width = 100, + Height = 100, + ZIndex = 1, HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Top, - RenderTransform = new TranslateTransform(110, 110), - } - }, + Child = target = new Border + { + Width = 50, + Height = 50, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Top, + RenderTransform = new TranslateTransform(110, 110), + } + }, + } } }; @@ -248,30 +261,33 @@ public void GetVisualsAt_Should_Find_Control_Translated_Outside_Parent_Bounds() [Fact] public void GetVisualsAt_Should_Not_Find_Control_Outside_Parent_Bounds_When_Clipped() { - using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) + using (TestApplication()) { Border target; - - var container = new Panel + Panel container; + var root = new TestRoot { - Width = 100, - Height = 200, - Children = new Controls.Controls + Child = container = new Panel { - new Panel() + Width = 100, + Height = 200, + Children = new Controls.Controls { - Width = 100, - Height = 100, - Margin = new Thickness(0, 100, 0, 0), - ClipToBounds = true, - Children = new Controls.Controls + new Panel() { - (target = new Border() + Width = 100, + Height = 100, + Margin = new Thickness(0, 100, 0, 0), + ClipToBounds = true, + Children = new Controls.Controls { - Width = 100, - Height = 100, - Margin = new Thickness(0, -100, 0, 0) - }) + (target = new Border() + { + Width = 100, + Height = 100, + Margin = new Thickness(0, -100, 0, 0) + }) + } } } } @@ -292,45 +308,48 @@ public void GetVisualsAt_Should_Not_Find_Control_Outside_Parent_Bounds_When_Clip [Fact] public void GetVisualsAt_Should_Not_Find_Control_Outside_Scroll_Viewport() { - using (UnitTestApplication.Start(new TestServices(renderInterface: new MockRenderInterface()))) + using (TestApplication()) { Border target; Border item1; Border item2; ScrollContentPresenter scroll; - - var container = new Panel + Panel container; + var root = new TestRoot { - Width = 100, - Height = 200, - Children = new Controls.Controls + Child = container = new Panel { - (target = new Border() - { - Width = 100, - Height = 100 - }), - new Border() + Width = 100, + Height = 200, + Children = new Controls.Controls { - Width = 100, - Height = 100, - Margin = new Thickness(0, 100, 0, 0), - Child = scroll = new ScrollContentPresenter() + (target = new Border() { - Content = new StackPanel() + Width = 100, + Height = 100 + }), + new Border() + { + Width = 100, + Height = 100, + Margin = new Thickness(0, 100, 0, 0), + Child = scroll = new ScrollContentPresenter() { - Children = new Controls.Controls + Content = new StackPanel() { - (item1 = new Border() - { - Width = 100, - Height = 100, - }), - (item2 = new Border() + Children = new Controls.Controls { - Width = 100, - Height = 100, - }), + (item1 = new Border() + { + Width = 100, + Height = 100, + }), + (item2 = new Border() + { + Width = 100, + Height = 100, + }), + } } } } @@ -373,5 +392,14 @@ public void GetVisualsAt_Should_Not_Find_Control_Outside_Scroll_Viewport() Assert.Equal(target, result); } } + + private IDisposable TestApplication() + { + return UnitTestApplication.Start( + new TestServices( + renderInterface: new MockRenderInterface(), + renderLoop: Mock.Of(), + renderer: (root, loop) => new DeferredRenderer(root, loop))); + } } } From 458007a1064df37a1999a2153dc30af33786425d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 6 Nov 2016 18:57:49 +0100 Subject: [PATCH 004/175] Updated libs with API changes. --- .../Avalonia.Cairo.v2.ncrunchproject | 2 +- .../Avalonia.Cairo/Media/DrawingContext.cs | 13 +++++---- .../Avalonia.Cairo/Media/FormattedTextImpl.cs | 9 +++--- .../Media/Imaging/RenderTargetBitmapImpl.cs | 2 +- src/Gtk/Avalonia.Cairo/Media/TileBrushes.cs | 2 +- src/Gtk/Avalonia.Cairo/RenderTarget.cs | 4 +-- .../Avalonia.Skia.Android.v2.ncrunchproject | 8 +++--- .../Avalonia.Skia.Desktop.v2.ncrunchproject | 6 ++-- .../Avalonia.Skia.Desktop/RenderTarget.cs | 13 ++++----- src/Skia/Avalonia.Skia.iOS/RenderTarget.cs | 12 +++----- src/Skia/Avalonia.Skia/BitmapImpl.cs | 4 +-- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 15 +++++----- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 28 +++++++++---------- ...valonia.Skia.RenderTests.v2.ncrunchproject | 6 ++-- 14 files changed, 59 insertions(+), 65 deletions(-) diff --git a/src/Gtk/Avalonia.Cairo/Avalonia.Cairo.v2.ncrunchproject b/src/Gtk/Avalonia.Cairo/Avalonia.Cairo.v2.ncrunchproject index f744eecae00..1f00ab7c749 100644 --- a/src/Gtk/Avalonia.Cairo/Avalonia.Cairo.v2.ncrunchproject +++ b/src/Gtk/Avalonia.Cairo/Avalonia.Cairo.v2.ncrunchproject @@ -7,7 +7,7 @@ true false false - true + false false false true diff --git a/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs b/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs index 9ec37d5514c..08c97867cb4 100644 --- a/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs +++ b/src/Gtk/Avalonia.Cairo/Media/DrawingContext.cs @@ -12,6 +12,7 @@ namespace Avalonia.Cairo.Media { using Avalonia.Media.Imaging; + using Platform; using Cairo = global::Cairo; /// @@ -75,9 +76,9 @@ public void Dispose() /// The opacity to draw with. /// The rect in the image to draw. /// The rect in the output to draw to. - public void DrawImage(IBitmap bitmap, double opacity, Rect sourceRect, Rect destRect) + public void DrawImage(IBitmapImpl bitmap, double opacity, Rect sourceRect, Rect destRect) { - var impl = bitmap.PlatformImpl as BitmapImpl; + var impl = bitmap as BitmapImpl; var size = new Size(impl.PixelWidth, impl.PixelHeight); var scale = new Vector(destRect.Width / sourceRect.Width, destRect.Height / sourceRect.Height); @@ -137,9 +138,9 @@ public void DrawLine(Pen pen, Point p1, Point p2) /// The fill brush. /// The stroke pen. /// The geometry. - public void DrawGeometry(IBrush brush, Pen pen, Geometry geometry) + public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry) { - var impl = geometry.PlatformImpl as StreamGeometryImpl; + var impl = geometry as StreamGeometryImpl; var oldMatrix = Transform; Transform = impl.Transform * Transform; @@ -192,9 +193,9 @@ public void DrawRectangle(Pen pen, Rect rect, float cornerRadius) /// The foreground brush. /// The upper-left corner of the text. /// The text. - public void DrawText(IBrush foreground, Point origin, FormattedText text) + public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) { - var layout = ((FormattedTextImpl)text.PlatformImpl).Layout; + var layout = ((FormattedTextImpl)text).Layout; _context.MoveTo(origin.X, origin.Y); using (var b = SetBrush(foreground, new Size(0, 0))) diff --git a/src/Gtk/Avalonia.Cairo/Media/FormattedTextImpl.cs b/src/Gtk/Avalonia.Cairo/Media/FormattedTextImpl.cs index e9d092559df..506773aaa04 100644 --- a/src/Gtk/Avalonia.Cairo/Media/FormattedTextImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/FormattedTextImpl.cs @@ -13,7 +13,6 @@ namespace Avalonia.Cairo.Media public class FormattedTextImpl : IFormattedTextImpl { private Size _size; - private readonly string _text; static double CorrectScale(double input) { @@ -32,7 +31,7 @@ public FormattedTextImpl( Contract.Requires(context != null); Contract.Requires(text != null); Layout = new Pango.Layout(context); - _text = text; + Text = text; Layout.SetText(text); Layout.FontDescription = new Pango.FontDescription { @@ -46,6 +45,8 @@ public FormattedTextImpl( Layout.Attributes = new Pango.AttrList(); } + public string Text { get; } + public Size Constraint { get @@ -99,7 +100,7 @@ public TextHitTestResult HitTestPoint(Point point) int PangoIndexToTextIndex(int pangoIndex) { - return Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(_text), 0, Math.Min(pangoIndex, _text.Length)).Length; + return Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(Text), 0, Math.Min(pangoIndex, Text.Length)).Length; } public Rect HitTestTextPosition(int index) @@ -109,7 +110,7 @@ public Rect HitTestTextPosition(int index) int TextIndexToPangoIndex(int textIndex) { - return Encoding.UTF8.GetByteCount(textIndex < _text.Length ? _text.Remove(textIndex) : _text); + return Encoding.UTF8.GetByteCount(textIndex < Text.Length ? Text.Remove(textIndex) : Text); } public IEnumerable HitTestTextRange(int index, int length) diff --git a/src/Gtk/Avalonia.Cairo/Media/Imaging/RenderTargetBitmapImpl.cs b/src/Gtk/Avalonia.Cairo/Media/Imaging/RenderTargetBitmapImpl.cs index db0aa082ccd..10ddd680e7e 100644 --- a/src/Gtk/Avalonia.Cairo/Media/Imaging/RenderTargetBitmapImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/Imaging/RenderTargetBitmapImpl.cs @@ -40,7 +40,7 @@ public void Save(string fileName) Surface.WriteToPng(fileName); } - public Avalonia.Media.DrawingContext CreateDrawingContext() + public IDrawingContextImpl CreateDrawingContext() { return _renderTarget.CreateDrawingContext(); } diff --git a/src/Gtk/Avalonia.Cairo/Media/TileBrushes.cs b/src/Gtk/Avalonia.Cairo/Media/TileBrushes.cs index b5332107496..e5aa022913d 100644 --- a/src/Gtk/Avalonia.Cairo/Media/TileBrushes.cs +++ b/src/Gtk/Avalonia.Cairo/Media/TileBrushes.cs @@ -23,7 +23,7 @@ public static SurfacePattern CreateTileBrush(TileBrush brush, Size targetSize) using (var intermediate = new ImageSurface(Format.ARGB32, (int)helper.IntermediateSize.Width, (int)helper.IntermediateSize.Height)) using (var ctx = new RenderTarget(intermediate).CreateDrawingContext()) { - helper.DrawIntermediate(ctx); + helper.DrawIntermediate(new Avalonia.Media.DrawingContext(ctx)); var result = new SurfacePattern(intermediate); diff --git a/src/Gtk/Avalonia.Cairo/RenderTarget.cs b/src/Gtk/Avalonia.Cairo/RenderTarget.cs index d2859867624..a4a41cb22d2 100644 --- a/src/Gtk/Avalonia.Cairo/RenderTarget.cs +++ b/src/Gtk/Avalonia.Cairo/RenderTarget.cs @@ -48,9 +48,7 @@ public RenderTarget(DrawingArea area) /// Creates a cairo surface that targets a platform-specific resource. /// /// A surface wrapped in an . - public DrawingContext CreateDrawingContext() => new DrawingContext(CreateMediaDrawingContext()); - - public IDrawingContextImpl CreateMediaDrawingContext() + public IDrawingContextImpl CreateDrawingContext() { if (_window != null) return new Media.DrawingContext(_window.GdkWindow); diff --git a/src/Skia/Avalonia.Skia.Android/Avalonia.Skia.Android.v2.ncrunchproject b/src/Skia/Avalonia.Skia.Android/Avalonia.Skia.Android.v2.ncrunchproject index 30815b1937d..f744eecae00 100644 --- a/src/Skia/Avalonia.Skia.Android/Avalonia.Skia.Android.v2.ncrunchproject +++ b/src/Skia/Avalonia.Skia.Android/Avalonia.Skia.Android.v2.ncrunchproject @@ -7,7 +7,7 @@ true false false - false + true false false true @@ -17,9 +17,9 @@ true true 60000 - - - + + + AutoDetect STA x86 diff --git a/src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.v2.ncrunchproject b/src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.v2.ncrunchproject index e1b4d7cf284..45b84227ee8 100644 --- a/src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.v2.ncrunchproject +++ b/src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.v2.ncrunchproject @@ -17,9 +17,9 @@ true true 60000 - - - + + + AutoDetect STA x86 diff --git a/src/Skia/Avalonia.Skia.Desktop/RenderTarget.cs b/src/Skia/Avalonia.Skia.Desktop/RenderTarget.cs index ef5fb88142b..f8f0d633c63 100644 --- a/src/Skia/Avalonia.Skia.Desktop/RenderTarget.cs +++ b/src/Skia/Avalonia.Skia.Desktop/RenderTarget.cs @@ -12,11 +12,9 @@ internal partial class RenderTarget : IRenderTarget { public SKSurface Surface { get; protected set; } - public virtual DrawingContext CreateDrawingContext() + public virtual IDrawingContextImpl CreateDrawingContext() { - return - new DrawingContext( - new DrawingContextImpl(Surface.Canvas)); + return new DrawingContextImpl(Surface.Canvas); } public void Dispose() @@ -102,7 +100,7 @@ private Size GetWindowDpiWin32() return new Size(96, 96); } - public override DrawingContext CreateDrawingContext() + public override IDrawingContextImpl CreateDrawingContext() { FixSize(); @@ -127,9 +125,8 @@ public override DrawingContext CreateDrawingContext() } } - var result = - new DrawingContext( - new WindowDrawingContextImpl(this), Matrix.CreateScale(scale, scale)); + // TODO: Broken DPI scaling on skia here. Fix this. + var result = new WindowDrawingContextImpl(this); return result; } diff --git a/src/Skia/Avalonia.Skia.iOS/RenderTarget.cs b/src/Skia/Avalonia.Skia.iOS/RenderTarget.cs index 083b611d5c8..a2be292bc60 100644 --- a/src/Skia/Avalonia.Skia.iOS/RenderTarget.cs +++ b/src/Skia/Avalonia.Skia.iOS/RenderTarget.cs @@ -11,11 +11,9 @@ internal partial class RenderTarget : IRenderTarget { public SKSurface Surface { get; protected set; } - public virtual DrawingContext CreateDrawingContext() + public virtual IDrawingContextImpl CreateDrawingContext() { - return - new DrawingContext( - new DrawingContextImpl(Surface.Canvas)); + return new DrawingContextImpl(Surface.Canvas); } public void Dispose() @@ -90,7 +88,7 @@ private void GetPlatformWindowSize(out int w, out int h) h = (int)bounds.Height; } - public override DrawingContext CreateDrawingContext() + public override IDrawingContextImpl CreateDrawingContext() { FixSize(); @@ -104,9 +102,7 @@ public override DrawingContext CreateDrawingContext() canvas.Clear(SKColors.Red); canvas.ResetMatrix(); - return - new DrawingContext( - new WindowDrawingContextImpl(this)); + return new WindowDrawingContextImpl(this); } public void Present() diff --git a/src/Skia/Avalonia.Skia/BitmapImpl.cs b/src/Skia/Avalonia.Skia/BitmapImpl.cs index 159e5852182..ab212e10cfa 100644 --- a/src/Skia/Avalonia.Skia/BitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/BitmapImpl.cs @@ -74,10 +74,10 @@ public override void Dispose() } } - public DrawingContext CreateDrawingContext() + public IDrawingContextImpl CreateDrawingContext() { - return new DrawingContext(new BitmapDrawingContext(Bitmap)); + return new BitmapDrawingContext(Bitmap); } public void Save(Stream stream) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 7744e151044..c3e7ce9d570 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Avalonia.Platform; namespace Avalonia.Skia { @@ -20,9 +21,9 @@ public DrawingContextImpl(SKCanvas canvas) Canvas.Clear(); } - public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect) + public void DrawImage(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect) { - var impl = (BitmapImpl)source.PlatformImpl; + var impl = (BitmapImpl)source; var s = sourceRect.ToSKRect(); var d = destRect.ToSKRect(); using (var paint = new SKPaint() @@ -40,9 +41,9 @@ public void DrawLine(Pen pen, Point p1, Point p2) } } - public void DrawGeometry(IBrush brush, Pen pen, Geometry geometry) + public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry) { - var impl = ((StreamGeometryImpl)geometry.PlatformImpl); + var impl = (StreamGeometryImpl)geometry; var size = geometry.Bounds.Size; using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper)) @@ -188,7 +189,7 @@ internal PaintWrapper CreatePaint(IBrush brush, Size targetSize) var bitmap = new BitmapImpl((int)helper.IntermediateSize.Width, (int)helper.IntermediateSize.Height); rv.AddDisposable(bitmap); using (var ctx = bitmap.CreateDrawingContext()) - helper.DrawIntermediate(ctx); + helper.DrawIntermediate(new DrawingContext(ctx)); SKMatrix translation = SKMatrix.MakeTranslation(-(float)helper.DestinationRect.X, -(float)helper.DestinationRect.Y); SKShaderTileMode tileX = tileBrush.TileMode == TileMode.None @@ -278,11 +279,11 @@ public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0) } } - public void DrawText(IBrush foreground, Point origin, FormattedText text) + public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) { using (var paint = CreatePaint(foreground, text.Measure())) { - var textImpl = text.PlatformImpl as FormattedTextImpl; + var textImpl = text as FormattedTextImpl; textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint); } } diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 51f4b2b724d..f0bacb801f9 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -15,10 +15,10 @@ public class FormattedTextImpl : IFormattedTextImpl public FormattedTextImpl(string text, string fontFamilyName, double fontSize, FontStyle fontStyle, TextAlignment textAlignment, FontWeight fontWeight, TextWrapping wrapping) { - _text = text ?? string.Empty; + Text = text ?? string.Empty; // Replace 0 characters with zero-width spaces (200B) - _text = _text.Replace((char)0, (char)0x200B); + Text = Text.Replace((char)0, (char)0x200B); var typeface = TypefaceCache.GetTypeface(fontFamilyName, fontStyle, fontWeight); @@ -98,7 +98,7 @@ public TextHitTestResult HitTestPoint(Point point) { IsInside = false, TextPosition = line.Start + offset, - IsTrailing = _text.Length == (line.Start + offset + 1) + IsTrailing = Text.Length == (line.Start + offset + 1) }; } @@ -108,7 +108,7 @@ public TextHitTestResult HitTestPoint(Point point) { IsInside = false, IsTrailing = end, - TextPosition = end ? _text.Length - 1 : 0 + TextPosition = end ? Text.Length - 1 : 0 }; } @@ -182,7 +182,7 @@ public void SetForegroundBrush(IBrush brush, int startIndex, int length) public override string ToString() { - return _text; + return Text; } internal void Draw(DrawingContextImpl context, @@ -231,7 +231,7 @@ internal void Draw(DrawingContextImpl context, if (!hasCusomFGBrushes) { - var subString = _text.Substring(line.Start, line.Length); + var subString = Text.Substring(line.Start, line.Length); canvas.DrawText(subString, x, origin.Y + line.Top + _lineOffset, paint); } else @@ -255,7 +255,7 @@ internal void Draw(DrawingContextImpl context, currentWrapper = foreground; } - subStr = _text.Substring(i, len); + subStr = Text.Substring(i, len); if (currFGPaint != currentWrapper.Paint) { @@ -284,7 +284,7 @@ internal void Draw(DrawingContextImpl context, private readonly List _lines = new List(); private readonly SKPaint _paint; private readonly List _rects = new List(); - private readonly string _text; + public string Text { get; } private readonly TextWrapping _wrapping; private Size _constraint = new Size(double.PositiveInfinity, double.PositiveInfinity); private float _lineHeight = 0; @@ -434,7 +434,7 @@ private void BuildRects() for (int i = line.Start; i < line.Start + line.TextLength; i++) { - float w = _paint.MeasureText(_text[i].ToString()); + float w = _paint.MeasureText(Text[i].ToString()); _rects.Add(new Rect( prevRight, @@ -490,7 +490,7 @@ private IBrush GetNextForegroundBrush(ref AvaloniaFormattedTextLine line, int in private List GetRects() { - if (_text.Length > _rects.Count) + if (Text.Length > _rects.Count) { BuildRects(); } @@ -500,7 +500,7 @@ private List GetRects() private void Rebuild() { - var length = _text.Length; + var length = Text.Length; _lines.Clear(); _rects.Clear(); @@ -536,7 +536,7 @@ private void Rebuild() int measured; int trailingnumber = 0; - subString = _text.Substring(curOff); + subString = Text.Substring(curOff); float constraint = -1; @@ -547,12 +547,12 @@ private void Rebuild() constraint = MAX_LINE_WIDTH; } - measured = LineBreak(_text, curOff, length, _paint, constraint, out trailingnumber); + measured = LineBreak(Text, curOff, length, _paint, constraint, out trailingnumber); AvaloniaFormattedTextLine line = new AvaloniaFormattedTextLine(); line.TextLength = measured; - subString = _text.Substring(line.Start, line.TextLength); + subString = Text.Substring(line.Start, line.TextLength); lineWidth = _paint.MeasureText(subString); line.Start = curOff; line.Length = measured - trailingnumber; diff --git a/tests/Avalonia.RenderTests/Avalonia.Skia.RenderTests.v2.ncrunchproject b/tests/Avalonia.RenderTests/Avalonia.Skia.RenderTests.v2.ncrunchproject index e1b4d7cf284..45b84227ee8 100644 --- a/tests/Avalonia.RenderTests/Avalonia.Skia.RenderTests.v2.ncrunchproject +++ b/tests/Avalonia.RenderTests/Avalonia.Skia.RenderTests.v2.ncrunchproject @@ -17,9 +17,9 @@ true true 60000 - - - + + + AutoDetect STA x86 From 86216d9420e101c02cab9154b3a80f4dbbaa9d61 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 6 Nov 2016 20:42:40 +0100 Subject: [PATCH 005/175] Use scene geometry for hit-testing. This means we now correctly hit test circles etc. --- samples/RenderTest/MainWindow.xaml.cs | 22 +++- src/Avalonia.Visuals/Avalonia.Visuals.csproj | 1 + src/Avalonia.Visuals/Media/DrawingContext.cs | 49 +++++++-- src/Avalonia.Visuals/Media/Geometry.cs | 13 ++- .../Platform/IGeometryImpl.cs | 12 ++- .../Rendering/SceneGraph/GeometryNode.cs | 5 +- .../Rendering/SceneGraph/IDrawNode.cs | 12 +++ .../Rendering/SceneGraph/ISceneNode.cs | 2 - .../Rendering/SceneGraph/IVisualNode.cs | 2 + .../Rendering/SceneGraph/ImageNode.cs | 4 +- .../Rendering/SceneGraph/LineNode.cs | 5 +- .../Rendering/SceneGraph/RectangleNode.cs | 4 +- .../Rendering/SceneGraph/Scene.cs | 2 + .../Rendering/SceneGraph/TextNode.cs | 4 +- .../Rendering/SceneGraph/VisualNode.cs | 14 ++- .../Media/StreamGeometryContextImpl.cs | 9 ++ .../Media/StreamGeometryImpl.cs | 5 + src/Skia/Avalonia.Skia/StreamGeometryImpl.cs | 7 ++ .../Avalonia.Direct2D1/Media/GeometryImpl.cs | 5 +- .../Rendering/SceneGraph/SceneBuilderTests.cs | 16 +++ .../VisualTree/MockRenderInterface.cs | 32 ++++-- .../VisualExtensionsTests_GetVisualsAt.cs | 100 ++++++++++++++++-- 22 files changed, 286 insertions(+), 39 deletions(-) create mode 100644 src/Avalonia.Visuals/Rendering/SceneGraph/IDrawNode.cs diff --git a/samples/RenderTest/MainWindow.xaml.cs b/samples/RenderTest/MainWindow.xaml.cs index 8fe306df7d3..b38415e5a4b 100644 --- a/samples/RenderTest/MainWindow.xaml.cs +++ b/samples/RenderTest/MainWindow.xaml.cs @@ -2,16 +2,19 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Linq; using System.Reactive.Linq; using Avalonia; using Avalonia.Animation; using Avalonia.Controls; using Avalonia.Controls.Shapes; using Avalonia.Data; +using Avalonia.Input; using Avalonia.Layout; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Rendering; +using Avalonia.VisualTree; namespace RenderTest { @@ -37,16 +40,18 @@ private void CreateAnimations() for (var i = 0; i < Count; ++i) { + Ellipse ellipse; var element = new Panel { Children = { - new Ellipse + (ellipse = new Ellipse { + Name = $"ellipse{i}", Width = 100, Height = 100, Fill = Brushes.Blue, - }, + }), new Path { Data = StreamGeometry.Parse( @@ -73,10 +78,23 @@ private void CreateAnimations() degrees, BindingPriority.Animation); + ellipse.PointerEnter += Ellipse_PointerEnter; + ellipse.PointerLeave += Ellipse_PointerLeave; + panel.Children.Add(element); } Content = panel; } + + private void Ellipse_PointerEnter(object sender, PointerEventArgs e) + { + ((Ellipse)sender).Fill = Brushes.Red; + } + + private void Ellipse_PointerLeave(object sender, PointerEventArgs e) + { + ((Ellipse)sender).Fill = Brushes.Blue; + } } } diff --git a/src/Avalonia.Visuals/Avalonia.Visuals.csproj b/src/Avalonia.Visuals/Avalonia.Visuals.csproj index 69caba17380..bc41f646d95 100644 --- a/src/Avalonia.Visuals/Avalonia.Visuals.csproj +++ b/src/Avalonia.Visuals/Avalonia.Visuals.csproj @@ -115,6 +115,7 @@ + diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index 9a5e83dbcb9..b724f53f203 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -75,7 +75,11 @@ private set /// The rect in the image to draw. /// The rect in the output to draw to. public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect) - => PlatformImpl.DrawImage(source.PlatformImpl, opacity, sourceRect, destRect); + { + Contract.Requires(source != null); + + PlatformImpl.DrawImage(source.PlatformImpl, opacity, sourceRect, destRect); + } /// /// Draws a line. @@ -83,7 +87,13 @@ public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect dest /// The stroke pen. /// The first point of the line. /// The second point of the line. - public void DrawLine(Pen pen, Point p1, Point p2) => PlatformImpl.DrawLine(pen, p1, p2); + public void DrawLine(Pen pen, Point p1, Point p2) + { + if (PenIsVisible(pen)) + { + PlatformImpl.DrawLine(pen, p1, p2); + } + } /// /// Draws a geometry. @@ -92,7 +102,12 @@ public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect dest /// The stroke pen. /// The geometry. public void DrawGeometry(IBrush brush, Pen pen, Geometry geometry) - => PlatformImpl.DrawGeometry(brush, pen, geometry.PlatformImpl); + { + if (brush != null || PenIsVisible(pen)) + { + PlatformImpl.DrawGeometry(brush, pen, geometry.PlatformImpl); + } + } /// /// Draws the outline of a rectangle. @@ -101,7 +116,12 @@ public void DrawGeometry(IBrush brush, Pen pen, Geometry geometry) /// The rectangle bounds. /// The corner radius. public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0.0f) - => PlatformImpl.DrawRectangle(pen, rect, cornerRadius); + { + if (PenIsVisible(pen)) + { + PlatformImpl.DrawRectangle(pen, rect, cornerRadius); + } + } /// /// Draws text. @@ -110,7 +130,14 @@ public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0.0f) /// The upper-left corner of the text. /// The text. public void DrawText(IBrush foreground, Point origin, FormattedText text) - => PlatformImpl.DrawText(foreground, origin, text.PlatformImpl); + { + Contract.Requires(text != null); + + if (foreground != null) + { + PlatformImpl.DrawText(foreground, origin, text.PlatformImpl); + } + } /// /// Draws a filled rectangle. @@ -119,7 +146,12 @@ public void DrawText(IBrush foreground, Point origin, FormattedText text) /// The rectangle bounds. /// The corner radius. public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f) - => PlatformImpl.FillRectangle(brush, rect, cornerRadius); + { + if (brush != null && rect != Rect.Empty) + { + PlatformImpl.FillRectangle(brush, rect, cornerRadius); + } + } public struct PushedState : IDisposable { @@ -277,5 +309,10 @@ public void Dispose() _transformContainers = null; PlatformImpl.Dispose(); } + + private static bool PenIsVisible(Pen pen) + { + return pen?.Brush != null && pen.Thickness > 0; + } } } diff --git a/src/Avalonia.Visuals/Media/Geometry.cs b/src/Avalonia.Visuals/Media/Geometry.cs index 092aea0c616..bbed5a9e640 100644 --- a/src/Avalonia.Visuals/Media/Geometry.cs +++ b/src/Avalonia.Visuals/Media/Geometry.cs @@ -68,7 +68,7 @@ public Rect GetRenderBounds(double strokeThickness) } /// - /// Indicates whether the geometry contains the specified point. + /// Indicates whether the geometry's fill contains the specified point. /// /// The point. /// true if the geometry contains the point; otherwise, false. @@ -76,5 +76,16 @@ public bool FillContains(Point point) { return PlatformImpl.FillContains(point); } + + /// + /// Indicates whether the geometry's stroke contains the specified point. + /// + /// The pen to use. + /// The point. + /// true if the geometry contains the point; otherwise, false. + public bool StrokeContains(Pen pen, Point point) + { + return PlatformImpl.StrokeContains(pen, point); + } } } diff --git a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs index e18a76d739a..34a7bb13941 100644 --- a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs +++ b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs @@ -1,6 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Media; + namespace Avalonia.Platform { /// @@ -26,10 +28,18 @@ public interface IGeometryImpl Rect GetRenderBounds(double strokeThickness); /// - /// Indicates whether the geometry contains the specified point. + /// Indicates whether the geometry's fill contains the specified point. /// /// The point. /// true if the geometry contains the point; otherwise, false. bool FillContains(Point point); + + /// + /// Indicates whether the geometry's stroke contains the specified point. + /// + /// The stroke to use. + /// The point. + /// true if the geometry contains the point; otherwise, false. + bool StrokeContains(Pen pen, Point point); } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs index db05533d4a3..fa40eb04ed8 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs @@ -7,7 +7,7 @@ namespace Avalonia.Rendering.SceneGraph { - public class GeometryNode : ISceneNode + public class GeometryNode : IDrawNode { public GeometryNode(Matrix transform, IBrush brush, Pen pen, IGeometryImpl geometry) { @@ -38,7 +38,8 @@ public void Render(IDrawingContextImpl context) public bool HitTest(Point p) { - throw new NotImplementedException(); + p *= Transform.Invert(); + return Geometry.FillContains(p) || Geometry.StrokeContains(Pen, p); } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IDrawNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/IDrawNode.cs new file mode 100644 index 00000000000..0b4507bccb6 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/IDrawNode.cs @@ -0,0 +1,12 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; + +namespace Avalonia.Rendering.SceneGraph +{ + public interface IDrawNode : ISceneNode + { + bool HitTest(Point p); + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs index e022970c1e0..f6586c19805 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ISceneNode.cs @@ -8,8 +8,6 @@ namespace Avalonia.Rendering.SceneGraph { public interface ISceneNode { - bool HitTest(Point p); - void Render(IDrawingContextImpl context); } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs index ddaf3a4e9ca..7850100804a 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs @@ -13,5 +13,7 @@ public interface IVisualNode : ISceneNode Rect ClipBounds { get; set; } bool ClipToBounds { get; set; } IReadOnlyList Children { get; } + + bool HitTest(Point p); } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs index 6d3e5c6a565..3d861279c3f 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs @@ -7,7 +7,7 @@ namespace Avalonia.Rendering.SceneGraph { - public class ImageNode : ISceneNode + public class ImageNode : IDrawNode { public ImageNode(Matrix transform, IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect) { @@ -41,7 +41,7 @@ public void Render(IDrawingContextImpl context) public bool HitTest(Point p) { - throw new NotImplementedException(); + return (DestRect * Transform).Contains(p); } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs index 4b30e94b7db..84f08b0efea 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs @@ -6,7 +6,7 @@ namespace Avalonia.Rendering.SceneGraph { - public class LineNode : ISceneNode + public class LineNode : IDrawNode { public LineNode(Matrix transform, Pen pen, Point p1, Point p2) { @@ -34,7 +34,8 @@ public void Render(IDrawingContextImpl context) public bool HitTest(Point p) { - throw new NotImplementedException(); + // TODO: Implement line hit testing. + return false; } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs index d4fed636a1b..f059c1c2d68 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs @@ -6,7 +6,7 @@ namespace Avalonia.Rendering.SceneGraph { - public class RectangleNode : ISceneNode + public class RectangleNode : IDrawNode { public RectangleNode(Matrix transform, IBrush brush, Pen pen, Rect rect, float cornerRadius) { @@ -49,7 +49,7 @@ public void Render(IDrawingContextImpl context) public bool HitTest(Point p) { - throw new NotImplementedException(); + return (Rect * Transform).Contains(p); } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs index 77c1a45c905..12a9a45f434 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs @@ -99,6 +99,8 @@ private IEnumerable HitTest(IVisualNode node, Point p, Rect? clip, Func } } + dynamic d = node.Visual; + if (node.HitTest(p)) { yield return node.Visual; diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs index f9bc16c807f..c7fae327697 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs @@ -7,7 +7,7 @@ namespace Avalonia.Rendering.SceneGraph { - public class TextNode : ISceneNode + public class TextNode : IDrawNode { public TextNode(Matrix transform, IBrush foreground, Point origin, IFormattedTextImpl text) { @@ -38,7 +38,7 @@ internal bool Equals(Matrix transform, IBrush foreground, Point origin, IFormatt public bool HitTest(Point p) { - throw new NotImplementedException(); + return (new Rect(Origin, Text.Measure()) * Transform).Contains(p); } } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 1482d7d468e..98d7ceab579 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -34,7 +34,17 @@ public VisualNode Clone() public bool HitTest(Point p) { - return ClipBounds.Contains(p); + foreach (var child in Children) + { + var drawNode = child as IDrawNode; + + if (drawNode?.HitTest(p) == true) + { + return true; + } + } + + return false; } public void Render(IDrawingContextImpl context) @@ -48,7 +58,7 @@ public void Render(IDrawingContextImpl context) if (ClipToBounds) { - context.PushClip(ClipBounds); + context.PushClip(ClipBounds * Transform.Invert()); } foreach (var child in Children) diff --git a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs index 0aca4b0b335..ca16ab56308 100644 --- a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryContextImpl.cs @@ -71,6 +71,15 @@ internal bool FillContains(Point point) } } + internal bool StrokeContains(Pen pen, Point point) + { + using (var context = new Cairo.Context(new Cairo.ImageSurface(Cairo.Format.Argb32, 0, 0))) + { + context.AppendPath(Path); + return context.InStroke(point.X, point.Y); + } + } + public void LineTo(Point point) { if (this.Path == null) diff --git a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs index c325ebab147..b7f8b698c95 100644 --- a/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs +++ b/src/Gtk/Avalonia.Cairo/Media/StreamGeometryImpl.cs @@ -72,5 +72,10 @@ public bool FillContains(Point point) { return _impl.FillContains(point); } + + public bool StrokeContains(Pen pen, Point point) + { + return _impl.StrokeContains(pen, point); + } } } diff --git a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs index 33d4ce10c7c..420d11b965f 100644 --- a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs @@ -84,6 +84,13 @@ public bool FillContains(Point point) return GetRenderBounds(0).Contains(point); } + public bool StrokeContains(Pen pen, Point point) + { + // TODO: Not supported by SkiaSharp yet, so use expanded Rect + // return EffectivePath.Contains(point.X, point.Y); + return GetRenderBounds(0).Contains(point); + } + class StreamContext : IStreamGeometryContextImpl { private readonly StreamGeometryImpl _geometryImpl; diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs index b98c944c7df..dda2f2af89f 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs @@ -85,11 +85,14 @@ public Rect GetRenderBounds(double strokeThickness) } } - public bool FillContains(Point point) { return Geometry.FillContainsPoint(point.ToSharpDX()); } + public bool StrokeContains(Avalonia.Media.Pen pen, Point point) + { + return Geometry.StrokeContainsPoint(point.ToSharpDX(), (float)pen.Thickness); + } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs index 36ff61ed6da..97630d56325 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs @@ -22,6 +22,8 @@ public void Should_Build_Initial_Scene() { Child = border = new Border { + Width = 100, + Height = 100, Background = Brushes.Red, Child = textBlock = new TextBlock { @@ -30,6 +32,9 @@ public void Should_Build_Initial_Scene() } }; + tree.Measure(Size.Infinity); + tree.Arrange(new Rect(tree.DesiredSize)); + var initial = new Scene(tree); var result = SceneBuilder.Update(initial); @@ -129,14 +134,20 @@ public void Should_Update_Border_Background_Node() { Child = border = new Border { + Width = 100, + Height = 100, Background = Brushes.Red, Child = textBlock = new TextBlock { + Foreground = Brushes.Green, Text = "Hello World", } } }; + tree.Measure(Size.Infinity); + tree.Arrange(new Rect(tree.DesiredSize)); + var initial = SceneBuilder.Update(new Scene(tree)); var initialBackgroundNode = initial.FindNode(border).Children[0]; var initialTextNode = initial.FindNode(textBlock).Children[0]; @@ -172,6 +183,8 @@ public void Should_Update_When_Control_Removed() TextBlock textBlock; var tree = new TestRoot { + Width = 100, + Height = 100, Child = border = new Border { Background = Brushes.Red, @@ -182,6 +195,9 @@ public void Should_Update_When_Control_Removed() } }; + tree.Measure(Size.Infinity); + tree.Arrange(new Rect(tree.DesiredSize)); + var initial = SceneBuilder.Update(new Scene(tree)); border.Child = null; diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs index 3d6a9093e4d..ee85867f7d2 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs @@ -48,13 +48,8 @@ public IBitmapImpl LoadBitmap(string fileName) class MockStreamGeometry : IStreamGeometryImpl { private MockStreamGeometryContext _impl = new MockStreamGeometryContext(); - public Rect Bounds - { - get - { - throw new NotImplementedException(); - } - } + + public Rect Bounds => _impl.CalculateBounds(); public Matrix Transform { @@ -79,6 +74,11 @@ public bool FillContains(Point point) return _impl.FillContains(point); } + public bool StrokeContains(Pen pen, Point point) + { + return false; + } + public Rect GetRenderBounds(double strokeThickness) { throw new NotImplementedException(); @@ -102,6 +102,24 @@ public void BeginFigure(Point startPoint, bool isFilled) points.Add(startPoint); } + public Rect CalculateBounds() + { + var left = double.MaxValue; + var right = double.MinValue; + var top = double.MaxValue; + var bottom = double.MinValue; + + foreach (var p in points) + { + left = Math.Min(p.X, left); + right = Math.Max(p.X, right); + top = Math.Min(p.Y, top); + bottom = Math.Max(p.Y, bottom); + } + + return new Rect(new Point(left, top), new Point(right, bottom)); + } + public void CubicBezierTo(Point point1, Point point2, Point point3) { throw new NotImplementedException(); diff --git a/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs b/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs index 39f90107551..ba2dcc82c6d 100644 --- a/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs +++ b/tests/Avalonia.Visuals.UnitTests/VisualTree/VisualExtensionsTests_GetVisualsAt.cs @@ -12,6 +12,7 @@ using Moq; using Xunit; using System; +using Avalonia.Controls.Shapes; namespace Avalonia.Visuals.UnitTests.VisualTree { @@ -30,6 +31,7 @@ public void GetVisualsAt_Should_Find_Controls_At_Point() { Width = 100, Height = 100, + Background = Brushes.Red, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center } @@ -43,7 +45,37 @@ public void GetVisualsAt_Should_Find_Controls_At_Point() var result = container.GetVisualsAt(new Point(100, 100)); - Assert.Equal(new[] { container.Child, container }, result); + Assert.Equal(new[] { container.Child }, result); + } + } + + [Fact] + public void GetVisualsAt_Should_Not_Find_Empty_Controls_At_Point() + { + using (TestApplication()) + { + var container = new TestRoot + { + Width = 200, + Height = 200, + Child = new Border + { + Width = 100, + Height = 100, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + }; + + container.Measure(Size.Infinity); + container.Arrange(new Rect(container.DesiredSize)); + + var context = new DrawingContext(Mock.Of()); + context.Render(container); + + var result = container.GetVisualsAt(new Point(100, 100)); + + Assert.Empty(result); } } @@ -52,6 +84,7 @@ public void GetVisualsAt_Should_Not_Find_Invisible_Controls_At_Point() { using (TestApplication()) { + Border visible; var container = new TestRoot { Width = 200, @@ -60,11 +93,13 @@ public void GetVisualsAt_Should_Not_Find_Invisible_Controls_At_Point() { Width = 100, Height = 100, + Background = Brushes.Red, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, IsVisible = false, - Child = new Border + Child = visible = new Border { + Background = Brushes.Red, HorizontalAlignment = HorizontalAlignment.Stretch, VerticalAlignment = VerticalAlignment.Stretch, } @@ -79,7 +114,7 @@ public void GetVisualsAt_Should_Not_Find_Invisible_Controls_At_Point() var result = container.GetVisualsAt(new Point(100, 100)); - Assert.Equal(new[] { container }, result); + Assert.Empty(result); } } @@ -96,6 +131,7 @@ public void GetVisualsAt_Should_Not_Find_Control_Outside_Point() { Width = 100, Height = 100, + Background = Brushes.Red, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center } @@ -109,7 +145,7 @@ public void GetVisualsAt_Should_Not_Find_Control_Outside_Point() var result = container.GetVisualsAt(new Point(10, 10)); - Assert.Equal(new[] { container }, result); + Assert.Empty(result); } } @@ -131,6 +167,7 @@ public void GetVisualsAt_Should_Return_Top_Controls_First() { Width = 100, Height = 100, + Background = Brushes.Red, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center }, @@ -138,6 +175,7 @@ public void GetVisualsAt_Should_Return_Top_Controls_First() { Width = 50, Height = 50, + Background = Brushes.Red, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center } @@ -153,7 +191,7 @@ public void GetVisualsAt_Should_Return_Top_Controls_First() var result = container.GetVisualsAt(new Point(100, 100)); - Assert.Equal(new[] { container.Children[1], container.Children[0], container }, result); + Assert.Equal(new[] { container.Children[1], container.Children[0] }, result); } } @@ -176,6 +214,7 @@ public void GetVisualsAt_Should_Return_Top_Controls_First_With_ZIndex() Width = 100, Height = 100, ZIndex = 1, + Background = Brushes.Red, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center }, @@ -183,6 +222,7 @@ public void GetVisualsAt_Should_Return_Top_Controls_First_With_ZIndex() { Width = 50, Height = 50, + Background = Brushes.Red, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center }, @@ -191,6 +231,7 @@ public void GetVisualsAt_Should_Return_Top_Controls_First_With_ZIndex() Width = 75, Height = 75, ZIndex = 2, + Background = Brushes.Red, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center } @@ -206,7 +247,7 @@ public void GetVisualsAt_Should_Return_Top_Controls_First_With_ZIndex() var result = container.GetVisualsAt(new Point(100, 100)); - Assert.Equal(new[] { container.Children[2], container.Children[0], container.Children[1], container }, result); + Assert.Equal(new[] { container.Children[2], container.Children[0], container.Children[1] }, result); } } @@ -223,6 +264,7 @@ public void GetVisualsAt_Should_Find_Control_Translated_Outside_Parent_Bounds() { Width = 200, Height = 200, + Background = Brushes.Red, ClipToBounds = false, Children = new Controls.Controls { @@ -231,12 +273,14 @@ public void GetVisualsAt_Should_Find_Control_Translated_Outside_Parent_Bounds() 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), @@ -271,12 +315,14 @@ public void GetVisualsAt_Should_Not_Find_Control_Outside_Parent_Bounds_When_Clip { Width = 100, Height = 200, + Background = Brushes.Red, Children = new Controls.Controls { new Panel() { Width = 100, Height = 100, + Background = Brushes.Red, Margin = new Thickness(0, 100, 0, 0), ClipToBounds = true, Children = new Controls.Controls @@ -285,6 +331,7 @@ public void GetVisualsAt_Should_Not_Find_Control_Outside_Parent_Bounds_When_Clip { Width = 100, Height = 100, + Background = Brushes.Red, Margin = new Thickness(0, -100, 0, 0) }) } @@ -321,17 +368,20 @@ public void GetVisualsAt_Should_Not_Find_Control_Outside_Scroll_Viewport() { Width = 100, Height = 200, + Background = Brushes.Red, Children = new Controls.Controls { (target = new Border() { Width = 100, - Height = 100 + Height = 100, + Background = Brushes.Red, }), new Border() { Width = 100, Height = 100, + Background = Brushes.Red, Margin = new Thickness(0, 100, 0, 0), Child = scroll = new ScrollContentPresenter() { @@ -343,11 +393,13 @@ public void GetVisualsAt_Should_Not_Find_Control_Outside_Scroll_Viewport() { Width = 100, Height = 100, + Background = Brushes.Red, }), (item2 = new Border() { Width = 100, Height = 100, + Background = Brushes.Red, }), } } @@ -393,6 +445,40 @@ public void GetVisualsAt_Should_Not_Find_Control_Outside_Scroll_Viewport() } } + + [Fact] + public void GetVisualsAt_Should_Not_Find_Path_When_Outside_Fill() + { + using (TestApplication()) + { + Path path; + var container = 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") + } + }; + + container.Measure(Size.Infinity); + container.Arrange(new Rect(container.DesiredSize)); + + var context = new DrawingContext(Mock.Of()); + context.Render(container); + + var result = container.GetVisualsAt(new Point(100, 100)); + Assert.Equal(new[] { path }, result); + + result = container.GetVisualsAt(new Point(10, 10)); + Assert.Empty(result); + } + } + private IDisposable TestApplication() { return UnitTestApplication.Start( From 213e3c89c66e45d2d3158e060f58e7b5ab956600 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 6 Nov 2016 20:45:10 +0100 Subject: [PATCH 006/175] Fix hit testing on tab strip items. Now that we use the scenegraph for hit testing. This fixes the tabs in ControlCatalog. --- src/Avalonia.Themes.Default/TabStripItem.xaml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Themes.Default/TabStripItem.xaml b/src/Avalonia.Themes.Default/TabStripItem.xaml index 35848bda592..cee1c5e4601 100644 --- a/src/Avalonia.Themes.Default/TabStripItem.xaml +++ b/src/Avalonia.Themes.Default/TabStripItem.xaml @@ -1,5 +1,6 @@ \ No newline at end of file diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 308a65dcd47..61bbcc19399 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -124,13 +124,12 @@ public void StyleResource_Can_Be_Assigned_To_StyleResource_Property() { var xaml = @" @@ -139,12 +138,12 @@ public void StyleResource_Can_Be_Assigned_To_StyleResource_Property() var loader = new AvaloniaXamlLoader(); var window = (Window)loader.Load(xaml); - var brush = (Avalonia.Media.Mutable.SolidColorBrush)window.FindStyleResource("brush"); + var brush = (Avalonia.Media.ISolidColorBrush)window.FindStyleResource("brush"); var button = window.FindControl