339 changed files with 10918 additions and 3645 deletions
@ -1,13 +1,7 @@ |
|||||
[submodule "src/Avalonia.ReactiveUI/src"] |
|
||||
path = src/Avalonia.ReactiveUI/src |
|
||||
url = https://github.com/reactiveui/ReactiveUI.git |
|
||||
[submodule "src/Avalonia.HtmlRenderer/external"] |
[submodule "src/Avalonia.HtmlRenderer/external"] |
||||
path = src/Avalonia.HtmlRenderer/external |
path = src/Avalonia.HtmlRenderer/external |
||||
url = https://github.com/AvaloniaUI/HTML-Renderer.git |
url = https://github.com/AvaloniaUI/HTML-Renderer.git |
||||
branch = perspex-pcl |
branch = perspex-pcl |
||||
[submodule "src/Markup/Avalonia.Markup.Xaml/OmniXAML"] |
[submodule "src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github"] |
||||
path = src/Markup/Avalonia.Markup.Xaml/OmniXAML |
path = src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github |
||||
url = https://github.com/AvaloniaUI/OmniXAML.git |
url = https://github.com/AvaloniaUI/Portable.Xaml.git |
||||
[submodule "src/Markup/Avalonia.Markup.Xaml/glass"] |
|
||||
path = src/Markup/Avalonia.Markup.Xaml/glass |
|
||||
url = https://github.com/SuperJMN/glass |
|
||||
|
|||||
@ -0,0 +1,5 @@ |
|||||
|
<ProjectConfiguration> |
||||
|
<Settings> |
||||
|
<PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully> |
||||
|
</Settings> |
||||
|
</ProjectConfiguration> |
||||
@ -0,0 +1,5 @@ |
|||||
|
<ProjectConfiguration> |
||||
|
<Settings> |
||||
|
<PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully> |
||||
|
</Settings> |
||||
|
</ProjectConfiguration> |
||||
@ -0,0 +1,5 @@ |
|||||
|
<ProjectConfiguration> |
||||
|
<Settings> |
||||
|
<PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully> |
||||
|
</Settings> |
||||
|
</ProjectConfiguration> |
||||
@ -0,0 +1,5 @@ |
|||||
|
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
||||
|
<ItemGroup> |
||||
|
<PackageReference Include="reactiveui" Version="8.0.0-alpha0034" /> |
||||
|
</ItemGroup> |
||||
|
</Project> |
||||
@ -1,11 +1,10 @@ |
|||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
||||
<ItemGroup> |
<ItemGroup> |
||||
<PackageReference Include="System.Reactive" Version="3.0.0" /> |
<PackageReference Include="System.Reactive" Version="3.1.0" /> |
||||
<PackageReference Include="System.Reactive.Core" Version="3.0.0" /> |
<PackageReference Include="System.Reactive.Core" Version="3.1.0" /> |
||||
<PackageReference Include="System.Reactive.Interfaces" Version="3.0.0" /> |
<PackageReference Include="System.Reactive.Interfaces" Version="3.1.0" /> |
||||
<PackageReference Include="System.Reactive.Linq" Version="3.0.0" /> |
<PackageReference Include="System.Reactive.Linq" Version="3.1.0" /> |
||||
<PackageReference Include="System.Reactive.PlatformServices" Version="3.0.0" /> |
<PackageReference Include="System.Reactive.PlatformServices" Version="3.1.0" /> |
||||
<PackageReference Condition="'$(TargetFramework)' == 'net45'" Include="System.Reactive.Windows.Threading" Version="3.0.0" /> |
<PackageReference Condition="$(TargetFramework.StartsWith('net4'))" Include="System.Reactive.Windows.Threading" Version="3.1.0" /> |
||||
<PackageReference Condition="'$(TargetFramework)' == 'net461'" Include="System.Reactive.Windows.Threading" Version="3.0.0" /> |
|
||||
</ItemGroup> |
</ItemGroup> |
||||
</Project> |
</Project> |
||||
|
|||||
@ -1,9 +1,9 @@ |
|||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
||||
<ItemGroup> |
<ItemGroup> |
||||
<PackageReference Include="SharpDX" Version="3.1.1" /> |
<PackageReference Include="SharpDX" Version="4.0.1" /> |
||||
<PackageReference Include="SharpDX.Direct2D1" Version="3.1.1" /> |
<PackageReference Include="SharpDX.Direct2D1" Version="4.0.1" /> |
||||
<PackageReference Include="SharpDX.Direct3D11" Version="3.1.1" /> |
<PackageReference Include="SharpDX.Direct3D11" Version="4.0.1" /> |
||||
<PackageReference Include="SharpDX.Direct3D9" Version="3.1.1" Condition="'$(UseDirect3D9)' == 'true'" /> |
<PackageReference Include="SharpDX.DXGI" Version="4.0.1" /> |
||||
<PackageReference Include="SharpDX.DXGI" Version="3.1.1" /> |
<PackageReference Include="SharpDX.Direct3D9" Version="4.0.1" Condition="'$(UseDirect3D9)' == 'true'" /> |
||||
</ItemGroup> |
</ItemGroup> |
||||
</Project> |
</Project> |
||||
|
|||||
@ -0,0 +1,15 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.Metadata |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Defines the ambient class/property
|
||||
|
/// </summary>
|
||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, Inherited = true)] |
||||
|
public class AmbientAttribute : Attribute |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.Metadata |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Defines the property that contains the object's content in markup.
|
||||
|
/// </summary>
|
||||
|
[AttributeUsage(AttributeTargets.Property)] |
||||
|
public class TemplateContentAttribute : Attribute |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
// Copyright (c) The Avalonia Project. All rights reserved.
|
||||
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
||||
|
|
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Threading; |
||||
|
using ReactiveUI; |
||||
|
using System; |
||||
|
using System.Reactive.Concurrency; |
||||
|
using System.Threading; |
||||
|
|
||||
|
namespace Avalonia |
||||
|
{ |
||||
|
public static class AppBuilderExtensions |
||||
|
{ |
||||
|
public static TAppBuilder UseReactiveUI<TAppBuilder>(this TAppBuilder builder) |
||||
|
where TAppBuilder : AppBuilderBase<TAppBuilder>, new() |
||||
|
{ |
||||
|
return builder.AfterSetup(_ => |
||||
|
{ |
||||
|
RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,23 +0,0 @@ |
|||||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|
||||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
||||
|
|
||||
using System; |
|
||||
using System.Reactive.Concurrency; |
|
||||
using System.Threading; |
|
||||
|
|
||||
|
|
||||
namespace ReactiveUI |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Ignore me. This class is a secret handshake between RxUI and RxUI.Xaml
|
|
||||
/// in order to register certain classes on startup that would be difficult
|
|
||||
/// to register otherwise.
|
|
||||
/// </summary>
|
|
||||
public class PlatformRegistrations : IWantsToRegisterStuff |
|
||||
{ |
|
||||
public void Register(Action<Func<object>, Type> registerFunction) |
|
||||
{ |
|
||||
RxApp.MainThreadScheduler = new SynchronizationContextScheduler(SynchronizationContext.Current); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,34 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Text; |
|
||||
|
|
||||
namespace System.Runtime.Serialization |
|
||||
{ |
|
||||
class IgnoreDataMemberAttribute : Attribute |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
class DataMemberAttribute : Attribute |
|
||||
{ |
|
||||
} |
|
||||
class OnDeserializedAttribute : Attribute |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
class DataContractAttribute : Attribute |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
class StreamingContext { } |
|
||||
} |
|
||||
|
|
||||
namespace System.Diagnostics.Contracts |
|
||||
{ |
|
||||
static class Contract |
|
||||
{ |
|
||||
public static void Requires(bool condition) |
|
||||
{ |
|
||||
|
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1 +0,0 @@ |
|||||
Subproject commit 3f725c808b1d4c8457f0d3204e0a071aa462cd75 |
|
||||
@ -0,0 +1,90 @@ |
|||||
|
using System; |
||||
|
using System.Collections; |
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Avalonia.Styling |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Holds resources for a <see cref="Style"/>.
|
||||
|
/// </summary>
|
||||
|
public class StyleResources : IDictionary<string, object>, IDictionary |
||||
|
{ |
||||
|
private Dictionary<string, object> _inner = new Dictionary<string, object>(); |
||||
|
|
||||
|
public object this[string key] |
||||
|
{ |
||||
|
get { return _inner[key]; } |
||||
|
set { _inner[key] = value; } |
||||
|
} |
||||
|
|
||||
|
public int Count => _inner.Count; |
||||
|
|
||||
|
ICollection<string> IDictionary<string, object>.Keys => _inner.Keys; |
||||
|
|
||||
|
ICollection<object> IDictionary<string, object>.Values => _inner.Values; |
||||
|
|
||||
|
bool ICollection<KeyValuePair<string, object>>.IsReadOnly => false; |
||||
|
|
||||
|
object IDictionary.this[object key] |
||||
|
{ |
||||
|
get { return ((IDictionary)_inner)[key]; } |
||||
|
set { ((IDictionary)_inner)[key] = value; } |
||||
|
} |
||||
|
|
||||
|
ICollection IDictionary.Keys => _inner.Keys; |
||||
|
|
||||
|
ICollection IDictionary.Values => _inner.Values; |
||||
|
|
||||
|
bool ICollection.IsSynchronized => false; |
||||
|
|
||||
|
object ICollection.SyncRoot => ((IDictionary)_inner).SyncRoot; |
||||
|
|
||||
|
bool IDictionary.IsFixedSize => false; |
||||
|
|
||||
|
bool IDictionary.IsReadOnly => false; |
||||
|
|
||||
|
public void Add(string key, object value) => _inner.Add(key, value); |
||||
|
|
||||
|
public void Clear() => _inner.Clear(); |
||||
|
|
||||
|
public bool ContainsKey(string key) => _inner.ContainsKey(key); |
||||
|
|
||||
|
public bool Remove(string key) => _inner.Remove(key); |
||||
|
|
||||
|
public IEnumerator<KeyValuePair<string, object>> GetEnumerator() => _inner.GetEnumerator(); |
||||
|
|
||||
|
public bool TryGetValue(string key, out object value) => _inner.TryGetValue(key, out value); |
||||
|
|
||||
|
bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item) |
||||
|
{ |
||||
|
return ((IDictionary<string, object>)_inner).Contains(item); |
||||
|
} |
||||
|
|
||||
|
void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item) |
||||
|
{ |
||||
|
((IDictionary<string, object>)_inner).Add(item); |
||||
|
} |
||||
|
|
||||
|
void ICollection<KeyValuePair<string, object>>.CopyTo(KeyValuePair<string, object>[] array, int arrayIndex) |
||||
|
{ |
||||
|
((IDictionary<string, object>)_inner).CopyTo(array, arrayIndex); |
||||
|
} |
||||
|
|
||||
|
bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item) |
||||
|
{ |
||||
|
return ((IDictionary<string, object>)_inner).Remove(item); |
||||
|
} |
||||
|
|
||||
|
void ICollection.CopyTo(Array array, int index) => ((IDictionary)_inner).CopyTo(array, index); |
||||
|
|
||||
|
IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator(); |
||||
|
|
||||
|
IDictionaryEnumerator IDictionary.GetEnumerator() => ((IDictionary)_inner).GetEnumerator(); |
||||
|
|
||||
|
void IDictionary.Add(object key, object value) => ((IDictionary)_inner).Add(key, value); |
||||
|
|
||||
|
bool IDictionary.Contains(object key) => ((IDictionary)_inner).Contains(key); |
||||
|
|
||||
|
void IDictionary.Remove(object key) => ((IDictionary)_inner).Remove(key); |
||||
|
} |
||||
|
} |
||||
@ -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,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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue