From ada15eba002f8266a9ed339fd2a4f10cccfa123e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 4 Nov 2016 18:54:52 +0100 Subject: [PATCH] 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 feb121186f..8fe306df7d 100644 --- a/samples/RenderTest/MainWindow.xaml.cs +++ b/samples/RenderTest/MainWindow.xaml.cs @@ -22,7 +22,7 @@ namespace RenderTest 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 427749263a..688d507c8e 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -64,7 +64,7 @@ namespace Avalonia.Controls.Shapes 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 d9736e4c6e..aeffde81ed 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 ddaa681bab..9a5e83dbcb 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 @@ namespace Avalonia.Media 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 @@ namespace Avalonia.Media var transform = _currentTransform*_currentContainerTransform; if (_hiddenPostTransform.HasValue) transform = transform*_hiddenPostTransform.Value; - _impl.Transform = transform; + PlatformImpl.Transform = transform; } } @@ -79,7 +75,7 @@ namespace Avalonia.Media /// 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 @@ namespace Avalonia.Media /// 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 @@ namespace Avalonia.Media /// 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 @@ namespace Avalonia.Media /// 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 @@ namespace Avalonia.Media /// 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 @@ namespace Avalonia.Media /// 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 @@ namespace Avalonia.Media 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 @@ namespace Avalonia.Media /// 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 @@ namespace Avalonia.Media 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 @@ namespace Avalonia.Media 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 @@ namespace Avalonia.Media /// 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 @@ namespace Avalonia.Media _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 c63abc160f..0fb57923dd 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 @@ namespace Avalonia.Media /// 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 @@ namespace Avalonia.Media /// 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 @@ namespace Avalonia.Media /// 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 4cc9c3fdf2..749d690d48 100644 --- a/src/Avalonia.Visuals/Media/Imaging/RenderTargetBitmap.cs +++ b/src/Avalonia.Visuals/Media/Imaging/RenderTargetBitmap.cs @@ -48,6 +48,6 @@ namespace Avalonia.Media.Imaging 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 14f9e987bd..e3a44c437e 100644 --- a/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IFormattedTextImpl.cs @@ -17,6 +17,11 @@ namespace Avalonia.Platform /// 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 75ba9d5a56..fcde194884 100644 --- a/src/Avalonia.Visuals/Platform/IRenderTarget.cs +++ b/src/Avalonia.Visuals/Platform/IRenderTarget.cs @@ -17,6 +17,6 @@ namespace Avalonia.Platform /// /// 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 0000000000..3721544234 --- /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 c643662179..119bb4c8d1 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 ed58d129af..3b2fe19be4 100644 --- a/src/Avalonia.Visuals/Rendering/Renderer.cs +++ b/src/Avalonia.Visuals/Rendering/Renderer.cs @@ -23,6 +23,8 @@ namespace Avalonia.Rendering _renderLoop.Tick += OnRenderLoopTick; } + public bool DrawFps { get; set; } + public void AddDirty(IVisual visual) { _dirty = true; @@ -42,6 +44,7 @@ namespace Avalonia.Rendering 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 3d13501edd..3a24e1a22f 100644 --- a/src/Avalonia.Visuals/Rendering/RendererMixin.cs +++ b/src/Avalonia.Visuals/Rendering/RendererMixin.cs @@ -42,34 +42,34 @@ namespace Avalonia.Rendering /// 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 0000000000..5717ccdf0e --- /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 0000000000..89850f0c0e --- /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 0000000000..073d9716d7 --- /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 0000000000..17fcb7e219 --- /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 0000000000..f8aef2aed9 --- /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 0000000000..9ff0fbde4e --- /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 0000000000..738cd189ad --- /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 0000000000..f0212d23c3 --- /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 0000000000..b3ac55fe6a --- /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 0000000000..3718f16af4 --- /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 0000000000..3f4cea5c30 --- /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 30815b1937..1f00ab7c74 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 9f9558ff76..eeae2bff1c 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 b43eef2fa9..2b465619ef 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -29,9 +29,9 @@ namespace Avalonia.Direct2D1 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 @@ namespace Avalonia.Direct2D1 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 a1b9f85783..51b4eef62d 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 75a0f43d9f..2747ad7ca0 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContext.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -5,6 +5,7 @@ using System; 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 @@ namespace Avalonia.Direct2D1.Media 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 @@ namespace Avalonia.Direct2D1.Media /// 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 @@ namespace Avalonia.Direct2D1.Media /// 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 @@ namespace Avalonia.Direct2D1.Media { if (d2dBrush.PlatformBrush != null) { - var impl = (GeometryImpl)geometry.PlatformImpl; + var impl = (GeometryImpl)geometry; _renderTarget.FillGeometry(impl.Geometry, d2dBrush.PlatformBrush); } } @@ -141,7 +142,7 @@ namespace Avalonia.Direct2D1.Media { 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 @@ namespace Avalonia.Direct2D1.Media /// 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 45d06b5e27..b8e295e8ab 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FormattedTextImpl.cs @@ -23,6 +23,8 @@ namespace Avalonia.Direct2D1.Media { var factory = AvaloniaLocator.Current.GetService(); + Text = text; + using (var format = new DWrite.TextFormat( factory, fontFamily, @@ -58,6 +60,8 @@ namespace Avalonia.Direct2D1.Media } } + 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 eff832407e..219693ca23 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs @@ -39,7 +39,7 @@ namespace Avalonia.Direct2D1.Media 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 dd1aac3776..87f886008c 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TileBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TileBrushImpl.cs @@ -23,7 +23,7 @@ namespace Avalonia.Direct2D1.Media 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 180e1a7472..32dd4ac794 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 @@ namespace Avalonia.Direct2D1 /// Creates a drawing context for a rendering session. /// /// An . - public DrawingContext CreateDrawingContext() + public IDrawingContextImpl CreateDrawingContext() { var window = _renderTarget as WindowRenderTarget; @@ -100,7 +102,7 @@ namespace Avalonia.Direct2D1 } } - 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 30815b1937..3f48b966ba 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 9e7f739a1c..c885914279 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 0000000000..1ccd8655bc --- /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 bdcbae59ce..b291afb6d3 100644 --- a/tests/Avalonia.Visuals.UnitTests/TestRoot.cs +++ b/tests/Avalonia.Visuals.UnitTests/TestRoot.cs @@ -23,7 +23,7 @@ namespace Avalonia.Visuals.UnitTests public IRenderer Renderer { - get { throw new NotImplementedException(); } + get { return AvaloniaLocator.Current.GetService(); } } public Point PointToClient(Point p)