Browse Source

Merge pull request #827 from AvaloniaUI/scenegraph

Rewrite of rendering stack
pull/1085/head
Steven Kirk 9 years ago
committed by GitHub
parent
commit
8d68db8922
  1. 5
      .ncrunch/Avalonia.LinuxFramebuffer.v3.ncrunchproject
  2. 5
      .ncrunch/Avalonia.Visuals.UnitTests.net461.v3.ncrunchproject
  3. 5
      .ncrunch/Avalonia.Win32.Interop.v3.ncrunchproject
  4. 1
      samples/ControlCatalog/MainWindow.xaml.cs
  5. 2
      samples/interop/Direct3DInteropSample/Program.cs
  6. 2
      src/Android/Avalonia.Android/AndroidPlatform.cs
  7. 8
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  8. 7
      src/Avalonia.Controls/Platform/ITopLevelImpl.cs
  9. 6
      src/Avalonia.Controls/TopLevel.cs
  10. 9
      src/Avalonia.Controls/Window.cs
  11. 2
      src/Avalonia.Controls/WindowBase.cs
  12. 3
      src/Avalonia.Themes.Default/CheckBox.xaml
  13. 1
      src/Avalonia.Themes.Default/DropDownItem.xaml
  14. 6
      src/Avalonia.Themes.Default/ListBox.xaml
  15. 1
      src/Avalonia.Themes.Default/ListBoxItem.xaml
  16. 1
      src/Avalonia.Themes.Default/MenuItem.xaml
  17. 3
      src/Avalonia.Themes.Default/RadioButton.xaml
  18. 3
      src/Avalonia.Themes.Default/ScrollViewer.xaml
  19. 1
      src/Avalonia.Themes.Default/TabStripItem.xaml
  20. 6
      src/Avalonia.Themes.Default/TreeView.xaml
  21. 3
      src/Avalonia.Themes.Default/TreeViewItem.xaml
  22. 51
      src/Avalonia.Visuals/Media/BrushExtensions.cs
  23. 0
      src/Avalonia.Visuals/Media/IImageBrush.cs
  24. 12
      src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
  25. 8
      src/Avalonia.Visuals/Properties/AssemblyInfo.cs
  26. 34
      src/Avalonia.Visuals/Rendering/DefaultRenderLayerFactory.cs
  27. 430
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  28. 88
      src/Avalonia.Visuals/Rendering/DirtyRects.cs
  29. 107
      src/Avalonia.Visuals/Rendering/DirtyVisuals.cs
  30. 51
      src/Avalonia.Visuals/Rendering/DisplayDirtyRect.cs
  31. 62
      src/Avalonia.Visuals/Rendering/DisplayDirtyRects.cs
  32. 11
      src/Avalonia.Visuals/Rendering/IRenderLayerFactory.cs
  33. 5
      src/Avalonia.Visuals/Rendering/IRenderRoot.cs
  34. 10
      src/Avalonia.Visuals/Rendering/IRenderer.cs
  35. 18
      src/Avalonia.Visuals/Rendering/IRendererFactory.cs
  36. 17
      src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
  37. 47
      src/Avalonia.Visuals/Rendering/RenderLayer.cs
  38. 63
      src/Avalonia.Visuals/Rendering/RenderLayers.cs
  39. 31
      src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs
  40. 64
      src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs
  41. 392
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  42. 64
      src/Avalonia.Visuals/Rendering/SceneGraph/GeometryClipNode.cs
  43. 101
      src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
  44. 36
      src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs
  45. 24
      src/Avalonia.Visuals/Rendering/SceneGraph/ISceneBuilder.cs
  46. 94
      src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs
  47. 94
      src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs
  48. 95
      src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs
  49. 80
      src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs
  50. 64
      src/Avalonia.Visuals/Rendering/SceneGraph/OpacityNode.cs
  51. 116
      src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
  52. 184
      src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs
  53. 384
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  54. 75
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayer.cs
  55. 199
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs
  56. 96
      src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs
  57. 285
      src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
  58. 11
      src/Avalonia.Visuals/Vector.cs
  59. 24
      src/Avalonia.Visuals/VisualTree/VisualExtensions.cs
  60. 26
      src/Gtk/Avalonia.Cairo/Avalonia.Cairo.v2.ncrunchproject
  61. 1
      src/Gtk/Avalonia.Cairo/CairoPlatform.cs
  62. 5
      src/Gtk/Avalonia.Cairo/Media/RadialGradientBrushImpl.cs
  63. 8
      src/Gtk/Avalonia.Gtk/GtkPlatform.cs
  64. 6
      src/Gtk/Avalonia.Gtk/TopLevelImpl.cs
  65. 8
      src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs
  66. 6
      src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs
  67. 6
      src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
  68. 1
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  69. 1
      src/Shared/SharedAssemblyInfo.cs
  70. 26
      src/Skia/Avalonia.Skia.Android/Avalonia.Skia.Android.v2.ncrunchproject
  71. 26
      src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.v2.ncrunchproject
  72. 172
      src/Skia/Avalonia.Skia.Desktop/RenderTarget.cs
  73. 20
      src/Skia/Avalonia.Skia/BitmapImpl.cs
  74. 19
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  75. 1
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  76. 3
      src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs
  77. 4
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  78. 11
      src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
  79. 2
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  80. 8
      src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs
  81. 6
      src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs
  82. 2
      src/Windows/Avalonia.Win32/FramebufferManager.cs
  83. 28
      src/Windows/Avalonia.Win32/Win32Platform.cs
  84. 10
      src/Windows/Avalonia.Win32/WindowImpl.cs
  85. 8
      src/iOS/Avalonia.iOS/TopLevelImpl.cs
  86. 1
      src/iOS/Avalonia.iOS/iOSPlatform.cs
  87. 1
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs
  88. 1
      tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs
  89. 58
      tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs
  90. 53
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  91. 29
      tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj
  92. 85
      tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs
  93. 14
      tests/Avalonia.LeakTests/ControlTests.cs
  94. 27
      tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v2.ncrunchproject
  95. 26
      tests/Avalonia.RenderTests/Avalonia.Direct2D1.RenderTests.v2.ncrunchproject
  96. 2
      tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems
  97. 26
      tests/Avalonia.RenderTests/Avalonia.Skia.RenderTests.v2.ncrunchproject
  98. 61
      tests/Avalonia.RenderTests/Controls/BorderTests.cs
  99. 162
      tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs
  100. 17
      tests/Avalonia.RenderTests/Controls/ImageTests.cs

5
.ncrunch/Avalonia.LinuxFramebuffer.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
</Settings>
</ProjectConfiguration>

5
.ncrunch/Avalonia.Visuals.UnitTests.net461.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
</Settings>
</ProjectConfiguration>

5
.ncrunch/Avalonia.Win32.Interop.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
</Settings>
</ProjectConfiguration>

1
samples/ControlCatalog/MainWindow.xaml.cs

@ -10,6 +10,7 @@ namespace ControlCatalog
{
this.InitializeComponent();
this.AttachDevTools();
Renderer.DrawDirtyRects = Renderer.DrawFps = true;
}
private void InitializeComponent()

2
samples/interop/Direct3DInteropSample/Program.cs

@ -11,7 +11,7 @@ namespace Direct3DInteropSample
{
static void Main(string[] args)
{
AppBuilder.Configure<App>().UseWin32().UseDirect2D1().Start<MainWindow>();
AppBuilder.Configure<App>().UseWin32(deferredRendering: false).UseDirect2D1().Start<MainWindow>();
}
}
}

2
src/Android/Avalonia.Android/AndroidPlatform.cs

@ -52,13 +52,11 @@ namespace Avalonia.Android
.Bind<IStandardCursorFactory>().ToTransient<CursorFactory>()
.Bind<IKeyboardDevice>().ToSingleton<AndroidKeyboardDevice>()
.Bind<IPlatformSettings>().ToConstant(Instance)
.Bind<IRendererFactory>().ToConstant(ImmediateRenderer.Factory)
.Bind<IPlatformThreadingInterface>().ToConstant(new AndroidThreadingInterface())
.Bind<ISystemDialogImpl>().ToTransient<SystemDialogImpl>()
.Bind<IWindowingPlatform>().ToConstant(Instance)
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoader>()
.Bind<IRenderLoop>().ToConstant(new DefaultRenderLoop(60))
.Bind<IAssetLoader>().ToConstant(new AssetLoader(app.GetType().Assembly));
SkiaPlatform.Initialize();

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

@ -13,6 +13,7 @@ using System.Reactive.Disposables;
using Avalonia.Android.Platform.Input;
using Avalonia.Controls;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Rendering;
namespace Avalonia.Android.Platform.SkiaPlatform
{
@ -85,7 +86,12 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public IPlatformHandle Handle => _view;
public IEnumerable<object> Surfaces => new object[] {this};
public IRenderer CreateRenderer(IRenderRoot root)
{
return new ImmediateRenderer(root);
}
public virtual void Hide()
{
_view.Visibility = ViewStates.Invisible;

7
src/Avalonia.Controls/Platform/ITopLevelImpl.cs

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Rendering;
using JetBrains.Annotations;
namespace Avalonia.Platform
@ -60,6 +61,12 @@ namespace Avalonia.Platform
/// </summary>
Action<double> ScalingChanged { get; set; }
/// <summary>
/// Creates a new renderer for the toplevel.
/// </summary>
/// <param name="root">The toplevel.</param>
IRenderer CreateRenderer(IRenderRoot root);
/// <summary>
/// Invalidates a rect on the toplevel.
/// </summary>

6
src/Avalonia.Controls/TopLevel.cs

@ -90,8 +90,7 @@ namespace Avalonia.Controls
_renderInterface = TryGetService<IPlatformRenderInterface>(dependencyResolver);
var renderLoop = TryGetService<IRenderLoop>(dependencyResolver);
var rendererFactory = TryGetService<IRendererFactory>(dependencyResolver);
Renderer = rendererFactory?.CreateRenderer(this, renderLoop);
Renderer = impl.CreateRenderer(this);
impl.SetInputRoot(this);
@ -181,6 +180,9 @@ namespace Avalonia.Controls
/// <inheritdoc/>
double ILayoutRoot.LayoutScaling => PlatformImpl?.Scaling ?? 1;
/// <inheritdoc/>
double IRenderRoot.RenderScaling => PlatformImpl?.Scaling ?? 1;
IStyleHost IStyleHost.StylingParent
{
get { return AvaloniaLocator.Current.GetService<IGlobalStyles>(); }

9
src/Avalonia.Controls/Window.cs

@ -225,8 +225,14 @@ namespace Avalonia.Controls
/// </summary>
public override void Hide()
{
if (!IsVisible)
{
return;
}
using (BeginAutoSizing())
{
Renderer?.Stop();
PlatformImpl?.Hide();
}
@ -252,6 +258,7 @@ namespace Avalonia.Controls
using (BeginAutoSizing())
{
PlatformImpl?.Show();
Renderer?.Start();
}
}
@ -297,6 +304,8 @@ namespace Avalonia.Controls
var modal = PlatformImpl?.ShowDialog();
var result = new TaskCompletionSource<TResult>();
Renderer?.Start();
Observable.FromEventPattern<EventHandler, EventArgs>(
x => this.Closed += x,
x => this.Closed -= x)

2
src/Avalonia.Controls/WindowBase.cs

@ -117,6 +117,7 @@ namespace Avalonia.Controls
try
{
Renderer?.Stop();
PlatformImpl?.Hide();
IsVisible = false;
}
@ -145,6 +146,7 @@ namespace Avalonia.Controls
}
PlatformImpl?.Show();
Renderer?.Start();
}
finally
{

3
src/Avalonia.Themes.Default/CheckBox.xaml

@ -1,10 +1,11 @@
<Styles xmlns="https://github.com/avaloniaui">
<Style Selector="CheckBox">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StyleResource ThemeBorderMidBrush}"/>
<Setter Property="BorderThickness" Value="{StyleResource ThemeBorderThickness}"/>
<Setter Property="Template">
<ControlTemplate>
<Grid ColumnDefinitions="Auto,*">
<Grid ColumnDefinitions="Auto,*" Background="{TemplateBinding Background}">
<Border Name="border"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"

1
src/Avalonia.Themes.Default/DropDownItem.xaml

@ -1,5 +1,6 @@
<Styles xmlns="https://github.com/avaloniaui">
<Style Selector="DropDownItem">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Padding" Value="2"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>

6
src/Avalonia.Themes.Default/ListBox.xaml

@ -1,13 +1,13 @@
<Style xmlns="https://github.com/avaloniaui" Selector="ListBox">
<Setter Property="Background" Value="{StyleResource ThemeBackgroundBrush}"/>
<Setter Property="BorderBrush" Value="{StyleResource ThemeBorderMidBrush}"/>
<Setter Property="BorderThickness" Value="{StyleResource ThemeBorderThickness}"/>
<Setter Property="Padding" Value="4"/>
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer Name="PART_ScrollViewer">
<ScrollViewer Name="PART_ScrollViewer" Background="{TemplateBinding Background}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"

1
src/Avalonia.Themes.Default/ListBoxItem.xaml

@ -1,5 +1,6 @@
<Styles xmlns="https://github.com/avaloniaui">
<Style Selector="ListBoxItem">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter Name="PART_ContentPresenter"

1
src/Avalonia.Themes.Default/MenuItem.xaml

@ -2,6 +2,7 @@
xmlns:sys="clr-namespace:System;assembly=mscorlib">
<Style Selector="MenuItem">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="6,0"/>
<Setter Property="Template">

3
src/Avalonia.Themes.Default/RadioButton.xaml

@ -1,10 +1,11 @@
<Styles xmlns="https://github.com/avaloniaui">
<Style Selector="RadioButton">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StyleResource ThemeBorderMidBrush}"/>
<Setter Property="BorderThickness" Value="{StyleResource ThemeBorderThickness}"/>
<Setter Property="Template">
<ControlTemplate>
<Grid ColumnDefinitions="Auto,*">
<Grid ColumnDefinitions="Auto,*" Background="{TemplateBinding Background}">
<Ellipse Name="border"
Stroke="{TemplateBinding BorderBrush}"
StrokeThickness="{TemplateBinding BorderThickness}"

3
src/Avalonia.Themes.Default/ScrollViewer.xaml

@ -1,8 +1,9 @@
<Style xmlns="https://github.com/avaloniaui" Selector="ScrollViewer">
<Setter Property="Template">
<ControlTemplate>
<Grid ColumnDefinitions="*,Auto" RowDefinitions="*,Auto" Background="{TemplateBinding Background}">
<Grid ColumnDefinitions="*,Auto" RowDefinitions="*,Auto">
<ScrollContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
Content="{TemplateBinding Content}"
Extent="{TemplateBinding Path=Extent, Mode=TwoWay}"
Margin="{TemplateBinding Padding}"

1
src/Avalonia.Themes.Default/TabStripItem.xaml

@ -1,5 +1,6 @@
<Styles xmlns="https://github.com/avaloniaui">
<Style Selector="TabStripItem">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="FontSize" Value="{StyleResource FontSizeLarge}"/>
<Setter Property="Foreground" Value="{StyleResource ThemeForegroundLightBrush}"/>
<Setter Property="Template">

6
src/Avalonia.Themes.Default/TreeView.xaml

@ -1,13 +1,13 @@
<Style xmlns="https://github.com/avaloniaui" Selector="TreeView">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StyleResource ThemeBorderMidBrush}"/>
<Setter Property="BorderThickness" Value="{StyleResource ThemeBorderThickness}"/>
<Setter Property="Padding" Value="4"/>
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer CanScrollHorizontally="True">
<ScrollViewer CanScrollHorizontally="True" Background="{TemplateBinding Background}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"

3
src/Avalonia.Themes.Default/TreeViewItem.xaml

@ -31,7 +31,8 @@
<Style Selector="TreeViewItem /template/ ToggleButton#expander">
<Setter Property="Template">
<ControlTemplate>
<Border Width="14"
<Border Background="Transparent"
Width="14"
Height="12"
HorizontalAlignment="Center"
VerticalAlignment="Center">

51
src/Avalonia.Visuals/Media/BrushExtensions.cs

@ -0,0 +1,51 @@
using System;
namespace Avalonia.Media
{
/// <summary>
/// Extension methods for brush classes.
/// </summary>
public static class BrushExtensions
{
/// <summary>
/// Converts a brush to an immutable brush.
/// </summary>
/// <param name="brush">The brush.</param>
/// <returns>
/// The result of calling <see cref="IMutableBrush.ToImmutable"/> if the brush is mutable,
/// otherwise <paramref name="brush"/>.
/// </returns>
public static IBrush ToImmutable(this IBrush brush)
{
Contract.Requires<ArgumentNullException>(brush != null);
return (brush as IMutableBrush)?.ToImmutable() ?? brush;
}
/// <summary>
/// Converts a pen to a pen with an immutable brush
/// </summary>
/// <param name="pen">The pen.</param>
/// <returns>
/// A copy of the pen with an immutable brush, or <paramref name="pen"/> if the pen's brush
/// is already immutable or null.
/// </returns>
public static Pen ToImmutable(this Pen pen)
{
Contract.Requires<ArgumentNullException>(pen != null);
var brush = pen?.Brush?.ToImmutable();
return pen == null || ReferenceEquals(pen?.Brush, brush) ?
pen :
new Pen(
brush,
thickness: pen.Thickness,
dashStyle: pen.DashStyle,
dashCap: pen.DashCap,
startLineCap: pen.StartLineCap,
endLineCap: pen.EndLineCap,
lineJoin: pen.LineJoin,
miterLimit: pen.MiterLimit);
}
}
}

0
src/Avalonia.Visuals/Media/Imaging/IImageBrush.cs → src/Avalonia.Visuals/Media/IImageBrush.cs

12
src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs

@ -3,7 +3,6 @@
using System;
using Avalonia.Media;
using Avalonia.Platform;
namespace Avalonia.Platform
{
@ -32,6 +31,15 @@ namespace Avalonia.Platform
/// <param name="destRect">The rect in the output to draw to.</param>
void DrawImage(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect);
/// <summary>
/// Draws a bitmap image.
/// </summary>
/// <param name="source">The bitmap image.</param>
/// <param name="opacityMask">The opacity mask to draw with.</param>
/// <param name="opacityMaskRect">The destination rect for the opacity mask.</param>
/// <param name="destRect">The rect in the output to draw to.</param>
void DrawImage(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect);
/// <summary>
/// Draws a line.
/// </summary>
@ -100,4 +108,4 @@ namespace Avalonia.Platform
void PopGeometryClip();
}
}
}

8
src/Avalonia.Visuals/Properties/AssemblyInfo.cs

@ -2,8 +2,14 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Reflection;
using System.Runtime.CompilerServices;
using Avalonia.Metadata;
[assembly: AssemblyTitle("Avalonia.Visuals")]
[assembly: InternalsVisibleTo("Avalonia.Visuals.UnitTests")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media")]
[assembly: InternalsVisibleTo("Avalonia.Cairo.RenderTests")]
[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")]
[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests")]

34
src/Avalonia.Visuals/Rendering/DefaultRenderLayerFactory.cs

@ -0,0 +1,34 @@
using System;
using Avalonia.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Rendering
{
public class DefaultRenderLayerFactory : IRenderLayerFactory
{
private IPlatformRenderInterface _renderInterface;
public DefaultRenderLayerFactory()
: this(AvaloniaLocator.Current.GetService<IPlatformRenderInterface>())
{
}
public DefaultRenderLayerFactory(IPlatformRenderInterface renderInterface)
{
_renderInterface = renderInterface;
}
public IRenderTargetBitmapImpl CreateLayer(
IVisual layerRoot,
Size size,
double dpiX,
double dpiY)
{
return _renderInterface.CreateRenderTargetBitmap(
(int)Math.Ceiling(size.Width),
(int)Math.Ceiling(size.Height),
dpiX,
dpiY);
}
}
}

430
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@ -0,0 +1,430 @@
// 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;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Threading;
using Avalonia.VisualTree;
using System.Collections.Generic;
using System.IO;
using Avalonia.Media.Immutable;
using System.Threading;
namespace Avalonia.Rendering
{
/// <summary>
/// A renderer which renders the state of the visual tree to an intermediate scene graph
/// representation which is then rendered on a rendering thread.
/// </summary>
public class DeferredRenderer : RendererBase, IRenderer, IVisualBrushRenderer
{
private readonly IDispatcher _dispatcher;
private readonly IRenderLoop _renderLoop;
private readonly IVisual _root;
private readonly ISceneBuilder _sceneBuilder;
private readonly RenderLayers _layers;
private readonly IRenderLayerFactory _layerFactory;
private bool _running;
private Scene _scene;
private IRenderTarget _renderTarget;
private DirtyVisuals _dirty;
private IRenderTargetBitmapImpl _overlay;
private bool _updateQueued;
private object _rendering = new object();
private int _lastSceneId = -1;
private DisplayDirtyRects _dirtyRectsDisplay = new DisplayDirtyRects();
private IDrawOperation _currentDraw;
/// <summary>
/// Initializes a new instance of the <see cref="DeferredRenderer"/> class.
/// </summary>
/// <param name="root">The control to render.</param>
/// <param name="renderLoop">The render loop.</param>
/// <param name="sceneBuilder">The scene builder to use. Optional.</param>
/// <param name="layerFactory">The layer factory to use. Optional.</param>
/// <param name="dispatcher">The dispatcher to use. Optional.</param>
public DeferredRenderer(
IRenderRoot root,
IRenderLoop renderLoop,
ISceneBuilder sceneBuilder = null,
IRenderLayerFactory layerFactory = null,
IDispatcher dispatcher = null)
{
Contract.Requires<ArgumentNullException>(root != null);
_dispatcher = dispatcher ?? Dispatcher.UIThread;
_root = root;
_sceneBuilder = sceneBuilder ?? new SceneBuilder();
_scene = new Scene(root);
_layerFactory = layerFactory ?? new DefaultRenderLayerFactory();
_layers = new RenderLayers(_layerFactory);
_renderLoop = renderLoop;
}
/// <summary>
/// Initializes a new instance of the <see cref="DeferredRenderer"/> class.
/// </summary>
/// <param name="root">The control to render.</param>
/// <param name="renderTarget">The render target.</param>
/// <param name="sceneBuilder">The scene builder to use. Optional.</param>
/// <param name="layerFactory">The layer factory to use. Optional.</param>
/// <remarks>
/// This constructor is intended to be used for unit testing.
/// </remarks>
public DeferredRenderer(
IVisual root,
IRenderTarget renderTarget,
ISceneBuilder sceneBuilder = null,
IRenderLayerFactory layerFactory = null)
{
Contract.Requires<ArgumentNullException>(root != null);
Contract.Requires<ArgumentNullException>(renderTarget != null);
_root = root;
_renderTarget = renderTarget;
_sceneBuilder = sceneBuilder ?? new SceneBuilder();
_scene = new Scene(root);
_layerFactory = layerFactory ?? new DefaultRenderLayerFactory();
_layers = new RenderLayers(_layerFactory);
}
/// <inheritdoc/>
public bool DrawFps { get; set; }
/// <inheritdoc/>
public bool DrawDirtyRects { get; set; }
/// <summary>
/// Gets or sets a path to which rendered frame should be rendered for debugging.
/// </summary>
public string DebugFramesPath { get; set; }
/// <inheritdoc/>
public void AddDirty(IVisual visual)
{
_dirty?.Add(visual);
}
/// <summary>
/// Disposes of the renderer and detaches from the render loop.
/// </summary>
public void Dispose() => Stop();
/// <inheritdoc/>
public IEnumerable<IVisual> HitTest(Point p, Func<IVisual, bool> filter)
{
if (_renderLoop == null && (_dirty == null || _dirty.Count > 0))
{
// When unit testing the renderLoop may be null, so update the scene manually.
UpdateScene();
}
return _scene.HitTest(p, filter);
}
/// <inheritdoc/>
public void Paint(Rect rect)
{
}
/// <inheritdoc/>
public void Resized(Size size)
{
}
/// <inheritdoc/>
public void Start()
{
if (!_running && _renderLoop != null)
{
_renderLoop.Tick += OnRenderLoopTick;
_running = true;
}
}
/// <inheritdoc/>
public void Stop()
{
if (_running && _renderLoop != null)
{
_renderLoop.Tick -= OnRenderLoopTick;
_running = false;
}
}
/// <inheritdoc/>
Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
{
return (_currentDraw as BrushDrawOperation)?.ChildScenes?[brush.Visual]?.Size ?? Size.Empty;
}
/// <inheritdoc/>
void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush)
{
var childScene = (_currentDraw as BrushDrawOperation)?.ChildScenes?[brush.Visual];
if (childScene != null)
{
Render(context, (VisualNode)childScene.Root, null, new Rect(childScene.Size));
}
}
internal void UnitTestUpdateScene() => UpdateScene();
internal void UnitTestRender() => Render(_scene);
private void Render(Scene scene)
{
_dirtyRectsDisplay.Tick();
if (scene.Size != Size.Empty)
{
if (scene.Generation != _lastSceneId)
{
_layers.Update(scene);
RenderToLayers(scene);
if (DebugFramesPath != null)
{
SaveDebugFrames(scene.Generation);
}
_lastSceneId = scene.Generation;
}
RenderOverlay(scene);
RenderComposite(scene);
}
}
private void Render(IDrawingContextImpl context, VisualNode node, IVisual layer, Rect clipBounds)
{
if (layer == null || node.LayerRoot == layer)
{
clipBounds = node.ClipBounds.Intersect(clipBounds);
if (!clipBounds.IsEmpty)
{
node.BeginRender(context);
foreach (var operation in node.DrawOperations)
{
_currentDraw = operation;
operation.Render(context);
_currentDraw = null;
}
foreach (var child in node.Children)
{
Render(context, (VisualNode)child, layer, clipBounds);
}
node.EndRender(context);
}
}
}
private void RenderToLayers(Scene scene)
{
if (scene.Layers.HasDirty)
{
foreach (var layer in scene.Layers)
{
var renderTarget = _layers[layer.LayerRoot].Bitmap;
var node = (VisualNode)scene.FindNode(layer.LayerRoot);
if (node != null)
{
using (var context = renderTarget.CreateDrawingContext(this))
{
foreach (var rect in layer.Dirty)
{
context.Transform = Matrix.Identity;
context.PushClip(rect);
context.Clear(Colors.Transparent);
Render(context, node, layer.LayerRoot, rect);
context.PopClip();
if (DrawDirtyRects)
{
_dirtyRectsDisplay.Add(rect);
}
}
}
}
}
}
}
private void RenderOverlay(Scene scene)
{
if (DrawDirtyRects)
{
var overlay = GetOverlay(scene.Size, scene.Scaling);
using (var context = overlay.CreateDrawingContext(this))
{
context.Clear(Colors.Transparent);
RenderDirtyRects(context);
}
}
else
{
_overlay?.Dispose();
_overlay = null;
}
}
private void RenderDirtyRects(IDrawingContextImpl context)
{
foreach (var r in _dirtyRectsDisplay)
{
var brush = new ImmutableSolidColorBrush(Colors.Magenta, r.Opacity);
context.FillRectangle(brush, r.Rect);
}
}
private void RenderComposite(Scene scene)
{
try
{
if (_renderTarget == null)
{
_renderTarget = ((IRenderRoot)_root).CreateRenderTarget();
}
using (var context = _renderTarget.CreateDrawingContext(this))
{
var clientRect = new Rect(scene.Size);
foreach (var layer in scene.Layers)
{
var bitmap = _layers[layer.LayerRoot].Bitmap;
var sourceRect = new Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight);
if (layer.GeometryClip != null)
{
context.PushGeometryClip(layer.GeometryClip);
}
if (layer.OpacityMask == null)
{
context.DrawImage(bitmap, layer.Opacity, sourceRect, clientRect);
}
else
{
context.DrawImage(bitmap, layer.OpacityMask, layer.OpacityMaskRect, sourceRect);
}
if (layer.GeometryClip != null)
{
context.PopGeometryClip();
}
}
if (_overlay != null)
{
var sourceRect = new Rect(0, 0, _overlay.PixelWidth, _overlay.PixelHeight);
context.DrawImage(_overlay, 0.5, sourceRect, clientRect);
}
if (DrawFps)
{
RenderFps(context, clientRect, true);
}
}
}
catch (RenderTargetCorruptedException ex)
{
Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex);
_renderTarget?.Dispose();
_renderTarget = null;
}
}
private void UpdateScene()
{
Dispatcher.UIThread.VerifyAccess();
try
{
var scene = _scene.Clone();
if (_dirty == null)
{
_dirty = new DirtyVisuals();
_sceneBuilder.UpdateAll(scene);
}
else if (_dirty.Count > 0)
{
foreach (var visual in _dirty)
{
_sceneBuilder.Update(scene, visual);
}
}
Interlocked.Exchange(ref _scene, scene);
_dirty.Clear();
(_root as IRenderRoot)?.Invalidate(new Rect(scene.Size));
}
finally
{
_updateQueued = false;
}
}
private void OnRenderLoopTick(object sender, EventArgs e)
{
if (Monitor.TryEnter(_rendering))
{
try
{
if (!_updateQueued && (_dirty == null || _dirty.Count > 0))
{
_updateQueued = true;
_dispatcher.InvokeAsync(UpdateScene, DispatcherPriority.Render);
}
Scene scene = null;
Interlocked.Exchange(ref scene, _scene);
Render(scene);
}
catch { }
finally
{
Monitor.Exit(_rendering);
}
}
}
private IRenderTargetBitmapImpl GetOverlay(Size size, double scaling)
{
size = new Size(size.Width * scaling, size.Height * scaling);
if (_overlay == null ||
_overlay.PixelWidth != size.Width ||
_overlay.PixelHeight != size.Height)
{
_overlay?.Dispose();
_overlay = _layerFactory.CreateLayer(null, size, 96 * scaling, 96 * scaling);
}
return _overlay;
}
private void SaveDebugFrames(int id)
{
var index = 0;
foreach (var layer in _layers)
{
var fileName = Path.Combine(DebugFramesPath, $"frame-{id}-layer-{index++}.png");
layer.Bitmap.Save(fileName);
}
}
}
}

88
src/Avalonia.Visuals/Rendering/DirtyRects.cs

@ -0,0 +1,88 @@
// 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;
using System.Collections.Generic;
namespace Avalonia.Rendering
{
/// <summary>
/// Tracks dirty rectangles.
/// </summary>
internal class DirtyRects : IEnumerable<Rect>
{
private List<Rect> _rects = new List<Rect>();
public bool IsEmpty => _rects.Count == 0;
/// <summary>
/// Adds a dirty rectangle, extending an existing dirty rectangle if it intersects.
/// </summary>
/// <param name="rect">The dirt rectangle.</param>
/// <remarks>
/// We probably want to do this more intellegently because:
/// - Adding e.g. the top left quarter of a scene and the bottom left quarter of a scene
/// will cause the whole scene to be invalidated if they overlap by a single pixel
/// - Adding two adjacent rectangles that don't overlap will not cause them to be
/// coalesced
/// - It only coaleces the first intersecting rectangle found - one needs to
/// call <see cref="Coalesce"/> at the end of the draw cycle to coalesce the rest.
/// </remarks>
public void Add(Rect rect)
{
if (!rect.IsEmpty)
{
for (var i = 0; i < _rects.Count; ++i)
{
var r = _rects[i];
if (r.Intersects(rect))
{
_rects[i] = r.Union(rect);
return;
}
}
_rects.Add(rect);
}
}
/// <summary>
/// Works around our flimsy dirt-rect coalescing algorithm.
/// </summary>
/// <remarks>
/// See the comments in <see cref="Add(Rect)"/>.
/// </remarks>
public void Coalesce()
{
for (var i = _rects.Count - 1; i >= 0; --i)
{
var a = _rects[i];
for (var j = 0; j < i; ++j)
{
var b = _rects[j];
if (i < _rects.Count && a.Intersects(b))
{
_rects[i] = _rects[i].Union(b);
_rects.RemoveAt(i);
}
}
}
}
/// <summary>
/// Gets the dirty rectangles.
/// </summary>
/// <returns>A collection of dirty rectangles</returns>
public IEnumerator<Rect> GetEnumerator() => _rects.GetEnumerator();
/// <summary>
/// Gets the dirty rectangles.
/// </summary>
/// <returns>A collection of dirty rectangles</returns>
IEnumerator IEnumerable.GetEnumerator() => _rects.GetEnumerator();
}
}

107
src/Avalonia.Visuals/Rendering/DirtyVisuals.cs

@ -0,0 +1,107 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Avalonia.VisualTree;
namespace Avalonia.Rendering
{
/// <summary>
/// Stores a list of dirty visuals for an <see cref="IRenderer"/>.
/// </summary>
/// <remarks>
/// This class stores the dirty visuals for a scene, ordered by their distance to the root
/// visual. TODO: We probably want to put an upper limit on the number of visuals that can be
/// stored and if we reach that limit, assume all visuals are dirty.
/// </remarks>
internal class DirtyVisuals : IEnumerable<IVisual>
{
private SortedDictionary<int, List<IVisual>> _inner = new SortedDictionary<int, List<IVisual>>();
private Dictionary<IVisual, int> _index = new Dictionary<IVisual, int>();
/// <summary>
/// Gets the number of dirty visuals.
/// </summary>
public int Count => _index.Count;
/// <summary>
/// Adds a visual to the dirty list.
/// </summary>
/// <param name="visual">The dirty visual.</param>
public void Add(IVisual visual)
{
var distance = visual.CalculateDistanceFromAncestor(visual.VisualRoot);
int existingDistance;
if (_index.TryGetValue(visual, out existingDistance))
{
if (distance == existingDistance)
{
return;
}
_inner[existingDistance].Remove(visual);
_index.Remove(visual);
}
List<IVisual> list;
if (!_inner.TryGetValue(distance, out list))
{
list = new List<IVisual>();
_inner.Add(distance, list);
}
list.Add(visual);
_index.Add(visual, distance);
}
/// <summary>
/// Clears the list.
/// </summary>
public void Clear()
{
_inner.Clear();
_index.Clear();
}
/// <summary>
/// Removes a visual from the dirty list.
/// </summary>
/// <param name="visual">The visual.</param>
/// <returns>True if the visual was present in the list; otherwise false.</returns>
public bool Remove(IVisual visual)
{
int distance;
if (_index.TryGetValue(visual, out distance))
{
_inner[distance].Remove(visual);
_index.Remove(visual);
return true;
}
return false;
}
/// <summary>
/// Gets the dirty visuals, in ascending order of distance to their root.
/// </summary>
/// <returns>A collection of visuals.</returns>
public IEnumerator<IVisual> GetEnumerator()
{
foreach (var i in _inner)
{
foreach (var j in i.Value)
{
yield return j;
}
}
}
/// <summary>
/// Gets the dirty visuals, in ascending order of distance to their root.
/// </summary>
/// <returns>A collection of visuals.</returns>
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

51
src/Avalonia.Visuals/Rendering/DisplayDirtyRect.cs

@ -0,0 +1,51 @@
using System;
namespace Avalonia.Rendering
{
/// <summary>
/// Holds the state for a dirty rect rendered when <see cref="IRenderer.DrawDirtyRects"/> is set.
/// </summary>
internal class DisplayDirtyRect
{
public static readonly TimeSpan TimeToLive = TimeSpan.FromMilliseconds(250);
/// <summary>
/// Initializes a new instance of the <see cref="DisplayDirtyRect"/> class.
/// </summary>
/// <param name="rect">The dirt rect.</param>
public DisplayDirtyRect(Rect rect)
{
Rect = rect;
ResetLifetime();
}
/// <summary>
/// Gets the bounds of the dirty rectangle.
/// </summary>
public Rect Rect { get; }
/// <summary>
/// Gets the time at which the rectangle was made dirty.
/// </summary>
public DateTimeOffset Born { get; private set; }
/// <summary>
/// Gets the time at which the rectagle should no longer be displayed.
/// </summary>
public DateTimeOffset Dies { get; private set; }
/// <summary>
/// Gets the opacity at which to display the dirty rectangle.
/// </summary>
public double Opacity => (Dies - DateTimeOffset.UtcNow).TotalMilliseconds / TimeToLive.TotalMilliseconds;
/// <summary>
/// Resets the rectangle's lifetime.
/// </summary>
public void ResetLifetime()
{
Born = DateTimeOffset.UtcNow;
Dies = Born + TimeToLive;
}
}
}

62
src/Avalonia.Visuals/Rendering/DisplayDirtyRects.cs

@ -0,0 +1,62 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace Avalonia.Rendering
{
/// <summary>
/// Holds a collection of <see cref="DisplayDirtyRect"/> objects and manages their aging.
/// </summary>
internal class DisplayDirtyRects : IEnumerable<DisplayDirtyRect>
{
private List<DisplayDirtyRect> _inner = new List<DisplayDirtyRect>();
/// <summary>
/// Adds new new dirty rect to the collection.
/// </summary>
/// <param name="rect"></param>
public void Add(Rect rect)
{
foreach (var r in _inner)
{
if (r.Rect == rect)
{
r.ResetLifetime();
return;
}
}
_inner.Add(new DisplayDirtyRect(rect));
}
/// <summary>
/// Removes dirty rects one they are no longer active.
/// </summary>
public void Tick()
{
var now = DateTimeOffset.UtcNow;
for (var i = _inner.Count - 1; i >= 0; --i)
{
var r = _inner[i];
if (now > r.Dies)
{
_inner.RemoveAt(i);
}
}
}
/// <summary>
/// Gets the dirty rects.
/// </summary>
/// <returns>A collection of <see cref="DisplayDirtyRect"/> objects.</returns>
public IEnumerator<DisplayDirtyRect> GetEnumerator() => _inner.GetEnumerator();
/// <summary>
/// Gets the dirty rects.
/// </summary>
/// <returns>A collection of <see cref="DisplayDirtyRect"/> objects.</returns>
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

11
src/Avalonia.Visuals/Rendering/IRenderLayerFactory.cs

@ -0,0 +1,11 @@
using System;
using Avalonia.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Rendering
{
public interface IRenderLayerFactory
{
IRenderTargetBitmapImpl CreateLayer(IVisual layerRoot, Size size, double dpiX, double dpiY);
}
}

5
src/Avalonia.Visuals/Rendering/IRenderRoot.cs

@ -22,6 +22,11 @@ namespace Avalonia.Rendering
/// </summary>
IRenderer Renderer { get; }
/// <summary>
/// The scaling factor to use in rendering.
/// </summary>
double RenderScaling { get; }
/// <summary>
/// Creates a render target for the window.
/// </summary>

10
src/Avalonia.Visuals/Rendering/IRenderer.cs

@ -48,5 +48,15 @@ namespace Avalonia.Rendering
/// </summary>
/// <param name="rect">The dirty rectangle.</param>
void Paint(Rect rect);
/// <summary>
/// Starts the renderer.
/// </summary>
void Start();
/// <summary>
/// Stops the renderer.
/// </summary>
void Stop();
}
}

18
src/Avalonia.Visuals/Rendering/IRendererFactory.cs

@ -1,18 +0,0 @@
using System;
namespace Avalonia.Rendering
{
/// <summary>
/// Defines a factory for creating <see cref="IRenderer"/> instances.
/// </summary>
public interface IRendererFactory
{
/// <summary>
/// Creates a new renderer for the specified render root.
/// </summary>
/// <param name="root">The render root.</param>
/// <param name="renderLoop">The render loop.</param>
/// <returns>An instance of an <see cref="IRenderer"/>.</returns>
IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop);
}
}

17
src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs

@ -20,11 +20,6 @@ namespace Avalonia.Rendering
/// </remarks>
public class ImmediateRenderer : RendererBase, IRenderer, IVisualBrushRenderer
{
class ImmediateRendererFactory : IRendererFactory
{
public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop) => new ImmediateRenderer(root);
}
private readonly IVisual _root;
private readonly IRenderRoot _renderRoot;
private IRenderTarget _renderTarget;
@ -41,8 +36,6 @@ namespace Avalonia.Rendering
_renderRoot = root as IRenderRoot;
}
public static IRendererFactory Factory { get; } = new ImmediateRendererFactory();
/// <inheritdoc/>
public bool DrawFps { get; set; }
@ -148,6 +141,16 @@ namespace Avalonia.Rendering
return HitTest(_root, p, filter);
}
/// <inheritdoc/>
public void Start()
{
}
/// <inheritdoc/>
public void Stop()
{
}
/// <inheritdoc/>
Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
{

47
src/Avalonia.Visuals/Rendering/RenderLayer.cs

@ -0,0 +1,47 @@
using System;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Rendering
{
public class RenderLayer
{
private readonly IRenderLayerFactory _factory;
public RenderLayer(
IRenderLayerFactory factory,
Size size,
double scaling,
IVisual layerRoot)
{
_factory = factory;
Bitmap = factory.CreateLayer(layerRoot, size * scaling, 96 * scaling, 96 * scaling);
Size = size;
Scaling = scaling;
LayerRoot = layerRoot;
}
public IRenderTargetBitmapImpl Bitmap { get; private set; }
public double Scaling { get; private set; }
public Size Size { get; private set; }
public IVisual LayerRoot { get; }
public void ResizeBitmap(Size size, double scaling)
{
if (Size != size || Scaling != scaling)
{
var resized = _factory.CreateLayer(LayerRoot, size * scaling, 96 * scaling, 96 * scaling);
using (var context = resized.CreateDrawingContext(null))
{
context.Clear(Colors.Transparent);
context.DrawImage(Bitmap, 1, new Rect(Size), new Rect(Size));
Bitmap.Dispose();
Bitmap = resized;
Size = size;
}
}
}
}
}

63
src/Avalonia.Visuals/Rendering/RenderLayers.cs

@ -0,0 +1,63 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Avalonia.Rendering.SceneGraph;
using Avalonia.VisualTree;
namespace Avalonia.Rendering
{
public class RenderLayers : IEnumerable<RenderLayer>
{
private readonly IRenderLayerFactory _factory;
private List<RenderLayer> _inner = new List<RenderLayer>();
private Dictionary<IVisual, RenderLayer> _index = new Dictionary<IVisual, RenderLayer>();
public RenderLayers(IRenderLayerFactory factory)
{
_factory = factory;
}
public int Count => _inner.Count;
public RenderLayer this[IVisual layerRoot] => _index[layerRoot];
public void Update(Scene scene)
{
for (var i = scene.Layers.Count - 1; i >= 0; --i)
{
var src = scene.Layers[i];
RenderLayer layer;
if (!_index.TryGetValue(src.LayerRoot, out layer))
{
layer = new RenderLayer(_factory, scene.Size, scene.Scaling, src.LayerRoot);
_inner.Add(layer);
_index.Add(src.LayerRoot, layer);
}
else
{
layer.ResizeBitmap(scene.Size, scene.Scaling);
}
}
for (var i = _inner.Count - 1; i >= 0; --i)
{
var layer = _inner[i];
if (!scene.Layers.Exists(layer.LayerRoot))
{
layer.Bitmap.Dispose();
_inner.RemoveAt(i);
_index.Remove(layer.LayerRoot);
}
}
}
public bool TryGetValue(IVisual layerRoot, out RenderLayer value)
{
return _index.TryGetValue(layerRoot, out value);
}
public IEnumerator<RenderLayer> GetEnumerator() => _inner.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

31
src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs

@ -0,0 +1,31 @@
// 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.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// Base class for draw operations that can use a brush.
/// </summary>
internal abstract class BrushDrawOperation : IDrawOperation
{
/// <inheritdoc/>
public abstract Rect Bounds { get; }
/// <inheritdoc/>
public abstract bool HitTest(Point p);
/// <summary>
/// Gets a collection of child scenes that are needed to draw visual brushes.
/// </summary>
public abstract IDictionary<IVisual, Scene> ChildScenes { get; }
/// <inheritdoc/>
public abstract void Render(IDrawingContextImpl context);
}
}

64
src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs

@ -0,0 +1,64 @@
using System;
using Avalonia.Platform;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// A node in the scene graph which represents a clip push or pop.
/// </summary>
internal class ClipNode : IDrawOperation
{
/// <summary>
/// Initializes a new instance of the <see cref="ClipNode"/> class that represents a
/// clip push.
/// </summary>
/// <param name="clip">The clip to push.</param>
public ClipNode(Rect clip)
{
Clip = clip;
}
/// <summary>
/// Initializes a new instance of the <see cref="ClipNode"/> class that represents a
/// clip pop.
/// </summary>
public ClipNode()
{
}
/// <inheritdoc/>
public Rect Bounds => Rect.Empty;
/// <summary>
/// Gets the clip to be pushed or null if the operation represents a pop.
/// </summary>
public Rect? Clip { get; }
/// <inheritdoc/>
public bool HitTest(Point p) => false;
/// <summary>
/// Determines if this draw operation equals another.
/// </summary>
/// <param name="clip">The clip of the other draw operation.</param>
/// <returns>True if the draw operations are the same, otherwise false.</returns>
/// <remarks>
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
/// </remarks>
public bool Equals(Rect? clip) => Clip == clip;
/// <inheritdoc/>
public void Render(IDrawingContextImpl context)
{
if (Clip.HasValue)
{
context.PushClip(Clip.Value);
}
else
{
context.PopClip();
}
}
}
}

392
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@ -0,0 +1,392 @@
// 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.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// A drawing context which builds a scene graph.
/// </summary>
internal class DeferredDrawingContextImpl : IDrawingContextImpl
{
private readonly ISceneBuilder _sceneBuilder;
private VisualNode _node;
private int _childIndex;
private int _drawOperationindex;
/// <summary>
/// Initializes a new instance of the <see cref="DeferredDrawingContextImpl"/> class.
/// </summary>
/// <param name="sceneBuilder">
/// A scene builder used for constructing child scenes for visual brushes.
/// </param>
/// <param name="layers">The scene layers.</param>
public DeferredDrawingContextImpl(ISceneBuilder sceneBuilder, SceneLayers layers)
{
_sceneBuilder = sceneBuilder;
Layers = layers;
}
/// <inheritdoc/>
public Matrix Transform { get; set; } = Matrix.Identity;
/// <summary>
/// Gets the layers in the scene being built.
/// </summary>
public SceneLayers Layers { get; }
/// <summary>
/// Informs the drawing context of the visual node that is about to be rendered.
/// </summary>
/// <param name="node">The visual node.</param>
/// <returns>
/// An object which when disposed will commit the changes to visual node.
/// </returns>
public UpdateState BeginUpdate(VisualNode node)
{
Contract.Requires<ArgumentNullException>(node != null);
if (_node != null)
{
if (_childIndex < _node.Children.Count)
{
_node.ReplaceChild(_childIndex, node);
}
else
{
_node.AddChild(node);
}
++_childIndex;
}
var state = new UpdateState(this, _node, _childIndex, _drawOperationindex);
_node = node;
_childIndex = _drawOperationindex = 0;
return state;
}
/// <inheritdoc/>
public void Clear(Color color)
{
// Cannot clear a deferred scene.
}
/// <inheritdoc/>
public void Dispose()
{
// Nothing to do here as we allocate no unmanaged resources.
}
/// <summary>
/// Removes any remaining drawing operations from the visual node.
/// </summary>
/// <remarks>
/// Drawing operations are updated in place, overwriting existing drawing operations if
/// they are different. Once drawing has completed for the current visual node, it is
/// possible that there are stale drawing operations at the end of the list. This method
/// trims these stale drawing operations.
/// </remarks>
public void TrimChildren()
{
_node.TrimChildren(_childIndex);
}
/// <inheritdoc/>
public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry)
{
var next = NextDrawAs<GeometryNode>();
if (next == null || !next.Equals(Transform, brush, pen, geometry))
{
Add(new GeometryNode(Transform, brush, pen, geometry, CreateChildScene(brush)));
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void DrawImage(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect)
{
var next = NextDrawAs<ImageNode>();
if (next == null || !next.Equals(Transform, source, opacity, sourceRect, destRect))
{
Add(new ImageNode(Transform, source, opacity, sourceRect, destRect));
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void DrawImage(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect sourceRect)
{
// This method is currently only used to composite layers so shouldn't be called here.
throw new NotSupportedException();
}
/// <inheritdoc/>
public void DrawLine(Pen pen, Point p1, Point p2)
{
var next = NextDrawAs<LineNode>();
if (next == null || !next.Equals(Transform, pen, p1, p2))
{
Add(new LineNode(Transform, pen, p1, p2, CreateChildScene(pen.Brush)));
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0)
{
var next = NextDrawAs<RectangleNode>();
if (next == null || !next.Equals(Transform, null, pen, rect, cornerRadius))
{
Add(new RectangleNode(Transform, null, pen, rect, cornerRadius, CreateChildScene(pen.Brush)));
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
{
var next = NextDrawAs<TextNode>();
if (next == null || !next.Equals(Transform, foreground, origin, text))
{
Add(new TextNode(Transform, foreground, origin, text, CreateChildScene(foreground)));
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0)
{
var next = NextDrawAs<RectangleNode>();
if (next == null || !next.Equals(Transform, brush, null, rect, cornerRadius))
{
Add(new RectangleNode(Transform, brush, null, rect, cornerRadius, CreateChildScene(brush)));
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void PopClip()
{
var next = NextDrawAs<ClipNode>();
if (next == null || !next.Equals(null))
{
Add(new ClipNode());
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void PopGeometryClip()
{
var next = NextDrawAs<GeometryClipNode>();
if (next == null || !next.Equals(null))
{
Add(new GeometryClipNode());
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void PopOpacity()
{
var next = NextDrawAs<OpacityNode>();
if (next == null || !next.Equals(null))
{
Add(new OpacityNode());
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void PopOpacityMask()
{
var next = NextDrawAs<OpacityMaskNode>();
if (next == null || !next.Equals(null, null))
{
Add(new OpacityMaskNode());
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void PushClip(Rect clip)
{
var next = NextDrawAs<ClipNode>();
if (next == null || !next.Equals(clip))
{
Add(new ClipNode(clip));
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void PushGeometryClip(IGeometryImpl clip)
{
var next = NextDrawAs<GeometryClipNode>();
if (next == null || !next.Equals(clip))
{
Add(new GeometryClipNode(clip));
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void PushOpacity(double opacity)
{
var next = NextDrawAs<OpacityNode>();
if (next == null || !next.Equals(opacity))
{
Add(new OpacityNode(opacity));
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void PushOpacityMask(IBrush mask, Rect bounds)
{
var next = NextDrawAs<OpacityMaskNode>();
if (next == null || !next.Equals(mask, bounds))
{
Add(new OpacityMaskNode(mask, bounds, CreateChildScene(mask)));
}
else
{
++_drawOperationindex;
}
}
public struct UpdateState : IDisposable
{
public UpdateState(
DeferredDrawingContextImpl owner,
VisualNode node,
int childIndex,
int drawOperationIndex)
{
Owner = owner;
Node = node;
ChildIndex = childIndex;
DrawOperationIndex = drawOperationIndex;
}
public void Dispose()
{
Owner._node.TrimDrawOperations(Owner._drawOperationindex);
var dirty = Owner.Layers.GetOrAdd(Owner._node.LayerRoot).Dirty;
foreach (var operation in Owner._node.DrawOperations)
{
dirty.Add(operation.Bounds);
}
Owner._node = Node;
Owner._childIndex = ChildIndex;
Owner._drawOperationindex = DrawOperationIndex;
}
public DeferredDrawingContextImpl Owner { get; }
public VisualNode Node { get; }
public int ChildIndex { get; }
public int DrawOperationIndex { get; }
}
private void Add(IDrawOperation node)
{
if (_drawOperationindex < _node.DrawOperations.Count)
{
_node.ReplaceDrawOperation(_drawOperationindex, node);
}
else
{
_node.AddDrawOperation(node);
}
++_drawOperationindex;
}
private T NextDrawAs<T>() where T : class, IDrawOperation
{
return _drawOperationindex < _node.DrawOperations.Count ? _node.DrawOperations[_drawOperationindex] as T : null;
}
private IDictionary<IVisual, Scene> CreateChildScene(IBrush brush)
{
var visualBrush = brush as VisualBrush;
if (visualBrush != null)
{
var visual = visualBrush.Visual;
if (visual != null)
{
(visual as IVisualBrushInitialize)?.EnsureInitialized();
var scene = new Scene(visual);
_sceneBuilder.UpdateAll(scene);
return new Dictionary<IVisual, Scene> { { visualBrush.Visual, scene } };
}
}
return null;
}
}
}

64
src/Avalonia.Visuals/Rendering/SceneGraph/GeometryClipNode.cs

@ -0,0 +1,64 @@
using System;
using Avalonia.Platform;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// A node in the scene graph which represents a geometry clip push or pop.
/// </summary>
internal class GeometryClipNode : IDrawOperation
{
/// <summary>
/// Initializes a new instance of the <see cref="GeometryClipNode"/> class that represents a
/// geometry clip push.
/// </summary>
/// <param name="clip">The clip to push.</param>
public GeometryClipNode(IGeometryImpl clip)
{
Clip = clip;
}
/// <summary>
/// Initializes a new instance of the <see cref="GeometryClipNode"/> class that represents a
/// geometry clip pop.
/// </summary>
public GeometryClipNode()
{
}
/// <inheritdoc/>
public Rect Bounds => Rect.Empty;
/// <summary>
/// Gets the clip to be pushed or null if the operation represents a pop.
/// </summary>
public IGeometryImpl Clip { get; }
/// <inheritdoc/>
public bool HitTest(Point p) => false;
/// <summary>
/// Determines if this draw operation equals another.
/// </summary>
/// <param name="clip">The clip of the other draw operation.</param>
/// <returns>True if the draw operations are the same, otherwise false.</returns>
/// <remarks>
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
/// </remarks>
public bool Equals(IGeometryImpl clip) => Clip == clip;
/// <inheritdoc/>
public void Render(IDrawingContextImpl context)
{
if (Clip != null)
{
context.PushGeometryClip(Clip);
}
else
{
context.PopGeometryClip();
}
}
}
}

101
src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs

@ -0,0 +1,101 @@
// 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.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// A node in the scene graph which represents a geometry draw.
/// </summary>
internal class GeometryNode : BrushDrawOperation
{
/// <summary>
/// Initializes a new instance of the <see cref="GeometryNode"/> class.
/// </summary>
/// <param name="transform">The transform.</param>
/// <param name="brush">The fill brush.</param>
/// <param name="pen">The stroke pen.</param>
/// <param name="geometry">The geometry.</param>
/// <param name="childScenes">Child scenes for drawing visual brushes.</param>
public GeometryNode(
Matrix transform,
IBrush brush,
Pen pen,
IGeometryImpl geometry,
IDictionary<IVisual, Scene> childScenes = null)
{
Bounds = geometry.GetRenderBounds(pen?.Thickness ?? 0).TransformToAABB(transform);
Transform = transform;
Brush = brush?.ToImmutable();
Pen = pen?.ToImmutable();
Geometry = geometry;
ChildScenes = childScenes;
}
/// <inheritdoc/>
public override Rect Bounds { get; }
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
/// <summary>
/// Gets the fill brush.
/// </summary>
public IBrush Brush { get; }
/// <summary>
/// Gets the stroke pen.
/// </summary>
public Pen Pen { get; }
/// <summary>
/// Gets the geometry to draw.
/// </summary>
public IGeometryImpl Geometry { get; }
/// <inheritdoc/>
public override IDictionary<IVisual, Scene> ChildScenes { get; }
/// <summary>
/// Determines if this draw operation equals another.
/// </summary>
/// <param name="transform">The transform of the other draw operation.</param>
/// <param name="brush">The fill of the other draw operation.</param>
/// <param name="pen">The stroke of the other draw operation.</param>
/// <param name="geometry">The geometry of the other draw operation.</param>
/// <returns>True if the draw operations are the same, otherwise false.</returns>
/// <remarks>
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
/// </remarks>
public bool Equals(Matrix transform, IBrush brush, Pen pen, IGeometryImpl geometry)
{
return transform == Transform &&
Equals(brush, Brush) &&
pen == Pen &&
Equals(geometry, Geometry);
}
/// <inheritdoc/>
public override void Render(IDrawingContextImpl context)
{
context.Transform = Transform;
context.DrawGeometry(Brush, Pen, Geometry);
}
/// <inheritdoc/>
public override bool HitTest(Point p)
{
p *= Transform.Invert();
return (Brush != null && Geometry.FillContains(p)) ||
(Pen != null && Geometry.StrokeContains(Pen, p));
}
}
}

36
src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs

@ -0,0 +1,36 @@
// 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;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// Represents a node in the low-level scene graph that represents geometry.
/// </summary>
public interface IDrawOperation
{
/// <summary>
/// Gets the bounds of the visible content in the node.
/// </summary>
Rect Bounds { get; }
/// <summary>
/// Hit test the geometry in this node.
/// </summary>
/// <param name="p">The point in global coordinates.</param>
/// <returns>True if the point hits the node's geometry; otherwise false.</returns>
/// <remarks>
/// This method does not recurse to child <see cref="IVisualNode"/>s, if you want
/// to hit test children they must be hit tested manually.
/// </remarks>
bool HitTest(Point p);
/// <summary>
/// Renders the node to a drawing context.
/// </summary>
/// <param name="context">The drawing context.</param>
void Render(IDrawingContextImpl context);
}
}

24
src/Avalonia.Visuals/Rendering/SceneGraph/ISceneBuilder.cs

@ -0,0 +1,24 @@
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// Builds a scene graph from a visual tree.
/// </summary>
public interface ISceneBuilder
{
/// <summary>
/// Builds the initial scene graph for a visual tree.
/// </summary>
/// <param name="scene">The scene to build.</param>
void UpdateAll(Scene scene);
/// <summary>
/// Updates the visual (and potentially its children) in a scene.
/// </summary>
/// <param name="scene">The scene.</param>
/// <param name="visual">The visual to update.</param>
/// <returns>True if changes were made, otherwise false.</returns>
bool Update(Scene scene, IVisual visual);
}
}

94
src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs

@ -0,0 +1,94 @@
// 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.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// Represents a node in the low-level scene graph representing an <see cref="IVisual"/>.
/// </summary>
public interface IVisualNode
{
/// <summary>
/// Gets the visual to which the node relates.
/// </summary>
IVisual Visual { get; }
/// <summary>
/// Gets the parent scene graph node.
/// </summary>
IVisualNode Parent { get; }
/// <summary>
/// Gets the transform for the node from global to control coordinates.
/// </summary>
Matrix Transform { get; }
/// <summary>
/// Gets the bounds for the node's geometry in global coordinates.
/// </summary>
Rect Bounds { get; }
/// <summary>
/// Gets the clip bounds for the node in global coordinates.
/// </summary>
/// <remarks>
/// This clip does not take into account parent clips, to find the absolute clip bounds
/// it is necessary to traverse the tree.
/// </remarks>
Rect ClipBounds { get; }
/// <summary>
/// Whether the node is clipped to <see cref="ClipBounds"/>.
/// </summary>
bool ClipToBounds { get; }
/// <summary>
/// Gets the node's clip geometry, if any.
/// </summary>
IGeometryImpl GeometryClip { get; set; }
/// <summary>
/// Gets a value indicating whether one of the node's ancestors has a geometry clip.
/// </summary>
bool HasAncestorGeometryClip { get; }
/// <summary>
/// Gets the child scene graph nodes.
/// </summary>
IReadOnlyList<IVisualNode> Children { get; }
/// <summary>
/// Gets the drawing operations for the visual.
/// </summary>
IReadOnlyList<IDrawOperation> DrawOperations { get; }
/// <summary>
/// Sets up the drawing context for rendering the node's geometry.
/// </summary>
/// <param name="context">The drawing context.</param>
void BeginRender(IDrawingContextImpl context);
/// <summary>
/// Resets the drawing context after rendering the node's geometry.
/// </summary>
/// <param name="context">The drawing context.</param>
void EndRender(IDrawingContextImpl context);
/// <summary>
/// Hit test the geometry in this node.
/// </summary>
/// <param name="p">The point in global coordinates.</param>
/// <returns>True if the point hits the node's geometry; otherwise false.</returns>
/// <remarks>
/// This method does not recurse to child <see cref="IVisualNode"/>s, if you want
/// to hit test children they must be hit tested manually.
/// </remarks>
bool HitTest(Point p);
}
}

94
src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs

@ -0,0 +1,94 @@
// 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;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// A node in the scene graph which represents an image draw.
/// </summary>
internal class ImageNode : IDrawOperation
{
/// <summary>
/// Initializes a new instance of the <see cref="ImageNode"/> class.
/// </summary>
/// <param name="transform">The transform.</param>
/// <param name="source">The image to draw.</param>
/// <param name="opacity">The draw opacity.</param>
/// <param name="sourceRect">The source rect.</param>
/// <param name="destRect">The destination rect.</param>
public ImageNode(Matrix transform, IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect)
{
Bounds = destRect.TransformToAABB(transform);
Transform = transform;
Source = source;
Opacity = opacity;
SourceRect = sourceRect;
DestRect = destRect;
}
/// <inheritdoc/>
public Rect Bounds { get; }
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
/// <summary>
/// Gets the image to draw.
/// </summary>
public IBitmapImpl Source { get; }
/// <summary>
/// Gets the draw opacity.
/// </summary>
public double Opacity { get; }
/// <summary>
/// Gets the source rect.
/// </summary>
public Rect SourceRect { get; }
/// <summary>
/// Gets the destination rect.
/// </summary>
public Rect DestRect { get; }
/// <summary>
/// Determines if this draw operation equals another.
/// </summary>
/// <param name="transform">The transform of the other draw operation.</param>
/// <param name="source">The image of the other draw operation.</param>
/// <param name="opacity">The opacity of the other draw operation.</param>
/// <param name="sourceRect">The source rect of the other draw operation.</param>
/// <param name="destRect">The dest rect of the other draw operation.</param>
/// <returns>True if the draw operations are the same, otherwise false.</returns>
/// <remarks>
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
/// </remarks>
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;
}
/// <inheritdoc/>
public void Render(IDrawingContextImpl context)
{
// TODO: Probably need to introduce some kind of locking mechanism in the case of
// WriteableBitmap.
context.Transform = Transform;
context.DrawImage(Source, Opacity, SourceRect, DestRect);
}
/// <inheritdoc/>
public bool HitTest(Point p) => Bounds.Contains(p);
}
}

95
src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs

@ -0,0 +1,95 @@
// 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.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// A node in the scene graph which represents a line draw.
/// </summary>
internal class LineNode : BrushDrawOperation
{
/// <summary>
/// Initializes a new instance of the <see cref="GeometryNode"/> class.
/// </summary>
/// <param name="transform">The transform.</param>
/// <param name="pen">The stroke pen.</param>
/// <param name="p1">The start point of the line.</param>
/// <param name="p2">The end point of the line.</param>
/// <param name="childScenes">Child scenes for drawing visual brushes.</param>
public LineNode(
Matrix transform,
Pen pen,
Point p1,
Point p2,
IDictionary<IVisual, Scene> childScenes = null)
{
Bounds = new Rect(P1, P2);
Transform = transform;
Pen = pen?.ToImmutable();
P1 = p1;
P2 = p2;
ChildScenes = childScenes;
}
/// <inheritdoc/>
public override Rect Bounds { get; }
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
/// <summary>
/// Gets the stroke pen.
/// </summary>
public Pen Pen { get; }
/// <summary>
/// Gets the start point of the line.
/// </summary>
public Point P1 { get; }
/// <summary>
/// Gets the end point of the line.
/// </summary>
public Point P2 { get; }
/// <inheritdoc/>
public override IDictionary<IVisual, Scene> ChildScenes { get; }
/// <summary>
/// Determines if this draw operation equals another.
/// </summary>
/// <param name="transform">The transform of the other draw operation.</param>
/// <param name="pen">The stroke of the other draw operation.</param>
/// <param name="p1">The start point of the other draw operation.</param>
/// <param name="p2">The end point of the other draw operation.</param>
/// <returns>True if the draw operations are the same, otherwise false.</returns>
/// <remarks>
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
/// </remarks>
public bool Equals(Matrix transform, Pen pen, Point p1, Point p2)
{
return transform == Transform && pen == Pen && p1 == P1 && p2 == P2;
}
public override void Render(IDrawingContextImpl context)
{
context.Transform = Transform;
context.DrawLine(Pen, P1, P2);
}
public override bool HitTest(Point p)
{
// TODO: Implement line hit testing.
return false;
}
}
}

80
src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs

@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// A node in the scene graph which represents an opacity mask push or pop.
/// </summary>
internal class OpacityMaskNode : BrushDrawOperation
{
/// <summary>
/// Initializes a new instance of the <see cref="OpacityMaskNode"/> class that represents an
/// opacity mask push.
/// </summary>
/// <param name="mask">The opacity mask to push.</param>
/// <param name="bounds">The bounds of the mask.</param>
/// <param name="childScenes">Child scenes for drawing visual brushes.</param>
public OpacityMaskNode(IBrush mask, Rect bounds, IDictionary<IVisual, Scene> childScenes = null)
{
Mask = mask?.ToImmutable();
MaskBounds = bounds;
ChildScenes = childScenes;
}
/// <summary>
/// Initializes a new instance of the <see cref="OpacityMaskNode"/> class that represents an
/// opacity mask pop.
/// </summary>
public OpacityMaskNode()
{
}
/// <inheritdoc/>
public override Rect Bounds => Rect.Empty;
/// <summary>
/// Gets the mask to be pushed or null if the operation represents a pop.
/// </summary>
public IBrush Mask { get; }
/// <summary>
/// Gets the bounds of the opacity mask or null if the operation represents a pop.
/// </summary>
public Rect? MaskBounds { get; }
/// <inheritdoc/>
public override IDictionary<IVisual, Scene> ChildScenes { get; }
/// <inheritdoc/>
public override bool HitTest(Point p) => false;
/// <summary>
/// Determines if this draw operation equals another.
/// </summary>
/// <param name="mask">The opacity mask of the other draw operation.</param>
/// <param name="bounds">The opacity mask bounds of the other draw operation.</param>
/// <returns>True if the draw operations are the same, otherwise false.</returns>
/// <remarks>
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
/// </remarks>
public bool Equals(IBrush mask, Rect? bounds) => Mask == mask && MaskBounds == bounds;
/// <inheritdoc/>
public override void Render(IDrawingContextImpl context)
{
if (Mask != null)
{
context.PushOpacityMask(Mask, MaskBounds.Value);
}
else
{
context.PopOpacityMask();
}
}
}
}

64
src/Avalonia.Visuals/Rendering/SceneGraph/OpacityNode.cs

@ -0,0 +1,64 @@
using System;
using Avalonia.Platform;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// A node in the scene graph which represents an opacity push or pop.
/// </summary>
internal class OpacityNode : IDrawOperation
{
/// <summary>
/// Initializes a new instance of the <see cref="OpacityNode"/> class that represents an
/// opacity push.
/// </summary>
/// <param name="opacity">The opacity to push.</param>
public OpacityNode(double opacity)
{
Opacity = opacity;
}
/// <summary>
/// Initializes a new instance of the <see cref="OpacityNode"/> class that represents an
/// opacity pop.
/// </summary>
public OpacityNode()
{
}
/// <inheritdoc/>
public Rect Bounds => Rect.Empty;
/// <summary>
/// Gets the opacity to be pushed or null if the operation represents a pop.
/// </summary>
public double? Opacity { get; }
/// <inheritdoc/>
public bool HitTest(Point p) => false;
/// <summary>
/// Determines if this draw operation equals another.
/// </summary>
/// <param name="opacity">The opacity of the other draw operation.</param>
/// <returns>True if the draw operations are the same, otherwise false.</returns>
/// <remarks>
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
/// </remarks>
public bool Equals(double? opacity) => Opacity == opacity;
/// <inheritdoc/>
public void Render(IDrawingContextImpl context)
{
if (Opacity.HasValue)
{
context.PushOpacity(Opacity.Value);
}
else
{
context.PopOpacity();
}
}
}
}

116
src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs

@ -0,0 +1,116 @@
// 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.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// A node in the scene graph which represents a rectangle draw.
/// </summary>
internal class RectangleNode : BrushDrawOperation
{
/// <summary>
/// Initializes a new instance of the <see cref="RectangleNode"/> class.
/// </summary>
/// <param name="transform">The transform.</param>
/// <param name="brush">The fill brush.</param>
/// <param name="pen">The stroke pen.</param>
/// <param name="rect">The rectanle to draw.</param>
/// <param name="cornerRadius">The rectangle corner radius.</param>
/// <param name="childScenes">Child scenes for drawing visual brushes.</param>
public RectangleNode(
Matrix transform,
IBrush brush,
Pen pen,
Rect rect,
float cornerRadius,
IDictionary<IVisual, Scene> childScenes = null)
{
Bounds = rect.TransformToAABB(transform).Inflate(pen?.Thickness ?? 0);
Transform = transform;
Brush = brush?.ToImmutable();
Pen = pen?.ToImmutable();
Rect = rect;
CornerRadius = cornerRadius;
ChildScenes = childScenes;
}
/// <inheritdoc/>
public override Rect Bounds { get; }
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
/// <summary>
/// Gets the fill brush.
/// </summary>
public IBrush Brush { get; }
/// <summary>
/// Gets the stroke pen.
/// </summary>
public Pen Pen { get; }
/// <summary>
/// Gets the rectangle to draw.
/// </summary>
public Rect Rect { get; }
/// <summary>
/// Gets the rectangle corner radius.
/// </summary>
public float CornerRadius { get; }
/// <inheritdoc/>
public override IDictionary<IVisual, Scene> ChildScenes { get; }
/// <summary>
/// Determines if this draw operation equals another.
/// </summary>
/// <param name="transform">The transform of the other draw operation.</param>
/// <param name="brush">The fill of the other draw operation.</param>
/// <param name="pen">The stroke of the other draw operation.</param>
/// <param name="rect">The rectangle of the other draw operation.</param>
/// <param name="cornerRadius">The rectangle corner radius of the other draw operation.</param>
/// <returns>True if the draw operations are the same, otherwise false.</returns>
/// <remarks>
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
/// </remarks>
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;
}
/// <inheritdoc/>
public override void Render(IDrawingContextImpl context)
{
context.Transform = Transform;
if (Brush != null)
{
context.FillRectangle(Brush, Rect, CornerRadius);
}
if (Pen != null)
{
context.DrawRectangle(Pen, Rect, CornerRadius);
}
}
// TODO: This doesn't respect CornerRadius yet.
/// <inheritdoc/>
public override bool HitTest(Point p) => Bounds.Contains(p);
}
}

184
src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs

@ -0,0 +1,184 @@
// 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
{
/// <summary>
/// Represents a scene graph used by the <see cref="DeferredRenderer"/>.
/// </summary>
public class Scene
{
private Dictionary<IVisual, IVisualNode> _index;
/// <summary>
/// Initializes a new instance of the <see cref="Scene"/> class.
/// </summary>
/// <param name="rootVisual">The root visual to draw.</param>
public Scene(IVisual rootVisual)
: this(
new VisualNode(rootVisual, null),
new Dictionary<IVisual, IVisualNode>(),
new SceneLayers(rootVisual),
0)
{
_index.Add(rootVisual, Root);
}
private Scene(VisualNode root, Dictionary<IVisual, IVisualNode> index, SceneLayers layers, int generation)
{
Contract.Requires<ArgumentNullException>(root != null);
var renderRoot = root.Visual as IRenderRoot;
_index = index;
Root = root;
Layers = layers;
Generation = generation;
root.LayerRoot = root.Visual;
}
/// <summary>
/// Gets a value identifying the scene's generation. This is incremented each time the scene is cloned.
/// </summary>
public int Generation { get; }
/// <summary>
/// Gets the layers for the scene.
/// </summary>
public SceneLayers Layers { get; }
/// <summary>
/// Gets the root node of the scene graph.
/// </summary>
public IVisualNode Root { get; }
/// <summary>
/// Gets or sets the size of the scene in device independent pixels.
/// </summary>
public Size Size { get; set; }
/// <summary>
/// Gets or sets the scene scaling.
/// </summary>
public double Scaling { get; set; } = 1;
/// <summary>
/// Adds a node to the scene index.
/// </summary>
/// <param name="node">The node.</param>
public void Add(IVisualNode node)
{
Contract.Requires<ArgumentNullException>(node != null);
_index.Add(node.Visual, node);
}
/// <summary>
/// Clones the scene.
/// </summary>
/// <returns>The cloned scene.</returns>
public Scene Clone()
{
var index = new Dictionary<IVisual, IVisualNode>();
var root = Clone((VisualNode)Root, null, index);
var result = new Scene(root, index, Layers.Clone(), Generation + 1)
{
Size = Size,
Scaling = Scaling,
};
return result;
}
/// <summary>
/// Tries to find a node in the scene graph representing the specified visual.
/// </summary>
/// <param name="visual">The visual.</param>
/// <returns>
/// The node representing the visual or null if it could not be found.
/// </returns>
public IVisualNode FindNode(IVisual visual)
{
IVisualNode node;
_index.TryGetValue(visual, out node);
return node;
}
/// <summary>
/// Gets the visuals at a point in the scene.
/// </summary>
/// <param name="p">The point.</param>
/// <param name="filter">A filter. May be null.</param>
/// <returns>The visuals at the specified point.</returns>
public IEnumerable<IVisual> HitTest(Point p, Func<IVisual, bool> filter)
{
return HitTest(Root, p, null, filter);
}
/// <summary>
/// Removes a node from the scene index.
/// </summary>
/// <param name="node">The node.</param>
public void Remove(IVisualNode node)
{
Contract.Requires<ArgumentNullException>(node != null);
_index.Remove(node.Visual);
}
private VisualNode Clone(VisualNode source, IVisualNode parent, Dictionary<IVisual, IVisualNode> index)
{
var result = source.Clone(parent);
index.Add(result.Visual, result);
foreach (var child in source.Children)
{
result.AddChild(Clone((VisualNode)child, result, index));
}
return result;
}
private IEnumerable<IVisual> HitTest(IVisualNode node, Point p, Rect? clip, Func<IVisual, bool> filter)
{
if (filter?.Invoke(node.Visual) != false)
{
var clipped = false;
if (node.ClipToBounds)
{
clip = clip == null ? node.ClipBounds : clip.Value.Intersect(node.ClipBounds);
clipped = !clip.Value.Contains(p);
}
if (node.GeometryClip != null)
{
var controlPoint = Root.Visual.TranslatePoint(p, node.Visual);
clipped = !node.GeometryClip.FillContains(controlPoint);
}
if (!clipped)
{
for (var i = node.Children.Count - 1; i >= 0; --i)
{
foreach (var h in HitTest(node.Children[i], p, clip, filter))
{
yield return h;
}
}
if (node.HitTest(p))
{
yield return node.Visual;
}
}
}
}
}
}

384
src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs

@ -0,0 +1,384 @@
// 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.Linq;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// Builds a scene graph from a visual tree.
/// </summary>
public class SceneBuilder : ISceneBuilder
{
/// <inheritdoc/>
public void UpdateAll(Scene scene)
{
Contract.Requires<ArgumentNullException>(scene != null);
Dispatcher.UIThread.VerifyAccess();
UpdateSize(scene);
scene.Layers.GetOrAdd(scene.Root.Visual);
using (var impl = new DeferredDrawingContextImpl(this, scene.Layers))
using (var context = new DrawingContext(impl))
{
Update(context, scene, (VisualNode)scene.Root, scene.Root.Visual.Bounds, true);
}
}
/// <inheritdoc/>
public bool Update(Scene scene, IVisual visual)
{
Contract.Requires<ArgumentNullException>(scene != null);
Contract.Requires<ArgumentNullException>(visual != null);
Dispatcher.UIThread.VerifyAccess();
var node = (VisualNode)scene.FindNode(visual);
if (visual == scene.Root.Visual)
{
UpdateSize(scene);
}
if (visual.VisualRoot != null)
{
if (visual.IsVisible)
{
// If the node isn't yet part of the scene, find the nearest ancestor that is.
node = node ?? FindExistingAncestor(scene, visual);
// We don't need to do anything if this part of the tree has already been fully
// updated.
if (node != null && !node.SubTreeUpdated)
{
// If the control we've been asked to update isn't part of the scene then
// we're carrying out an add operation, so recurse and add all the
// descendents too.
var recurse = node.Visual != visual;
using (var impl = new DeferredDrawingContextImpl(this, scene.Layers))
using (var context = new DrawingContext(impl))
{
var clip = scene.Root.Visual.Bounds;
if (node.Parent != null)
{
context.PushPostTransform(node.Parent.Transform);
clip = node.Parent.ClipBounds;
}
using (context.PushTransformContainer())
{
Update(context, scene, node, clip, recurse);
}
}
return true;
}
}
else
{
if (node != null)
{
// The control has been hidden so remove it from its parent and deindex the
// node and its descendents.
((VisualNode)node.Parent)?.RemoveChild(node);
Deindex(scene, node);
return true;
}
}
}
else if (node != null)
{
// The control has been removed so remove it from its parent and deindex the
// node and its descendents.
var trim = FindFirstDeadAncestor(scene, node);
((VisualNode)trim.Parent).RemoveChild(trim);
Deindex(scene, trim);
return true;
}
return false;
}
private static VisualNode FindExistingAncestor(Scene scene, IVisual visual)
{
var node = scene.FindNode(visual);
while (node == null && visual.IsVisible)
{
visual = visual.VisualParent;
node = scene.FindNode(visual);
}
return visual.IsVisible ? (VisualNode)node : null;
}
private static VisualNode FindFirstDeadAncestor(Scene scene, IVisualNode node)
{
var parent = node.Parent;
while (parent.Visual.VisualRoot == null)
{
node = parent;
parent = node.Parent;
}
return (VisualNode)node;
}
private static void Update(DrawingContext context, Scene scene, VisualNode node, Rect clip, bool forceRecurse)
{
var visual = node.Visual;
var opacity = visual.Opacity;
var clipToBounds = visual.ClipToBounds;
var bounds = new Rect(visual.Bounds.Size);
var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl;
contextImpl.Layers.Find(node.LayerRoot)?.Dirty.Add(node.Bounds);
if (visual.IsVisible)
{
var m = 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.BeginUpdate(node))
using (context.PushPostTransform(m))
using (context.PushTransformContainer())
{
var startLayer = opacity < 1 || visual.OpacityMask != null;
forceRecurse = forceRecurse || node.Transform != contextImpl.Transform;
node.Transform = contextImpl.Transform;
node.ClipBounds = bounds.TransformToAABB(node.Transform).Intersect(clip);
node.ClipToBounds = clipToBounds;
node.GeometryClip = visual.Clip?.PlatformImpl;
node.Opacity = opacity;
node.OpacityMask = visual.OpacityMask;
if (startLayer)
{
if (node.LayerRoot != visual)
{
MakeLayer(scene, node);
}
else
{
UpdateLayer(node, scene.Layers[node.LayerRoot]);
}
}
else if (!startLayer && node.LayerRoot == node.Visual && node.Parent != null)
{
ClearLayer(scene, node);
}
if (node.ClipToBounds)
{
clip = clip.Intersect(node.ClipBounds);
}
try
{
visual.Render(context);
}
catch { }
if (visual is Visual)
{
var transformed = new TransformedBounds(new Rect(visual.Bounds.Size), clip, node.Transform);
BoundsTracker.SetTransformedBounds((Visual)visual, transformed);
}
if (forceRecurse)
{
foreach (var child in visual.VisualChildren.OrderBy(x => x, ZIndexComparer.Instance))
{
var childNode = scene.FindNode(child) ?? CreateNode(scene, child, node);
Update(context, scene, (VisualNode)childNode, clip, forceRecurse);
}
node.SubTreeUpdated = true;
contextImpl.TrimChildren();
}
}
}
}
private void UpdateSize(Scene scene)
{
var renderRoot = scene.Root.Visual as IRenderRoot;
var newSize = renderRoot?.ClientSize ?? scene.Root.Visual.Bounds.Size;
scene.Scaling = renderRoot?.RenderScaling ?? 1;
if (scene.Size != newSize)
{
var oldSize = scene.Size;
scene.Size = newSize;
Rect horizontalDirtyRect = Rect.Empty;
Rect verticalDirtyRect = Rect.Empty;
if (newSize.Width > oldSize.Width)
{
horizontalDirtyRect = new Rect(oldSize.Width, 0, newSize.Width - oldSize.Width, oldSize.Height);
}
if (newSize.Height > oldSize.Height)
{
verticalDirtyRect = new Rect(0, oldSize.Height, newSize.Width, newSize.Height - oldSize.Height);
}
foreach (var layer in scene.Layers)
{
layer.Dirty.Add(horizontalDirtyRect);
layer.Dirty.Add(verticalDirtyRect);
}
}
}
private static VisualNode CreateNode(Scene scene, IVisual visual, VisualNode parent)
{
var node = new VisualNode(visual, parent);
node.LayerRoot = parent.LayerRoot;
scene.Add(node);
return node;
}
private static void Deindex(Scene scene, VisualNode node)
{
scene.Remove(node);
node.SubTreeUpdated = true;
scene.Layers[node.LayerRoot].Dirty.Add(node.Bounds);
if (node.Visual is Visual v)
{
BoundsTracker.SetTransformedBounds(v, null);
}
foreach (VisualNode child in node.Children)
{
var geometry = child as IDrawOperation;
if (child is VisualNode visual)
{
Deindex(scene, visual);
}
}
if (node.LayerRoot == node.Visual && node.Visual != scene.Root.Visual)
{
scene.Layers.Remove(node.LayerRoot);
}
}
private static void ClearLayer(Scene scene, VisualNode node)
{
var parent = (VisualNode)node.Parent;
var oldLayerRoot = node.LayerRoot;
var newLayerRoot = parent.LayerRoot;
var existingDirtyRects = scene.Layers[node.LayerRoot].Dirty;
var newDirtyRects = scene.Layers[newLayerRoot].Dirty;
existingDirtyRects.Coalesce();
foreach (var r in existingDirtyRects)
{
newDirtyRects.Add(r);
}
var oldLayer = scene.Layers[oldLayerRoot];
PropagateLayer(node, scene.Layers[newLayerRoot], oldLayer);
scene.Layers.Remove(oldLayer);
}
private static void MakeLayer(Scene scene, VisualNode node)
{
var oldLayerRoot = node.LayerRoot;
var layer = scene.Layers.Add(node.Visual);
var oldLayer = scene.Layers[oldLayerRoot];
UpdateLayer(node, layer);
PropagateLayer(node, layer, scene.Layers[oldLayerRoot]);
}
private static void UpdateLayer(VisualNode node, SceneLayer layer)
{
layer.Opacity = node.Visual.Opacity;
if (node.Visual.OpacityMask != null)
{
layer.OpacityMask = node.Visual.OpacityMask?.ToImmutable();
layer.OpacityMaskRect = node.ClipBounds;
}
else
{
layer.OpacityMask = null;
layer.OpacityMaskRect = Rect.Empty;
}
layer.GeometryClip = node.HasAncestorGeometryClip ?
CreateLayerGeometryClip(node) :
null;
}
private static void PropagateLayer(VisualNode node, SceneLayer layer, SceneLayer oldLayer)
{
node.LayerRoot = layer.LayerRoot;
layer.Dirty.Add(node.Bounds);
oldLayer.Dirty.Add(node.Bounds);
foreach (VisualNode child in node.Children)
{
// If the child is not the start of a new layer, recurse.
if (child.LayerRoot != child.Visual)
{
PropagateLayer(child, layer, oldLayer);
}
}
}
private static IGeometryImpl CreateLayerGeometryClip(VisualNode node)
{
IGeometryImpl result = null;
for (;;)
{
node = (VisualNode)node.Parent;
if (node == null || (node.GeometryClip == null && !node.HasAncestorGeometryClip))
{
break;
}
if (node?.GeometryClip != null)
{
var transformed = node.GeometryClip.WithTransform(node.Transform);
result = result == null ? transformed : result.Intersect(transformed);
}
}
return result;
}
}
}

75
src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayer.cs

@ -0,0 +1,75 @@
using System;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// Represents a layer in a <see cref="Scene"/>.
/// </summary>
public class SceneLayer
{
/// <summary>
/// Initializes a new instance of the <see cref="SceneLayer"/> class.
/// </summary>
/// <param name="layerRoot">The visual at the root of the layer.</param>
/// <param name="distanceFromRoot">The distance from the scene root.</param>
public SceneLayer(IVisual layerRoot, int distanceFromRoot)
{
LayerRoot = layerRoot;
Dirty = new DirtyRects();
DistanceFromRoot = distanceFromRoot;
}
/// <summary>
/// Clones the layer.
/// </summary>
/// <returns>The cloned layer.</returns>
public SceneLayer Clone()
{
return new SceneLayer(LayerRoot, DistanceFromRoot)
{
Opacity = Opacity,
OpacityMask = OpacityMask,
OpacityMaskRect = OpacityMaskRect,
GeometryClip = GeometryClip,
};
}
/// <summary>
/// Gets the visual at the root of the layer.
/// </summary>
public IVisual LayerRoot { get; }
/// <summary>
/// Gets the distance of the layer root from the root of the scene.
/// </summary>
public int DistanceFromRoot { get; }
/// <summary>
/// Gets or sets the opacity of the layer.
/// </summary>
public double Opacity { get; set; } = 1;
/// <summary>
/// Gets or sets the opacity mask for the layer.
/// </summary>
public IBrush OpacityMask { get; set; }
/// <summary>
/// Gets or sets the target rectangle for the layer opacity mask.
/// </summary>
public Rect OpacityMaskRect { get; set; }
/// <summary>
/// Gets the layer's geometry clip.
/// </summary>
public IGeometryImpl GeometryClip { get; set; }
/// <summary>
/// Gets the dirty rectangles for the layer.
/// </summary>
internal DirtyRects Dirty { get; }
}
}

199
src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs

@ -0,0 +1,199 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// Holds a collection of layers for a <see cref="Scene"/>.
/// </summary>
public class SceneLayers : IEnumerable<SceneLayer>
{
private readonly IVisual _root;
private readonly List<SceneLayer> _inner = new List<SceneLayer>();
private readonly Dictionary<IVisual, SceneLayer> _index = new Dictionary<IVisual, SceneLayer>();
/// <summary>
/// Initializes a new instance of the <see cref="SceneLayers"/> class.
/// </summary>
/// <param name="root">The scene's root visual.</param>
public SceneLayers(IVisual root)
{
_root = root;
}
/// <summary>
/// Gets the number of layers in the scene.
/// </summary>
public int Count => _inner.Count;
/// <summary>
/// Gets a value indicating whether any of the layers have a dirty region.
/// </summary>
public bool HasDirty
{
get
{
foreach (var layer in _inner)
{
if (!layer.Dirty.IsEmpty)
{
return true;
}
}
return false;
}
}
/// <summary>
/// Gets a layer by index.
/// </summary>
/// <param name="index">The index of the layer.</param>
/// <returns>The layer.</returns>
public SceneLayer this[int index] => _inner[index];
/// <summary>
/// Gets a layer by its root visual.
/// </summary>
/// <param name="visual">The layer's root visual.</param>
/// <returns>The layer.</returns>
public SceneLayer this[IVisual visual] => _index[visual];
/// <summary>
/// Adds a layer to the scene.
/// </summary>
/// <param name="layerRoot">The root visual of the layer.</param>
/// <returns>The created layer.</returns>
public SceneLayer Add(IVisual layerRoot)
{
Contract.Requires<ArgumentNullException>(layerRoot != null);
var distance = layerRoot.CalculateDistanceFromAncestor(_root);
var layer = new SceneLayer(layerRoot, distance);
var insert = FindInsertIndex(layer);
_index.Add(layerRoot, layer);
_inner.Insert(insert, layer);
return layer;
}
/// <summary>
/// Makes a deep clone of the layers.
/// </summary>
/// <returns>The cloned layers.</returns>
public SceneLayers Clone()
{
var result = new SceneLayers(_root);
foreach (var src in _inner)
{
var dest = src.Clone();
result._index.Add(dest.LayerRoot, dest);
result._inner.Add(dest);
}
return result;
}
/// <summary>
/// Tests whether a layer exists with the specified root visual.
/// </summary>
/// <param name="layerRoot">The root visual.</param>
/// <returns>
/// True if a layer exists with the specified root visual, otherwise false.
/// </returns>
public bool Exists(IVisual layerRoot)
{
Contract.Requires<ArgumentNullException>(layerRoot != null);
return _index.ContainsKey(layerRoot);
}
/// <summary>
/// Tries to find a layer with the specified root visual.
/// </summary>
/// <param name="layerRoot">The root visual.</param>
/// <returns>The layer if found, otherwise null.</returns>
public SceneLayer Find(IVisual layerRoot)
{
SceneLayer result;
_index.TryGetValue(layerRoot, out result);
return result;
}
/// <summary>
/// Gets an existing layer or creates a new one if no existing layer is found.
/// </summary>
/// <param name="layerRoot">The root visual.</param>
/// <returns>The layer.</returns>
public SceneLayer GetOrAdd(IVisual layerRoot)
{
Contract.Requires<ArgumentNullException>(layerRoot != null);
SceneLayer result;
if (!_index.TryGetValue(layerRoot, out result))
{
result = Add(layerRoot);
}
return result;
}
/// <summary>
/// Removes a layer from the scene.
/// </summary>
/// <param name="layerRoot">The root visual.</param>
/// <returns>True if a matching layer was removed, otherwise false.</returns>
public bool Remove(IVisual layerRoot)
{
Contract.Requires<ArgumentNullException>(layerRoot != null);
SceneLayer layer;
if (_index.TryGetValue(layerRoot, out layer))
{
Remove(layer);
}
return layer != null;
}
/// <summary>
/// Removes a layer from the scene.
/// </summary>
/// <param name="layer">The layer.</param>
/// <returns>True if the layer was part of the scene, otherwise false.</returns>
public bool Remove(SceneLayer layer)
{
Contract.Requires<ArgumentNullException>(layer != null);
_index.Remove(layer.LayerRoot);
return _inner.Remove(layer);
}
/// <inheritdoc/>
public IEnumerator<SceneLayer> GetEnumerator() => _inner.GetEnumerator();
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
private int FindInsertIndex(SceneLayer insert)
{
var index = 0;
foreach (var layer in _inner)
{
if (layer.DistanceFromRoot > insert.DistanceFromRoot)
{
break;
}
++index;
}
return index;
}
}
}

96
src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs

@ -0,0 +1,96 @@
// 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.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// A node in the scene graph which represents a text draw.
/// </summary>
internal class TextNode : BrushDrawOperation
{
/// <summary>
/// Initializes a new instance of the <see cref="TextNode"/> class.
/// </summary>
/// <param name="transform">The transform.</param>
/// <param name="foreground">The foreground brush.</param>
/// <param name="origin">The draw origin.</param>
/// <param name="text">The text to draw.</param>
/// <param name="childScenes">Child scenes for drawing visual brushes.</param>
public TextNode(
Matrix transform,
IBrush foreground,
Point origin,
IFormattedTextImpl text,
IDictionary<IVisual, Scene> childScenes = null)
{
Bounds = new Rect(origin, text.Size).TransformToAABB(transform);
Transform = transform;
Foreground = foreground?.ToImmutable();
Origin = origin;
Text = text;
ChildScenes = childScenes;
}
/// <inheritdoc/>
public override Rect Bounds { get; }
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
/// <summary>
/// Gets the foreground brush.
/// </summary>
public IBrush Foreground { get; }
/// <summary>
/// Gets the draw origin.
/// </summary>
public Point Origin { get; }
/// <summary>
/// Gets the text to draw.
/// </summary>
public IFormattedTextImpl Text { get; }
/// <inheritdoc/>
public override IDictionary<IVisual, Scene> ChildScenes { get; }
/// <inheritdoc/>
public override void Render(IDrawingContextImpl context)
{
context.Transform = Transform;
context.DrawText(Foreground, Origin, Text);
}
/// <summary>
/// Determines if this draw operation equals another.
/// </summary>
/// <param name="transform">The transform of the other draw operation.</param>
/// <param name="foreground">The foregroundof the other draw operation.</param>
/// <param name="origin">The draw origin of the other draw operation.</param>
/// <param name="text">The text of the other draw operation.</param>
/// <returns>True if the draw operations are the same, otherwise false.</returns>
/// <remarks>
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
/// </remarks>
internal bool Equals(Matrix transform, IBrush foreground, Point origin, IFormattedTextImpl text)
{
return transform == Transform &&
Equals(foreground, Foreground) &&
origin == Origin &&
Equals(text, Text);
}
/// <inheritdoc/>
public override bool HitTest(Point p) => Bounds.Contains(p);
}
}

285
src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs

@ -0,0 +1,285 @@
// 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.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// A node in the low-level scene graph representing an <see cref="IVisual"/>.
/// </summary>
internal class VisualNode : IVisualNode
{
private static readonly IReadOnlyList<IVisualNode> EmptyChildren = new IVisualNode[0];
private static readonly IReadOnlyList<IDrawOperation> EmptyDrawOperations = new IDrawOperation[0];
private Rect? _bounds;
private double _opacity;
private List<IVisualNode> _children;
private List<IDrawOperation> _drawOperations;
private bool _drawOperationsCloned;
/// <summary>
/// Initializes a new instance of the <see cref="VisualNode"/> class.
/// </summary>
/// <param name="visual">The visual that this node represents.</param>
/// <param name="parent">The parent scene graph node, if any.</param>
public VisualNode(IVisual visual, IVisualNode parent)
{
Contract.Requires<ArgumentNullException>(visual != null);
Visual = visual;
Parent = parent;
HasAncestorGeometryClip = parent != null &&
(parent.HasAncestorGeometryClip || parent.GeometryClip != null);
}
/// <inheritdoc/>
public IVisual Visual { get; }
/// <inheritdoc/>
public IVisualNode Parent { get; }
/// <inheritdoc/>
public Matrix Transform { get; set; }
/// <inheritdoc/>
public Rect Bounds => _bounds ?? CalculateBounds();
/// <inheritdoc/>
public Rect ClipBounds { get; set; }
/// <inheritdoc/>
public bool ClipToBounds { get; set; }
/// <inheritdoc/>
public IGeometryImpl GeometryClip { get; set; }
/// <inheritdoc/>
public bool HasAncestorGeometryClip { get; }
/// <summary>
/// Gets or sets the opacity of the scene graph node.
/// </summary>
public double Opacity
{
get { return _opacity; }
set
{
if (_opacity != value)
{
_opacity = value;
OpacityChanged = true;
}
}
}
/// <summary>
/// Gets or sets the opacity mask for the scnee graph node.
/// </summary>
public IBrush OpacityMask { get; set; }
/// <summary>
/// Gets a value indicating whether this node in the scene graph has already
/// been updated in the current update pass.
/// </summary>
public bool SubTreeUpdated { get; set; }
/// <summary>
/// Gets a value indicating whether the <see cref="Opacity"/> property has changed.
/// </summary>
public bool OpacityChanged { get; private set; }
public IVisual LayerRoot { get; set; }
/// <inheritdoc/>
public IReadOnlyList<IVisualNode> Children => _children ?? EmptyChildren;
/// <inheritdoc/>
public IReadOnlyList<IDrawOperation> DrawOperations => _drawOperations ?? EmptyDrawOperations;
/// <summary>
/// Adds a child to the <see cref="Children"/> collection.
/// </summary>
/// <param name="child">The child to add.</param>
public void AddChild(IVisualNode child)
{
EnsureChildrenCreated();
_children.Add(child);
}
/// <summary>
/// Adds an operation to the <see cref="DrawOperations"/> collection.
/// </summary>
/// <param name="operation">The operation to add.</param>
public void AddDrawOperation(IDrawOperation operation)
{
EnsureDrawOperationsCreated();
_drawOperations.Add(operation);
}
/// <summary>
/// Removes a child from the <see cref="Children"/> collection.
/// </summary>
/// <param name="child">The child to remove.</param>
public void RemoveChild(IVisualNode child)
{
EnsureChildrenCreated();
_children.Remove(child);
}
/// <summary>
/// Replaces a child in the <see cref="Children"/> collection.
/// </summary>
/// <param name="index">The child to be replaced.</param>
/// <param name="node">The child to add.</param>
public void ReplaceChild(int index, IVisualNode node)
{
EnsureChildrenCreated();
_children[index] = node;
}
/// <summary>
/// Replaces an item in the <see cref="DrawOperations"/> collection.
/// </summary>
/// <param name="index">The opeation to be replaced.</param>
/// <param name="operation">The operation to add.</param>
public void ReplaceDrawOperation(int index, IDrawOperation operation)
{
EnsureDrawOperationsCreated();
_drawOperations[index] = operation;
}
/// <summary>
/// Removes items in the <see cref="Children"/> collection from the specified index
/// to the end.
/// </summary>
/// <param name="first">The index of the first child to be removed.</param>
public void TrimChildren(int first)
{
if (first < _children?.Count)
{
EnsureChildrenCreated();
_children.RemoveRange(first, _children.Count - first);
}
}
/// <summary>
/// Removes items in the <see cref="DrawOperations"/> collection from the specified index
/// to the end.
/// </summary>
/// <param name="first">The index of the first operation to be removed.</param>
public void TrimDrawOperations(int first)
{
if (first < _drawOperations?.Count)
{
EnsureDrawOperationsCreated();
_drawOperations.RemoveRange(first, _drawOperations.Count - first);
}
}
/// <summary>
/// Makes a copy of the node
/// </summary>
/// <param name="parent">The new parent node.</param>
/// <returns>A cloned node.</returns>
public VisualNode Clone(IVisualNode parent)
{
return new VisualNode(Visual, parent)
{
Transform = Transform,
ClipBounds = ClipBounds,
ClipToBounds = ClipToBounds,
GeometryClip = GeometryClip,
_opacity = Opacity,
OpacityMask = OpacityMask,
_drawOperations = _drawOperations,
_drawOperationsCloned = true,
LayerRoot= LayerRoot,
};
}
/// <inheritdoc/>
public bool HitTest(Point p)
{
foreach (var operation in DrawOperations)
{
if (operation.HitTest(p) == true)
{
return true;
}
}
return false;
}
/// <inheritdoc/>
public void BeginRender(IDrawingContextImpl context)
{
if (ClipToBounds)
{
context.Transform = Matrix.Identity;
context.PushClip(ClipBounds);
}
context.Transform = Transform;
if (GeometryClip != null)
{
context.PushGeometryClip(GeometryClip);
}
}
/// <inheritdoc/>
public void EndRender(IDrawingContextImpl context)
{
if (GeometryClip != null)
{
context.PopGeometryClip();
}
if (ClipToBounds)
{
context.PopClip();
}
}
private Rect CalculateBounds()
{
var result = new Rect();
foreach (var operation in DrawOperations)
{
result = result.Union(operation.Bounds);
}
_bounds = result;
return result;
}
private void EnsureChildrenCreated()
{
if (_children == null)
{
_children = new List<IVisualNode>();
}
}
private void EnsureDrawOperationsCreated()
{
if (_drawOperations == null)
{
_drawOperations = new List<IDrawOperation>();
}
else if (_drawOperationsCloned)
{
_drawOperations = new List<IDrawOperation>(_drawOperations);
_drawOperationsCloned = false;
}
}
}
}

11
src/Avalonia.Visuals/Vector.cs

@ -74,6 +74,17 @@ namespace Avalonia
return new Vector(vector._x * scale, vector._y * scale);
}
/// <summary>
/// Scales a vector.
/// </summary>
/// <param name="vector">The vector</param>
/// <param name="scale">The divisor.</param>
/// <returns>The scaled vector.</returns>
public static Vector operator /(Vector vector, double scale)
{
return new Vector(vector._x / scale, vector._y / scale);
}
/// <summary>
/// Length of the vector
/// </summary>

24
src/Avalonia.Visuals/VisualTree/VisualExtensions.cs

@ -13,6 +13,30 @@ namespace Avalonia.VisualTree
/// </summary>
public static class VisualExtensions
{
/// <summary>
/// Calculates the distance from a visual's <see cref="IRenderRoot"/>.
/// </summary>
/// <param name="visual">The visual.</param>
/// <param name="ancestor">The ancestor visual.</param>
/// <returns>
/// The number of steps from the visual to the ancestor or -1 if
/// <paramref name="visual"/> is not a descendent of <paramref name="ancestor"/>.
/// </returns>
public static int CalculateDistanceFromAncestor(this IVisual visual, IVisual ancestor)
{
Contract.Requires<ArgumentNullException>(visual != null);
var result = 0;
while (visual != null && visual != ancestor)
{
++result;
visual = visual.VisualParent;
}
return visual != null ? result : -1;
}
/// <summary>
/// Tries to get the first common ancestor of two visuals.
/// </summary>

26
src/Gtk/Avalonia.Cairo/Avalonia.Cairo.v2.ncrunchproject

@ -1,26 +0,0 @@
<ProjectConfiguration>
<AutoDetectNugetBuildDependencies>true</AutoDetectNugetBuildDependencies>
<BuildPriority>1000</BuildPriority>
<CopyReferencedAssembliesToWorkspace>false</CopyReferencedAssembliesToWorkspace>
<ConsiderInconclusiveTestsAsPassing>false</ConsiderInconclusiveTestsAsPassing>
<PreloadReferencedAssemblies>false</PreloadReferencedAssemblies>
<AllowDynamicCodeContractChecking>true</AllowDynamicCodeContractChecking>
<AllowStaticCodeContractChecking>false</AllowStaticCodeContractChecking>
<AllowCodeAnalysis>false</AllowCodeAnalysis>
<IgnoreThisComponentCompletely>false</IgnoreThisComponentCompletely>
<RunPreBuildEvents>false</RunPreBuildEvents>
<RunPostBuildEvents>false</RunPostBuildEvents>
<PreviouslyBuiltSuccessfully>true</PreviouslyBuiltSuccessfully>
<InstrumentAssembly>true</InstrumentAssembly>
<PreventSigningOfAssembly>false</PreventSigningOfAssembly>
<AnalyseExecutionTimes>true</AnalyseExecutionTimes>
<DetectStackOverflow>true</DetectStackOverflow>
<IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
<DefaultTestTimeout>60000</DefaultTestTimeout>
<UseBuildConfiguration />
<UseBuildPlatform />
<ProxyProcessPath />
<UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
<MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
<BuildProcessArchitecture>x86</BuildProcessArchitecture>
</ProjectConfiguration>

1
src/Gtk/Avalonia.Cairo/CairoPlatform.cs

@ -26,6 +26,7 @@ namespace Avalonia.Cairo
{
using System.IO;
using global::Cairo;
using Rendering;
public class CairoPlatform : IPlatformRenderInterface
{

5
src/Gtk/Avalonia.Cairo/Media/RadialGradientBrushImpl.cs

@ -10,9 +10,10 @@ namespace Avalonia.Cairo
{
var center = brush.Center.ToPixels(destinationSize);
var gradientOrigin = brush.GradientOrigin.ToPixels(destinationSize);
var radius = brush.Radius;
var radius = brush.Radius * Math.Min(destinationSize.Width, destinationSize.Height);
this.PlatformBrush = new RadialGradient(center.X, center.Y, radius, gradientOrigin.X, gradientOrigin.Y, radius);
this.PlatformBrush = new RadialGradient(center.X, center.Y, 1, gradientOrigin.X, gradientOrigin.Y, radius);
this.PlatformBrush.Matrix = Matrix.Identity.ToCairo();
foreach (var stop in brush.GradientStops)
{

8
src/Gtk/Avalonia.Gtk/GtkPlatform.cs

@ -28,7 +28,7 @@ namespace Avalonia.Gtk
using Rendering;
using Gtk = global::Gtk;
public class GtkPlatform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader, IRendererFactory
public class GtkPlatform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader
{
private static readonly GtkPlatform s_instance = new GtkPlatform();
private static Thread _uiThread;
@ -53,7 +53,6 @@ namespace Avalonia.Gtk
.Bind<IKeyboardDevice>().ToConstant(GtkKeyboardDevice.Instance)
.Bind<IPlatformSettings>().ToConstant(s_instance)
.Bind<IPlatformThreadingInterface>().ToConstant(s_instance)
.Bind<IRendererFactory>().ToConstant(s_instance)
.Bind<IRenderLoop>().ToConstant(new DefaultRenderLoop(60))
.Bind<ISystemDialogImpl>().ToSingleton<SystemDialogImpl>()
.Bind<IPlatformIconLoader>().ToConstant(s_instance);
@ -112,11 +111,6 @@ namespace Avalonia.Gtk
return new PopupImpl();
}
public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop)
{
return new ImmediateRenderer(root);
}
public IWindowIconImpl LoadIcon(string fileName)
{
return new IconImpl(new Gdk.Pixbuf(fileName));

6
src/Gtk/Avalonia.Gtk/TopLevelImpl.cs

@ -10,6 +10,7 @@ using Gdk;
using Action = System.Action;
using WindowEdge = Avalonia.Controls.WindowEdge;
using GLib;
using Avalonia.Rendering;
namespace Avalonia.Gtk
{
@ -141,6 +142,11 @@ namespace Avalonia.Gtk
return new PopupImpl();
}
public IRenderer CreateRenderer(IRenderRoot root)
{
return new ImmediateRenderer(root);
}
public void Invalidate(Rect rect)
{
if (_widget?.GdkWindow != null)

8
src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs

@ -15,7 +15,7 @@ using Avalonia.Gtk3;
namespace Avalonia.Gtk3
{
public class Gtk3Platform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface, IRendererFactory
public class Gtk3Platform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface
{
internal static readonly Gtk3Platform Instance = new Gtk3Platform();
internal static readonly MouseDevice Mouse = new MouseDevice();
@ -38,7 +38,6 @@ namespace Avalonia.Gtk3
.Bind<IPlatformThreadingInterface>().ToConstant(Instance)
.Bind<ISystemDialogImpl>().ToSingleton<SystemDialog>()
.Bind<IRenderLoop>().ToConstant(new DefaultRenderLoop(60))
.Bind<IRendererFactory>().ToConstant(ImmediateRenderer.Factory)
.Bind<IPlatformIconLoader>().ToConstant(new PlatformIconLoader());
}
@ -52,11 +51,6 @@ namespace Avalonia.Gtk3
public IPopupImpl CreatePopup() => new PopupImpl();
public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop)
{
return new ImmediateRenderer(root);
}
public Size DoubleClickSize => new Size(4, 4);
public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(100); //STUB

6
src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs

@ -9,6 +9,7 @@ using Avalonia.Gtk3.Interop;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Platform;
using Avalonia.Rendering;
namespace Avalonia.Gtk3
{
@ -354,5 +355,10 @@ namespace Avalonia.Gtk3
IntPtr IPlatformHandle.Handle => Native.GetNativeGdkWindowHandle(Native.GtkWidgetGetWindow(GtkWidget));
public IEnumerable<object> Surfaces => new object[] {Handle, _framebuffer};
public IRenderer CreateRenderer(IRenderRoot root)
{
return new ImmediateRenderer(root);
}
}
}

6
src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs

@ -5,6 +5,7 @@ using System.Text;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Threading;
namespace Avalonia.LinuxFramebuffer
@ -24,6 +25,11 @@ namespace Avalonia.LinuxFramebuffer
mice.Event += e => Input?.Invoke(e);
}
public IRenderer CreateRenderer(IRenderRoot root)
{
return new ImmediateRenderer(root);
}
public void Dispose()
{
throw new NotSupportedException();

1
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

@ -34,7 +34,6 @@ namespace Avalonia.LinuxFramebuffer
.Bind<IStandardCursorFactory>().ToTransient<CursorFactoryStub>()
.Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)
.Bind<IPlatformSettings>().ToSingleton<PlatformSettings>()
.Bind<IRendererFactory>().ToConstant(ImmediateRenderer.Factory)
.Bind<IPlatformThreadingInterface>().ToConstant(PlatformThreadingInterface.Instance)
.Bind<IRenderLoop>().ToConstant(PlatformThreadingInterface.Instance);
}

1
src/Shared/SharedAssemblyInfo.cs

@ -3,6 +3,7 @@
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
[assembly: AssemblyCompany("")]
[assembly: AssemblyConfiguration("")]

26
src/Skia/Avalonia.Skia.Android/Avalonia.Skia.Android.v2.ncrunchproject

@ -1,26 +0,0 @@
<ProjectConfiguration>
<AutoDetectNugetBuildDependencies>true</AutoDetectNugetBuildDependencies>
<BuildPriority>1000</BuildPriority>
<CopyReferencedAssembliesToWorkspace>false</CopyReferencedAssembliesToWorkspace>
<ConsiderInconclusiveTestsAsPassing>false</ConsiderInconclusiveTestsAsPassing>
<PreloadReferencedAssemblies>false</PreloadReferencedAssemblies>
<AllowDynamicCodeContractChecking>true</AllowDynamicCodeContractChecking>
<AllowStaticCodeContractChecking>false</AllowStaticCodeContractChecking>
<AllowCodeAnalysis>false</AllowCodeAnalysis>
<IgnoreThisComponentCompletely>false</IgnoreThisComponentCompletely>
<RunPreBuildEvents>false</RunPreBuildEvents>
<RunPostBuildEvents>false</RunPostBuildEvents>
<PreviouslyBuiltSuccessfully>true</PreviouslyBuiltSuccessfully>
<InstrumentAssembly>true</InstrumentAssembly>
<PreventSigningOfAssembly>false</PreventSigningOfAssembly>
<AnalyseExecutionTimes>true</AnalyseExecutionTimes>
<DetectStackOverflow>true</DetectStackOverflow>
<IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
<DefaultTestTimeout>60000</DefaultTestTimeout>
<UseBuildConfiguration />
<UseBuildPlatform />
<ProxyProcessPath />
<UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
<MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
<BuildProcessArchitecture>x86</BuildProcessArchitecture>
</ProjectConfiguration>

26
src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.v2.ncrunchproject

@ -1,26 +0,0 @@
<ProjectConfiguration>
<AutoDetectNugetBuildDependencies>true</AutoDetectNugetBuildDependencies>
<BuildPriority>1000</BuildPriority>
<CopyReferencedAssembliesToWorkspace>false</CopyReferencedAssembliesToWorkspace>
<ConsiderInconclusiveTestsAsPassing>false</ConsiderInconclusiveTestsAsPassing>
<PreloadReferencedAssemblies>false</PreloadReferencedAssemblies>
<AllowDynamicCodeContractChecking>true</AllowDynamicCodeContractChecking>
<AllowStaticCodeContractChecking>false</AllowStaticCodeContractChecking>
<AllowCodeAnalysis>false</AllowCodeAnalysis>
<IgnoreThisComponentCompletely>true</IgnoreThisComponentCompletely>
<RunPreBuildEvents>false</RunPreBuildEvents>
<RunPostBuildEvents>false</RunPostBuildEvents>
<PreviouslyBuiltSuccessfully>false</PreviouslyBuiltSuccessfully>
<InstrumentAssembly>true</InstrumentAssembly>
<PreventSigningOfAssembly>false</PreventSigningOfAssembly>
<AnalyseExecutionTimes>true</AnalyseExecutionTimes>
<DetectStackOverflow>true</DetectStackOverflow>
<IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
<DefaultTestTimeout>60000</DefaultTestTimeout>
<UseBuildConfiguration />
<UseBuildPlatform />
<ProxyProcessPath />
<UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
<MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
<BuildProcessArchitecture>x86</BuildProcessArchitecture>
</ProjectConfiguration>

172
src/Skia/Avalonia.Skia.Desktop/RenderTarget.cs

@ -1,172 +0,0 @@
using System;
using Avalonia.Media;
using Avalonia.Platform;
using SkiaSharp;
#if WIN32
using Avalonia.Win32.Interop;
#endif
namespace Avalonia.Skia
{
internal partial class RenderTarget : IRenderTarget
{
public SKSurface Surface { get; protected set; }
public virtual DrawingContext CreateDrawingContext()
{
return
new DrawingContext(
new DrawingContextImpl(Surface.Canvas));
}
public void Dispose()
{
// Nothing to do here.
}
}
internal class WindowRenderTarget : RenderTarget
{
private readonly IPlatformHandle _hwnd;
SKBitmap _bitmap;
int Width { get; set; }
int Height { get; set; }
public WindowRenderTarget(IPlatformHandle hwnd)
{
_hwnd = hwnd;
FixSize();
}
private void FixSize()
{
int width, height;
GetPlatformWindowSize(out width, out height);
if (Width == width && Height == height)
return;
Width = width;
Height = height;
if (Surface != null)
{
Surface.Dispose();
}
if (_bitmap != null)
{
_bitmap.Dispose();
}
_bitmap = new SKBitmap(width, height, SKImageInfo.PlatformColorType, SKAlphaType.Premul);
IntPtr length;
var pixels = _bitmap.GetPixels(out length);
// Wrap the bitmap in a Surface and keep it cached
Surface = SKSurface.Create(_bitmap.Info, pixels, _bitmap.RowBytes);
}
private void GetPlatformWindowSize(out int w, out int h)
{
#if WIN32
UnmanagedMethods.RECT rc;
UnmanagedMethods.GetClientRect(_hwnd.Handle, out rc);
w = rc.right - rc.left;
h = rc.bottom - rc.top;
#else
throw new NotImplementedException();
#endif
}
private Size GetWindowDpiWin32()
{
if (UnmanagedMethods.ShCoreAvailable)
{
uint dpix, dpiy;
var monitor = UnmanagedMethods.MonitorFromWindow(
_hwnd.Handle,
UnmanagedMethods.MONITOR.MONITOR_DEFAULTTONEAREST);
if (UnmanagedMethods.GetDpiForMonitor(
monitor,
UnmanagedMethods.MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI,
out dpix,
out dpiy) == 0)
{
return new Size(dpix, dpiy);
}
}
return new Size(96, 96);
}
public override DrawingContext CreateDrawingContext()
{
FixSize();
var canvas = Surface.Canvas;
canvas.RestoreToCount(0);
canvas.Save();
canvas.Clear(SKColors.Red);
canvas.ResetMatrix();
double scale = 1.0;
var runtimeService = AvaloniaLocator.Current.GetService<IRuntimePlatform>();
if (runtimeService != null)
{
switch (runtimeService.GetRuntimeInfo().OperatingSystem)
{
case OperatingSystemType.WinNT:
var dpi = GetWindowDpiWin32();
scale = dpi.Width / 96.0;
break;
}
}
var result =
new DrawingContext(
new WindowDrawingContextImpl(this), Matrix.CreateScale(scale, scale));
return result;
}
public void Present()
{
_bitmap.LockPixels();
IntPtr length;
var pixels = _bitmap.GetPixels(out length);
#if WIN32
UnmanagedMethods.BITMAPINFO bmi = new UnmanagedMethods.BITMAPINFO();
bmi.biSize = UnmanagedMethods.SizeOf_BITMAPINFOHEADER;
bmi.biWidth = _bitmap.Width;
bmi.biHeight = -_bitmap.Height; // top-down image
bmi.biPlanes = 1;
bmi.biBitCount = 32;
bmi.biCompression = (uint)UnmanagedMethods.BitmapCompressionMode.BI_RGB;
bmi.biSizeImage = 0;
IntPtr hdc = UnmanagedMethods.GetDC(_hwnd.Handle);
int ret = UnmanagedMethods.SetDIBitsToDevice(hdc,
0, 0,
(uint)_bitmap.Width, (uint)_bitmap.Height,
0, 0,
0, (uint)_bitmap.Height,
pixels,
ref bmi,
(uint)UnmanagedMethods.DIBColorTable.DIB_RGB_COLORS);
UnmanagedMethods.ReleaseDC(_hwnd.Handle, hdc);
#else
throw new NotImplementedException();
#endif
_bitmap.UnlockPixels();
}
}
}

20
src/Skia/Avalonia.Skia/BitmapImpl.cs

@ -1,9 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
using SkiaSharp;
@ -12,6 +8,8 @@ namespace Avalonia.Skia
{
class BitmapImpl : IRenderTargetBitmapImpl, IWritableBitmapImpl
{
private Vector _dpi;
public SKBitmap Bitmap { get; private set; }
public BitmapImpl(SKBitmap bm)
@ -19,12 +17,14 @@ namespace Avalonia.Skia
Bitmap = bm;
PixelHeight = bm.Height;
PixelWidth = bm.Width;
_dpi = new Vector(96, 96);
}
public BitmapImpl(int width, int height, PixelFormat? fmt = null)
public BitmapImpl(int width, int height, Vector dpi, PixelFormat? fmt = null)
{
PixelHeight = height;
PixelWidth = width;
_dpi = dpi;
var colorType = fmt?.ToSkColorType() ?? SKImageInfo.PlatformColorType;
var runtime = AvaloniaLocator.Current?.GetService<IRuntimePlatform>()?.GetRuntimeInfo();
if (runtime?.IsDesktop == true && runtime?.OperatingSystem == OperatingSystemType.Linux)
@ -68,8 +68,8 @@ namespace Avalonia.Skia
{
private readonly SKSurface _surface;
public BitmapDrawingContext(SKBitmap bitmap, IVisualBrushRenderer visualBrushRenderer)
: this(CreateSurface(bitmap), visualBrushRenderer)
public BitmapDrawingContext(SKBitmap bitmap, Vector dpi, IVisualBrushRenderer visualBrushRenderer)
: this(CreateSurface(bitmap), dpi, visualBrushRenderer)
{
}
@ -83,8 +83,8 @@ namespace Avalonia.Skia
return rv;
}
public BitmapDrawingContext(SKSurface surface, IVisualBrushRenderer visualBrushRenderer)
: base(surface.Canvas, visualBrushRenderer)
public BitmapDrawingContext(SKSurface surface, Vector dpi, IVisualBrushRenderer visualBrushRenderer)
: base(surface.Canvas, dpi, visualBrushRenderer)
{
_surface = surface;
}
@ -98,7 +98,7 @@ namespace Avalonia.Skia
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
{
return new BitmapDrawingContext(Bitmap, visualBrushRenderer);
return new BitmapDrawingContext(Bitmap, _dpi, visualBrushRenderer);
}
public void Save(Stream stream)

19
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -11,6 +11,7 @@ namespace Avalonia.Skia
{
internal class DrawingContextImpl : IDrawingContextImpl
{
private readonly Vector _dpi;
private readonly Matrix? _postTransform;
private readonly IDisposable[] _disposables;
private readonly IVisualBrushRenderer _visualBrushRenderer;
@ -20,12 +21,13 @@ namespace Avalonia.Skia
public DrawingContextImpl(
SKCanvas canvas,
Vector dpi,
IVisualBrushRenderer visualBrushRenderer,
Matrix? postTransform = null,
params IDisposable[] disposables)
{
if (postTransform.HasValue && !postTransform.Value.IsIdentity)
_postTransform = postTransform;
_dpi = dpi;
if (dpi.X != 96 || dpi.Y != 96)
_postTransform = Matrix.CreateScale(dpi.X / 96, dpi.Y / 96);
_visualBrushRenderer = visualBrushRenderer;
_disposables = disposables;
Canvas = canvas;
@ -49,6 +51,13 @@ namespace Avalonia.Skia
}
}
public void DrawImage(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
{
PushOpacityMask(opacityMask, opacityMaskRect);
DrawImage(source, 1, new Rect(0, 0, source.PixelWidth, source.PixelHeight), destRect);
PopOpacityMask();
}
public void DrawLine(Pen pen, Point p1, Point p2)
{
using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
@ -204,7 +213,7 @@ namespace Avalonia.Skia
if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1)
{
var intermediate = new BitmapImpl((int)intermediateSize.Width, (int)intermediateSize.Height);
var intermediate = new BitmapImpl((int)intermediateSize.Width, (int)intermediateSize.Height, _dpi);
using (var ctx = intermediate.CreateDrawingContext(_visualBrushRenderer))
{
@ -229,7 +238,7 @@ namespace Avalonia.Skia
if (tileBrush != null && tileBrushImage != null)
{
var calc = new TileBrushCalculator(tileBrush, new Size(tileBrushImage.PixelWidth, tileBrushImage.PixelHeight), targetSize);
var bitmap = new BitmapImpl((int)calc.IntermediateSize.Width, (int)calc.IntermediateSize.Height);
var bitmap = new BitmapImpl((int)calc.IntermediateSize.Width, (int)calc.IntermediateSize.Height, _dpi);
rv.AddDisposable(bitmap);
using (var context = bitmap.CreateDrawingContext(null))
{

1
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@ -615,6 +615,7 @@ namespace Avalonia.Skia
if (brush != null)
{
brush = brush.ToImmutable();
_foregroundBrushes.Insert(0, new KeyValuePair<FBrushRange, IBrush>(key, brush));
}
}

3
src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs

@ -76,8 +76,7 @@ namespace Avalonia.Skia
canvas.RestoreToCount(0);
canvas.Save();
canvas.ResetMatrix();
var scale = Matrix.CreateScale(fb.Dpi.X / 96, fb.Dpi.Y / 96);
return new DrawingContextImpl(canvas, visualBrushRenderer, scale, canvas, surface, shim, fb);
return new DrawingContextImpl(canvas, fb.Dpi, visualBrushRenderer, canvas, surface, shim, fb);
}
}
}

4
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -77,7 +77,7 @@ namespace Avalonia.Skia
if (height < 1)
throw new ArgumentException("Height can't be less than 1", nameof(height));
return new BitmapImpl(width, height);
return new BitmapImpl(width, height, new Vector(dpiX, dpiY));
}
public virtual IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
@ -90,7 +90,7 @@ namespace Avalonia.Skia
public IWritableBitmapImpl CreateWritableBitmap(int width, int height, PixelFormat? format = null)
{
return new BitmapImpl(width, height, format);
return new BitmapImpl(width, height, new Vector(96, 96), format);
}
}
}

11
src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs

@ -90,6 +90,17 @@ namespace Avalonia.Skia
}
}
public static TextAlignment ToAvalonia(this SKTextAlign a)
{
switch (a)
{
default:
case SKTextAlign.Left: return TextAlignment.Left;
case SKTextAlign.Center: return TextAlignment.Center;
case SKTextAlign.Right: return TextAlignment.Right;
}
}
public static SKPath Clone(this SKPath src)
{
return src != null ? new SKPath(src) : null;

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

@ -191,4 +191,4 @@ namespace Avalonia.Direct2D1
return new WicBitmapImpl(s_imagingFactory, format, data, width, height, stride);
}
}
}
}

8
src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs

@ -25,11 +25,11 @@ namespace Avalonia.Direct2D1.Media
}).ToArray();
var centerPoint = brush.Center.ToPixels(destinationSize);
var GradientOriginOffset = brush.GradientOrigin.ToPixels(destinationSize);
var gradientOrigin = brush.GradientOrigin.ToPixels(destinationSize) - centerPoint;
// Note: Direct2D supports RadiusX and RadiusY but Cairo backend supports only Radius property
var radiusX = brush.Radius;
var radiusY = brush.Radius;
var radiusX = brush.Radius * destinationSize.Width;
var radiusY = brush.Radius * destinationSize.Height;
using (var stops = new SharpDX.Direct2D1.GradientStopCollection(
target,
@ -41,7 +41,7 @@ namespace Avalonia.Direct2D1.Media
new SharpDX.Direct2D1.RadialGradientBrushProperties
{
Center = centerPoint.ToSharpDX(),
GradientOriginOffset = GradientOriginOffset.ToSharpDX(),
GradientOriginOffset = gradientOrigin.ToSharpDX(),
RadiusX = (float)radiusX,
RadiusY = (float)radiusY
},

6
src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs

@ -13,6 +13,7 @@ using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Layout;
using Avalonia.Platform;
using Avalonia.Rendering;
using Key = Avalonia.Input.Key;
using KeyEventArgs = System.Windows.Input.KeyEventArgs;
using MouseButton = System.Windows.Input.MouseButton;
@ -88,6 +89,11 @@ namespace Avalonia.Win32.Interop.Wpf
_ttl.ScalingChanged?.Invoke(_ttl.Scaling);
}
public IRenderer CreateRenderer(IRenderRoot root)
{
return new ImmediateRenderer(root);
}
public void Dispose()
{
_ttl.Closed?.Invoke();

2
src/Windows/Avalonia.Win32/FramebufferManager.cs

@ -25,7 +25,7 @@ namespace Avalonia.Win32
UnmanagedMethods.GetClientRect(_hwnd, out rc);
var width = rc.right - rc.left;
var height = rc.bottom - rc.top;
if (_fb == null || _fb.Width != width || _fb.Height != height)
if ((_fb == null || _fb.Width != width || _fb.Height != height) && width > 0 && height > 0)
{
_fb?.Deallocate();
_fb = null;

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

@ -25,17 +25,21 @@ namespace Avalonia
{
public static class Win32ApplicationExtensions
{
public static T UseWin32<T>(this T builder) where T : AppBuilderBase<T>, new()
public static T UseWin32<T>(
this T builder,
bool deferredRendering = true)
where T : AppBuilderBase<T>, new()
{
builder.UseWindowingSubsystem(Win32.Win32Platform.Initialize, "Win32");
return builder;
return builder.UseWindowingSubsystem(
() => Win32.Win32Platform.Initialize(deferredRendering),
"Win32");
}
}
}
namespace Avalonia.Win32
{
partial class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader, IRendererFactory
partial class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader
{
private static readonly Win32Platform s_instance = new Win32Platform();
private static uint _uiThread;
@ -54,6 +58,8 @@ namespace Avalonia.Win32
CreateMessageWindow();
}
public static bool UseDeferredRendering { get; set; }
public Size DoubleClickSize => new Size(
UnmanagedMethods.GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CXDOUBLECLK),
UnmanagedMethods.GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CYDOUBLECLK));
@ -61,6 +67,11 @@ namespace Avalonia.Win32
public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(UnmanagedMethods.GetDoubleClickTime());
public static void Initialize()
{
Initialize(true);
}
public static void Initialize(bool deferredRendering = true)
{
AvaloniaLocator.CurrentMutable
.Bind<IClipboard>().ToSingleton<ClipboardImpl>()
@ -69,11 +80,11 @@ namespace Avalonia.Win32
.Bind<IPlatformSettings>().ToConstant(s_instance)
.Bind<IPlatformThreadingInterface>().ToConstant(s_instance)
.Bind<IRenderLoop>().ToConstant(new RenderLoop(60))
.Bind<IRendererFactory>().ToConstant(s_instance)
.Bind<ISystemDialogImpl>().ToSingleton<SystemDialogImpl>()
.Bind<IWindowingPlatform>().ToConstant(s_instance)
.Bind<IPlatformIconLoader>().ToConstant(s_instance);
UseDeferredRendering = deferredRendering;
_uiThread = UnmanagedMethods.GetCurrentThreadId();
}
@ -197,10 +208,5 @@ namespace Avalonia.Win32
{
return new PopupImpl();
}
public IRenderer CreateRenderer(IRenderRoot root, IRenderLoop renderLoop)
{
return new ImmediateRenderer(root);
}
}
}

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

@ -15,6 +15,7 @@ using Avalonia.Platform;
using Avalonia.Win32.Input;
using Avalonia.Win32.Interop;
using static Avalonia.Win32.Interop.UnmanagedMethods;
using Avalonia.Rendering;
#if NETSTANDARD
using Win32Exception = Avalonia.Win32.NetStandard.AvaloniaWin32Exception;
#endif
@ -90,6 +91,15 @@ namespace Avalonia.Win32
}
}
public IRenderer CreateRenderer(IRenderRoot root)
{
var loop = AvaloniaLocator.Current.GetService<IRenderLoop>();
return Win32Platform.UseDeferredRendering ?
(IRenderer)new DeferredRenderer(root, loop) :
new ImmediateRenderer(root);
}
public void Resize(Size value)
{
if (value != ClientSize)

8
src/iOS/Avalonia.iOS/TopLevelImpl.cs

@ -18,6 +18,7 @@ using Avalonia.iOS.Specific;
using ObjCRuntime;
using Avalonia.Controls;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Rendering;
namespace Avalonia.iOS
{
@ -62,7 +63,12 @@ namespace Avalonia.iOS
public Size ClientSize => Bounds.Size.ToAvalonia();
public IMouseDevice MouseDevice => iOSPlatform.MouseDevice;
public IRenderer CreateRenderer(IRenderRoot root)
{
return new ImmediateRenderer(root);
}
public override void Draw(CGRect rect)
{
Paint?.Invoke(new Rect(rect.X, rect.Y, rect.Width, rect.Height));

1
src/iOS/Avalonia.iOS/iOSPlatform.cs

@ -57,7 +57,6 @@ namespace Avalonia.iOS
//.Bind<ISystemDialogImpl>().ToTransient<SystemDialogImpl>()
.Bind<IStandardCursorFactory>().ToTransient<CursorFactory>()
.Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)
.Bind<IRendererFactory>().ToConstant(ImmediateRenderer.Factory)
.Bind<IPlatformSettings>().ToSingleton<PlatformSettings>()
.Bind<IPlatformThreadingInterface>().ToConstant(PlatformThreadingInterface.Instance)
.Bind<IPlatformIconLoader>().ToSingleton<PlatformIconLoader>()

1
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs

@ -317,6 +317,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
{
public IRenderer Renderer { get; }
public Size ClientSize { get; }
public double RenderScaling => 1;
public IRenderTarget CreateRenderTarget()
{

1
tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs

@ -1034,6 +1034,7 @@ namespace Avalonia.Controls.UnitTests.Presenters
{
public IRenderer Renderer { get; }
public Size ClientSize { get; }
public double RenderScaling => 1;
public IRenderTarget CreateRenderTarget()
{

58
tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs

@ -27,6 +27,8 @@ namespace Avalonia.Controls.UnitTests
{
var impl = Mock.Of<IWindowBaseImpl>(x => x.Scaling == 1);
Mock.Get(impl).Setup(x => x.Resize(It.IsAny<Size>())).Callback(() => { });
var target = new TestWindowBase(impl)
{
Template = CreateTemplate(),
@ -183,6 +185,56 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Showing_Should_Start_Renderer()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var renderer = new Mock<IRenderer>();
var target = new TestWindowBase(renderer.Object);
target.Show();
renderer.Verify(x => x.Start(), Times.Once);
}
}
[Fact]
public void Hiding_Should_Stop_Renderer()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var renderer = new Mock<IRenderer>();
var target = new TestWindowBase(renderer.Object);
target.Show();
target.Hide();
renderer.Verify(x => x.Stop(), Times.Once);
}
}
[Fact]
public void Renderer_Should_Be_Disposed_When_Impl_Signals_Close()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var renderer = new Mock<IRenderer>();
var windowImpl = new Mock<IPopupImpl>();
windowImpl.Setup(x => x.Scaling).Returns(1);
windowImpl.SetupProperty(x => x.Closed);
windowImpl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>())).Returns(renderer.Object);
var target = new TestWindowBase(windowImpl.Object);
target.Show();
windowImpl.Object.Closed();
renderer.Verify(x => x.Dispose(), Times.Once);
}
}
private FuncControlTemplate<TestWindowBase> CreateTemplate()
{
return new FuncControlTemplate<TestWindowBase>(x =>
@ -197,8 +249,10 @@ namespace Avalonia.Controls.UnitTests
{
public bool IsClosed { get; private set; }
public TestWindowBase()
: base(Mock.Of<IWindowBaseImpl>(x => x.Scaling == 1))
public TestWindowBase(IRenderer renderer = null)
: base(Mock.Of<IWindowBaseImpl>(x =>
x.Scaling == 1 &&
x.CreateRenderer(It.IsAny<IRenderRoot>()) == renderer))
{
}

53
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@ -4,8 +4,10 @@
// </copyright>
// -----------------------------------------------------------------------
using System;
using System.Collections.Generic;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.UnitTests;
using Moq;
using Xunit;
@ -185,11 +187,62 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Showing_Should_Start_Renderer()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var renderer = new Mock<IRenderer>();
var target = new Window(CreateImpl(renderer));
target.Show();
renderer.Verify(x => x.Start(), Times.Once);
}
}
[Fact]
public void ShowDialog_Should_Start_Renderer()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var renderer = new Mock<IRenderer>();
var target = new Window(CreateImpl(renderer));
target.Show();
renderer.Verify(x => x.Start(), Times.Once);
}
}
[Fact]
public void Hiding_Should_Stop_Renderer()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var renderer = new Mock<IRenderer>();
var target = new Window(CreateImpl(renderer));
target.Show();
target.Hide();
renderer.Verify(x => x.Stop(), Times.Once);
}
}
private void ClearOpenWindows()
{
// HACK: We really need a decent way to have "statics" that can be scoped to
// AvaloniaLocator scopes.
((IList<Window>)Window.OpenWindows).Clear();
}
private IWindowImpl CreateImpl(Mock<IRenderer> renderer)
{
return Mock.Of<IWindowImpl>(x =>
x.Scaling == 1 &&
x.CreateRenderer(It.IsAny<IRenderRoot>()) == renderer.Object);
}
}
}

29
tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj

@ -3,6 +3,35 @@
<TargetFrameworks>net461;netcoreapp1.1</TargetFrameworks>
<OutputType>Library</OutputType>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<DocumentationFile>bin\Debug\Avalonia.Input.UnitTests.XML</DocumentationFile>
<NoWarn>CS1591</NoWarn>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
<Reference Include="WindowsBase" />
</ItemGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\Moq.props" />
<Import Project="..\..\build\XUnit.props" />

85
tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs

@ -0,0 +1,85 @@
using Avalonia.Controls;
using Avalonia.Input.Raw;
using Avalonia.Layout;
using Avalonia.Rendering;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
using System;
using Xunit;
namespace Avalonia.Input.UnitTests
{
public class MouseDeviceTests
{
[Fact]
public void MouseMove_Should_Update_PointerOver()
{
var renderer = new Mock<IRenderer>();
using (TestApplication(renderer.Object))
{
var inputManager = InputManager.Instance;
Canvas canvas;
Border border;
Decorator decorator;
var root = new TestRoot
{
MouseDevice = new MouseDevice(),
Renderer = renderer.Object,
Child = new Panel
{
Children =
{
(canvas = new Canvas()),
(border = new Border
{
Child = decorator = new Decorator(),
})
}
}
};
renderer.Setup(x => x.HitTest(It.IsAny<Point>(), It.IsAny<Func<IVisual, bool>>()))
.Returns(new[] { decorator });
inputManager.ProcessInput(new RawMouseEventArgs(
root.MouseDevice,
0,
root,
RawMouseEventType.Move,
new Point(),
InputModifiers.None));
Assert.True(decorator.IsPointerOver);
Assert.True(border.IsPointerOver);
Assert.False(canvas.IsPointerOver);
Assert.True(root.IsPointerOver);
renderer.Setup(x => x.HitTest(It.IsAny<Point>(), It.IsAny<Func<IVisual, bool>>()))
.Returns(new[] { canvas });
inputManager.ProcessInput(new RawMouseEventArgs(
root.MouseDevice,
0,
root,
RawMouseEventType.Move,
new Point(),
InputModifiers.None));
Assert.False(decorator.IsPointerOver);
Assert.False(border.IsPointerOver);
Assert.True(canvas.IsPointerOver);
Assert.True(root.IsPointerOver);
}
}
private IDisposable TestApplication(IRenderer renderer)
{
return UnitTestApplication.Start(
new TestServices(inputManager: new InputManager()));
}
}
}

14
tests/Avalonia.LeakTests/ControlTests.cs

@ -318,12 +318,11 @@ namespace Avalonia.LeakTests
var impl = new Mock<IWindowImpl>();
impl.SetupGet(x => x.Scaling).Returns(1);
impl.SetupProperty(x => x.Closed);
impl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>())).Returns(renderer.Object);
impl.Setup(x => x.Dispose()).Callback(() => impl.Object.Closed());
AvaloniaLocator.CurrentMutable.Bind<IWindowingPlatform>()
.ToConstant(new MockWindowingPlatform(() => impl.Object));
AvaloniaLocator.CurrentMutable.Bind<IRendererFactory>()
.ToConstant(new MockRendererFactory(renderer.Object));
var window = new Window()
{
Content = new Button()
@ -336,8 +335,7 @@ namespace Avalonia.LeakTests
private IDisposable Start()
{
var services = TestServices.StyledWindow.With(renderer: (root, loop) => new NullRenderer());
return UnitTestApplication.Start(services);
return UnitTestApplication.Start(TestServices.StyledWindow);
}
private class Node
@ -368,6 +366,14 @@ namespace Avalonia.LeakTests
public void Resized(Size size)
{
}
public void Start()
{
}
public void Stop()
{
}
}
}
}

27
tests/Avalonia.RenderTests/Avalonia.Cairo.RenderTests.v2.ncrunchproject

@ -1,27 +0,0 @@
<ProjectConfiguration>
<AutoDetectNugetBuildDependencies>true</AutoDetectNugetBuildDependencies>
<BuildPriority>1000</BuildPriority>
<CopyReferencedAssembliesToWorkspace>false</CopyReferencedAssembliesToWorkspace>
<ConsiderInconclusiveTestsAsPassing>false</ConsiderInconclusiveTestsAsPassing>
<PreloadReferencedAssemblies>false</PreloadReferencedAssemblies>
<AllowDynamicCodeContractChecking>true</AllowDynamicCodeContractChecking>
<AllowStaticCodeContractChecking>false</AllowStaticCodeContractChecking>
<AllowCodeAnalysis>false</AllowCodeAnalysis>
<IgnoreThisComponentCompletely>false</IgnoreThisComponentCompletely>
<RunPreBuildEvents>false</RunPreBuildEvents>
<RunPostBuildEvents>false</RunPostBuildEvents>
<PreviouslyBuiltSuccessfully>true</PreviouslyBuiltSuccessfully>
<InstrumentAssembly>true</InstrumentAssembly>
<PreventSigningOfAssembly>false</PreventSigningOfAssembly>
<AnalyseExecutionTimes>true</AnalyseExecutionTimes>
<DetectStackOverflow>true</DetectStackOverflow>
<IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
<DefaultTestTimeout>60000</DefaultTestTimeout>
<UseBuildConfiguration />
<UseBuildPlatform />
<ProxyProcessPath />
<UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
<MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
<BuildProcessArchitecture>x86</BuildProcessArchitecture>
<HiddenWarnings>AbnormalReferenceResolution</HiddenWarnings>
</ProjectConfiguration>

26
tests/Avalonia.RenderTests/Avalonia.Direct2D1.RenderTests.v2.ncrunchproject

@ -1,26 +0,0 @@
<ProjectConfiguration>
<AutoDetectNugetBuildDependencies>true</AutoDetectNugetBuildDependencies>
<BuildPriority>1000</BuildPriority>
<CopyReferencedAssembliesToWorkspace>false</CopyReferencedAssembliesToWorkspace>
<ConsiderInconclusiveTestsAsPassing>false</ConsiderInconclusiveTestsAsPassing>
<PreloadReferencedAssemblies>false</PreloadReferencedAssemblies>
<AllowDynamicCodeContractChecking>true</AllowDynamicCodeContractChecking>
<AllowStaticCodeContractChecking>false</AllowStaticCodeContractChecking>
<AllowCodeAnalysis>false</AllowCodeAnalysis>
<IgnoreThisComponentCompletely>false</IgnoreThisComponentCompletely>
<RunPreBuildEvents>false</RunPreBuildEvents>
<RunPostBuildEvents>false</RunPostBuildEvents>
<PreviouslyBuiltSuccessfully>true</PreviouslyBuiltSuccessfully>
<InstrumentAssembly>true</InstrumentAssembly>
<PreventSigningOfAssembly>false</PreventSigningOfAssembly>
<AnalyseExecutionTimes>true</AnalyseExecutionTimes>
<DetectStackOverflow>true</DetectStackOverflow>
<IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
<DefaultTestTimeout>60000</DefaultTestTimeout>
<UseBuildConfiguration />
<UseBuildPlatform />
<ProxyProcessPath />
<UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
<MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
<BuildProcessArchitecture>x86</BuildProcessArchitecture>
</ProjectConfiguration>

2
tests/Avalonia.RenderTests/Avalonia.RenderTests.projitems

@ -9,7 +9,9 @@
<Import_RootNamespace>Avalonia.RenderTests</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)Controls\CustomRenderTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)GeometryClippingTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Media\RadialGradientBrushTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Media\BitmapTests.cs" />
<Compile Include="$(MSBuildThisFileDirectory)OpacityMaskTests.cs" />
<Compile Include="Media\FormattedTextImplTests.cs" />

26
tests/Avalonia.RenderTests/Avalonia.Skia.RenderTests.v2.ncrunchproject

@ -1,26 +0,0 @@
<ProjectConfiguration>
<AutoDetectNugetBuildDependencies>true</AutoDetectNugetBuildDependencies>
<BuildPriority>1000</BuildPriority>
<CopyReferencedAssembliesToWorkspace>false</CopyReferencedAssembliesToWorkspace>
<ConsiderInconclusiveTestsAsPassing>false</ConsiderInconclusiveTestsAsPassing>
<PreloadReferencedAssemblies>false</PreloadReferencedAssemblies>
<AllowDynamicCodeContractChecking>true</AllowDynamicCodeContractChecking>
<AllowStaticCodeContractChecking>false</AllowStaticCodeContractChecking>
<AllowCodeAnalysis>false</AllowCodeAnalysis>
<IgnoreThisComponentCompletely>true</IgnoreThisComponentCompletely>
<RunPreBuildEvents>false</RunPreBuildEvents>
<RunPostBuildEvents>false</RunPostBuildEvents>
<PreviouslyBuiltSuccessfully>false</PreviouslyBuiltSuccessfully>
<InstrumentAssembly>true</InstrumentAssembly>
<PreventSigningOfAssembly>false</PreventSigningOfAssembly>
<AnalyseExecutionTimes>true</AnalyseExecutionTimes>
<DetectStackOverflow>true</DetectStackOverflow>
<IncludeStaticReferencesInWorkspace>true</IncludeStaticReferencesInWorkspace>
<DefaultTestTimeout>60000</DefaultTestTimeout>
<UseBuildConfiguration />
<UseBuildPlatform />
<ProxyProcessPath />
<UseCPUArchitecture>AutoDetect</UseCPUArchitecture>
<MSTestThreadApartmentState>STA</MSTestThreadApartmentState>
<BuildProcessArchitecture>x86</BuildProcessArchitecture>
</ProjectConfiguration>

61
tests/Avalonia.RenderTests/Controls/BorderTests.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 System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
@ -22,7 +23,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
}
[Fact]
public void Border_1px_Border()
public async Task Border_1px_Border()
{
Decorator target = new Decorator
{
@ -36,12 +37,12 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
}
};
RenderToFile(target);
await RenderToFile(target);
CompareImages();
}
[Fact]
public void Border_2px_Border()
public async Task Border_2px_Border()
{
Decorator target = new Decorator
{
@ -55,12 +56,12 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
}
};
RenderToFile(target);
await RenderToFile(target);
CompareImages();
}
[Fact]
public void Border_Fill()
public async Task Border_Fill()
{
Decorator target = new Decorator
{
@ -73,12 +74,12 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
}
};
RenderToFile(target);
await RenderToFile(target);
CompareImages();
}
[Fact]
public void Border_Brush_Offsets_Content()
public async Task Border_Brush_Offsets_Content()
{
Decorator target = new Decorator
{
@ -96,12 +97,12 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
}
};
RenderToFile(target);
await RenderToFile(target);
CompareImages();
}
[Fact]
public void Border_Padding_Offsets_Content()
public async Task Border_Padding_Offsets_Content()
{
Decorator target = new Decorator
{
@ -120,12 +121,12 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
}
};
RenderToFile(target);
await RenderToFile(target);
CompareImages();
}
[Fact]
public void Border_Margin_Offsets_Content()
public async Task Border_Margin_Offsets_Content()
{
Decorator target = new Decorator
{
@ -144,7 +145,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
}
};
RenderToFile(target);
await RenderToFile(target);
CompareImages();
}
@ -153,7 +154,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
#else
[Fact]
#endif
public void Border_Centers_Content_Horizontally()
public async Task Border_Centers_Content_Horizontally()
{
Decorator target = new Decorator
{
@ -175,7 +176,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
}
};
RenderToFile(target);
await RenderToFile(target);
CompareImages();
}
@ -184,7 +185,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
#else
[Fact]
#endif
public void Border_Centers_Content_Vertically()
public async Task Border_Centers_Content_Vertically()
{
Decorator target = new Decorator
{
@ -206,7 +207,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
}
};
RenderToFile(target);
await RenderToFile(target);
CompareImages();
}
@ -215,7 +216,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
#else
[Fact]
#endif
public void Border_Stretches_Content_Horizontally()
public async Task Border_Stretches_Content_Horizontally()
{
Decorator target = new Decorator
{
@ -237,7 +238,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
}
};
RenderToFile(target);
await RenderToFile(target);
CompareImages();
}
@ -246,7 +247,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
#else
[Fact]
#endif
public void Border_Stretches_Content_Vertically()
public async Task Border_Stretches_Content_Vertically()
{
Decorator target = new Decorator
{
@ -268,7 +269,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
}
};
RenderToFile(target);
await RenderToFile(target);
CompareImages();
}
@ -277,7 +278,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
#else
[Fact]
#endif
public void Border_Left_Aligns_Content()
public async Task Border_Left_Aligns_Content()
{
Decorator target = new Decorator
{
@ -299,7 +300,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
}
};
RenderToFile(target);
await RenderToFile(target);
CompareImages();
}
@ -308,7 +309,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
#else
[Fact]
#endif
public void Border_Right_Aligns_Content()
public async Task Border_Right_Aligns_Content()
{
Decorator target = new Decorator
{
@ -330,7 +331,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
}
};
RenderToFile(target);
await RenderToFile(target);
CompareImages();
}
@ -339,7 +340,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
#else
[Fact]
#endif
public void Border_Top_Aligns_Content()
public async Task Border_Top_Aligns_Content()
{
Decorator target = new Decorator
{
@ -361,7 +362,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
}
};
RenderToFile(target);
await RenderToFile(target);
CompareImages();
}
@ -370,7 +371,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
#else
[Fact]
#endif
public void Border_Bottom_Aligns_Content()
public async Task Border_Bottom_Aligns_Content()
{
Decorator target = new Decorator
{
@ -392,12 +393,12 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
}
};
RenderToFile(target);
await RenderToFile(target);
CompareImages();
}
[Fact]
public void Border_Nested_Rotate()
public async Task Border_Nested_Rotate()
{
Decorator target = new Decorator
{
@ -420,7 +421,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
}
};
RenderToFile(target);
await RenderToFile(target);
CompareImages();
}
}

162
tests/Avalonia.RenderTests/Controls/CustomRenderTests.cs

@ -0,0 +1,162 @@
// 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.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using Xunit;
#if AVALONIA_CAIRO
namespace Avalonia.Cairo.RenderTests.Controls
#elif AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests
#else
namespace Avalonia.Direct2D1.RenderTests.Controls
#endif
{
public class CustomRenderTests : TestBase
{
public CustomRenderTests()
: base(@"Controls\CustomRender")
{
}
[Fact]
public async Task Clip()
{
Decorator target = new Decorator
{
Padding = new Thickness(8),
Width = 200,
Height = 200,
Child = new CustomRenderer((control, context) =>
{
context.FillRectangle(
Brushes.Red,
new Rect(control.Bounds.Size),
4);
using (context.PushClip(new Rect(control.Bounds.Size).Deflate(20)))
{
context.FillRectangle(
Brushes.Blue,
new Rect(control.Bounds.Size),
4);
}
}),
};
await RenderToFile(target);
CompareImages();
}
[Fact]
public async Task GeometryClip()
{
Decorator target = new Decorator
{
Padding = new Thickness(8),
Width = 200,
Height = 200,
Child = new CustomRenderer((control, context) =>
{
var clip = new EllipseGeometry(new Rect(control.Bounds.Size));
context.FillRectangle(
Brushes.Red,
new Rect(control.Bounds.Size),
4);
using (context.PushGeometryClip(clip))
{
context.FillRectangle(
Brushes.Blue,
new Rect(control.Bounds.Size),
4);
}
}),
};
await RenderToFile(target);
CompareImages();
}
[Fact]
public async Task Opacity()
{
Decorator target = new Decorator
{
Padding = new Thickness(8),
Width = 200,
Height = 200,
Child = new CustomRenderer((control, context) =>
{
context.FillRectangle(
Brushes.Red,
new Rect(control.Bounds.Size),
4);
using (context.PushOpacity(0.5))
{
context.FillRectangle(
Brushes.Blue,
new Rect(control.Bounds.Size).Deflate(20),
4);
}
}),
};
await RenderToFile(target);
CompareImages();
}
[Fact]
public async Task OpacityMask()
{
Decorator target = new Decorator
{
Padding = new Thickness(8),
Width = 200,
Height = 200,
Child = new CustomRenderer((control, context) =>
{
var mask = new LinearGradientBrush
{
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
GradientStops = new[]
{
new GradientStop(Color.FromUInt32(0xffffffff), 0),
new GradientStop(Color.FromUInt32(0x00ffffff), 1)
},
};
context.FillRectangle(
Brushes.Red,
new Rect(control.Bounds.Size),
4);
using (context.PushOpacityMask(mask, new Rect(control.Bounds.Size)))
{
context.FillRectangle(
Brushes.Blue,
new Rect(control.Bounds.Size).Deflate(20),
4);
}
}),
};
await RenderToFile(target);
CompareImages();
}
class CustomRenderer : Control
{
private Action<CustomRenderer, DrawingContext> _render;
public CustomRenderer(Action<CustomRenderer, DrawingContext> render) => _render = render;
public override void Render(DrawingContext context) => _render(this, context);
}
}
}

17
tests/Avalonia.RenderTests/Controls/ImageTests.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.IO;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Media.Imaging;
@ -26,7 +27,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
}
[Fact]
public void Image_Stretch_None()
public async Task Image_Stretch_None()
{
Decorator target = new Decorator
{
@ -44,12 +45,12 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
}
};
RenderToFile(target);
await RenderToFile(target);
CompareImages();
}
[Fact]
public void Image_Stretch_Fill()
public async Task Image_Stretch_Fill()
{
Decorator target = new Decorator
{
@ -67,12 +68,12 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
}
};
RenderToFile(target);
await RenderToFile(target);
CompareImages();
}
[Fact]
public void Image_Stretch_Uniform()
public async Task Image_Stretch_Uniform()
{
Decorator target = new Decorator
{
@ -90,12 +91,12 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
}
};
RenderToFile(target);
await RenderToFile(target);
CompareImages();
}
[Fact]
public void Image_Stretch_UniformToFill()
public async Task Image_Stretch_UniformToFill()
{
Decorator target = new Decorator
{
@ -113,7 +114,7 @@ namespace Avalonia.Direct2D1.RenderTests.Controls
}
};
RenderToFile(target);
await RenderToFile(target);
CompareImages();
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save