From a2c736de46b9fb93b1d6e0482d21c0b8e0310902 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 27 Sep 2020 14:39:32 +0300 Subject: [PATCH 01/71] Manually setup FBOs for layers --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 12 +- src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs | 14 ++ .../Gpu/OpenGl/FboSkiaSurface.cs | 124 ++++++++++++++++++ .../Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs | 5 + .../Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs | 7 +- .../Avalonia.Skia/PlatformRenderInterface.cs | 2 +- src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs | 43 ++++-- 7 files changed, 194 insertions(+), 13 deletions(-) create mode 100644 src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index a155fd863b..669da8d03c 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -31,6 +31,7 @@ namespace Avalonia.Skia private bool _disposed; private GRContext _grContext; public GRContext GrContext => _grContext; + private ISkiaGpu _gpu; private readonly SKPaint _strokePaint = new SKPaint(); private readonly SKPaint _fillPaint = new SKPaint(); private readonly SKPaint _boxShadowPaint = new SKPaint(); @@ -65,6 +66,11 @@ namespace Avalonia.Skia /// GPU-accelerated context (optional) /// public GRContext GrContext; + + /// + /// Skia GPU provider context (optional) + /// + public ISkiaGpu Gpu; } /// @@ -79,6 +85,7 @@ namespace Avalonia.Skia _disposables = disposables; _canTextUseLcdRendering = !createInfo.DisableTextLcdRendering; _grContext = createInfo.GrContext; + _gpu = createInfo.Gpu; if (_grContext != null) Monitor.Enter(_grContext); Surface = createInfo.Surface; @@ -417,7 +424,7 @@ namespace Avalonia.Skia /// public IRenderTargetBitmapImpl CreateLayer(Size size) { - return CreateRenderTarget(size); + return CreateRenderTarget( size); } /// @@ -925,7 +932,8 @@ namespace Avalonia.Skia Dpi = _dpi, Format = format, DisableTextLcdRendering = !_canTextUseLcdRendering, - GrContext = _grContext + GrContext = _grContext, + Gpu = _gpu }; return new SurfaceRenderTarget(createInfo); diff --git a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs index 1a7a9b75cf..ebb0cb3a1b 100644 --- a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Avalonia.OpenGL.Imaging; using SkiaSharp; @@ -15,6 +16,19 @@ namespace Avalonia.Skia /// Surfaces. /// Created render target or if it fails. ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable surfaces); + + /// + /// Creates an offscreen render target surface + /// + /// size in pixels + ISkiaSurface TryCreateSurface(PixelSize size); + } + + public interface ISkiaSurface : IDisposable + { + SKSurface WriteSurface { get; } + SKSurface ReadSurface { get; } + } public interface IOpenGlAwareSkiaGpu : ISkiaGpu diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs new file mode 100644 index 0000000000..93afd14286 --- /dev/null +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs @@ -0,0 +1,124 @@ +using System; +using Avalonia.OpenGL; +using SkiaSharp; +using static Avalonia.OpenGL.GlConsts; +namespace Avalonia.Skia +{ + public class FboSkiaSurface : ISkiaSurface + { + private readonly GRContext _grContext; + private readonly IGlContext _glContext; + private readonly PixelSize _pixelSize; + private int _fbo; + private int _depthStencil; + private int _texture; + + private static readonly bool[] TrueFalse = new[] { true, false }; + public FboSkiaSurface(GRContext grContext, IGlContext glContext, PixelSize pixelSize) + { + _grContext = grContext; + _glContext = glContext; + _pixelSize = pixelSize; + var InternalFormat = glContext.Version.Type == GlProfileType.OpenGLES ? GL_RGBA : GL_RGBA8; + var gl = glContext.GlInterface; + + // Save old bindings + gl.GetIntegerv(GL_FRAMEBUFFER_BINDING, out var oldFbo); + gl.GetIntegerv(GL_RENDERBUFFER_BINDING, out var oldRenderbuffer); + gl.GetIntegerv(GL_TEXTURE_BINDING_2D, out var oldTexture); + + var arr = new int[2]; + + // Generate FBO + gl.GenFramebuffers(1, arr); + _fbo = arr[0]; + gl.BindFramebuffer(GL_FRAMEBUFFER, _fbo); + + // Create a texture to render into + gl.GenTextures(1, arr); + _texture = arr[0]; + gl.BindTexture(GL_TEXTURE_2D, _texture); + gl.TexImage2D(GL_TEXTURE_2D, 0, + InternalFormat, pixelSize.Width, pixelSize.Height, + 0, GL_RGBA, GL_UNSIGNED_BYTE, IntPtr.Zero); + gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + gl.FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0); + + var success = false; + foreach (var useStencil8 in TrueFalse) + { + gl.GenRenderbuffers(1, arr); + _depthStencil = arr[0]; + gl.BindRenderbuffer(GL_RENDERBUFFER, _depthStencil); + + if (useStencil8) + { + gl.RenderbufferStorage(GL_RENDERBUFFER, GL_STENCIL_INDEX8, pixelSize.Width, pixelSize.Height); + gl.FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, _depthStencil); + } + else + { + gl.RenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, pixelSize.Width, pixelSize.Height); + gl.FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthStencil); + gl.FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, _depthStencil); + } + + var status = gl.CheckFramebufferStatus(GL_FRAMEBUFFER); + if (status == GL_FRAMEBUFFER_COMPLETE) + { + success = true; + break; + } + else + { + gl.BindRenderbuffer(GL_RENDERBUFFER, oldRenderbuffer); + gl.DeleteRenderbuffers(1, arr); + } + } + + gl.BindRenderbuffer(GL_RENDERBUFFER, oldRenderbuffer); + gl.BindTexture(GL_TEXTURE_2D, oldTexture); + gl.BindFramebuffer(GL_FRAMEBUFFER, oldFbo); + + if (!success) + { + arr[0] = _fbo; + gl.DeleteFramebuffers(1, arr); + arr[0] = _texture; + gl.DeleteTextures(1, arr); + throw new OpenGlException("Unable to create FBO with stencil"); + } + + ReadSurface = SKSurface.Create(_grContext, new GRBackendTexture(pixelSize.Width, pixelSize.Height, false, + new GRGlTextureInfo(GL_TEXTURE_2D, (uint)_texture, (uint)InternalFormat)), SKColorType.Rgba8888); + + var target = new GRBackendRenderTarget(pixelSize.Width, pixelSize.Height, 0, 0, + new GRGlFramebufferInfo((uint)_fbo, SKColorType.Rgba8888.ToGlSizedFormat())); + WriteSurface = SKSurface.Create(_grContext, target, + GRSurfaceOrigin.BottomLeft, SKColorType.Rgba8888); + } + + public void Dispose() + { + using (_glContext.EnsureCurrent()) + { + WriteSurface.Dispose(); + WriteSurface = null; + ReadSurface.Dispose(); + ReadSurface = null; + var gl = _glContext.GlInterface; + if (_fbo != 0) + { + gl.DeleteFramebuffers(1, new[] { _fbo }); + gl.DeleteTextures(1, new[] { _texture }); + gl.DeleteRenderbuffers(1, new[] { _depthStencil }); + _fbo = _texture = _depthStencil = 0; + } + } + } + + public SKSurface WriteSurface { get; private set; } + public SKSurface ReadSurface { get; private set; } + } +} diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs index 46d42dfdab..f606929b76 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs @@ -43,6 +43,11 @@ namespace Avalonia.Skia return null; } + public ISkiaSurface TryCreateSurface(PixelSize size) + { + return new FboSkiaSurface(_grContext, _glContext, size); + } + public IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi) => new GlOpenGlBitmapImpl(_glContext, size, dpi); } } diff --git a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs index ef5da5eb08..9992c9ba8c 100644 --- a/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs @@ -8,10 +8,12 @@ namespace Avalonia.Skia /// internal class SkiaGpuRenderTarget : IRenderTargetWithCorruptionInfo { + private readonly ISkiaGpu _skiaGpu; private readonly ISkiaGpuRenderTarget _renderTarget; - public SkiaGpuRenderTarget(ISkiaGpuRenderTarget renderTarget) + public SkiaGpuRenderTarget(ISkiaGpu skiaGpu, ISkiaGpuRenderTarget renderTarget) { + _skiaGpu = skiaGpu; _renderTarget = renderTarget; } @@ -30,7 +32,8 @@ namespace Avalonia.Skia Surface = session.SkSurface, Dpi = SkiaPlatform.DefaultDpi * session.ScaleFactor, VisualBrushRenderer = visualBrushRenderer, - DisableTextLcdRendering = true + DisableTextLcdRendering = true, + Gpu = _skiaGpu }; return new DrawingContextImpl(nfo, session); diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index b9c1cbc673..d6f76a2c20 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -138,7 +138,7 @@ namespace Avalonia.Skia var gpuRenderTarget = _skiaGpu?.TryCreateRenderTarget(surfaces); if (gpuRenderTarget != null) { - return new SkiaGpuRenderTarget(gpuRenderTarget); + return new SkiaGpuRenderTarget(_skiaGpu, gpuRenderTarget); } foreach (var surface in surfaces) diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index 428087ac56..d6ea229089 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -13,10 +13,29 @@ namespace Avalonia.Skia /// internal class SurfaceRenderTarget : IRenderTargetBitmapImpl, IDrawableBitmapImpl { - private readonly SKSurface _surface; + private readonly ISkiaSurface _surface; private readonly SKCanvas _canvas; private readonly bool _disableLcdRendering; private readonly GRContext _grContext; + private readonly ISkiaGpu _gpu; + + class SkiaSurfaceWrapper : ISkiaSurface + { + public SKSurface WriteSurface { get; private set; } + public SKSurface ReadSurface { get; private set; } + + public SkiaSurfaceWrapper(SKSurface surface) + { + WriteSurface = ReadSurface = surface; + } + + public void Dispose() + { + WriteSurface?.Dispose(); + WriteSurface = null; + ReadSurface = null; + } + } /// /// Create new surface render target. @@ -29,9 +48,14 @@ namespace Avalonia.Skia _disableLcdRendering = createInfo.DisableTextLcdRendering; _grContext = createInfo.GrContext; - _surface = CreateSurface(createInfo.GrContext, PixelSize.Width, PixelSize.Height, createInfo.Format); + _gpu = createInfo.Gpu; - _canvas = _surface?.Canvas; + _surface = _gpu?.TryCreateSurface(PixelSize); + if (_surface == null) + _surface = new SkiaSurfaceWrapper(CreateSurface(createInfo.GrContext, PixelSize.Width, PixelSize.Height, + createInfo.Format)); + + _canvas = _surface?.WriteSurface.Canvas; if (_surface == null || _canvas == null) { @@ -70,11 +94,12 @@ namespace Avalonia.Skia var createInfo = new DrawingContextImpl.CreateInfo { - Surface = _surface, + Surface = _surface.WriteSurface, Dpi = Dpi, VisualBrushRenderer = visualBrushRenderer, DisableTextLcdRendering = _disableLcdRendering, - GrContext = _grContext + GrContext = _grContext, + Gpu = _gpu }; return new DrawingContextImpl(createInfo, Disposable.Create(() => Version++)); @@ -111,8 +136,8 @@ namespace Avalonia.Skia { if (sourceRect.Left == 0 && sourceRect.Top == 0 && sourceRect.Size == destRect.Size) { - _surface.Canvas.Flush(); - _surface.Draw(context.Canvas, destRect.Left, destRect.Top, paint); + _surface.ReadSurface.Canvas.Flush(); + _surface.ReadSurface.Draw(context.Canvas, destRect.Left, destRect.Top, paint); } else using (var image = SnapshotImage()) @@ -127,7 +152,7 @@ namespace Avalonia.Skia /// Image snapshot. public SKImage SnapshotImage() { - return _surface.Snapshot(); + return _surface.ReadSurface.Snapshot(); } /// @@ -178,6 +203,8 @@ namespace Avalonia.Skia /// GPU-accelerated context (optional) /// public GRContext GrContext; + + public ISkiaGpu Gpu; } } } From 445290ecec998d5a0d345429c9c8a781322c5a04 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 27 Sep 2020 15:01:24 +0300 Subject: [PATCH 02/71] Do the manual blit if possible --- src/Avalonia.OpenGL/GlInterface.cs | 13 +++++++++++++ src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs | 4 +++- .../Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs | 15 +++++++++++++-- src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs | 10 ++++++++-- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.OpenGL/GlInterface.cs b/src/Avalonia.OpenGL/GlInterface.cs index ea2fe0a99c..28b62136da 100644 --- a/src/Avalonia.OpenGL/GlInterface.cs +++ b/src/Avalonia.OpenGL/GlInterface.cs @@ -117,6 +117,19 @@ namespace Avalonia.OpenGL public delegate int GlCheckFramebufferStatus(int target); [GlEntryPoint("glCheckFramebufferStatus")] public GlCheckFramebufferStatus CheckFramebufferStatus { get; } + + public delegate void GlBlitFramebuffer(int srcX0, + int srcY0, + int srcX1, + int srcY1, + int dstX0, + int dstY0, + int dstX1, + int dstY1, + int mask, + int filter); + [GlMinVersionEntryPoint("glBlitFramebuffer", 3, 0)] + public GlBlitFramebuffer BlitFramebuffer { get; } public delegate void GlGenRenderbuffers(int count, int[] res); [GlEntryPoint("glGenRenderbuffers")] diff --git a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs index ebb0cb3a1b..f394fe7ac1 100644 --- a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs @@ -28,7 +28,9 @@ namespace Avalonia.Skia { SKSurface WriteSurface { get; } SKSurface ReadSurface { get; } - + bool CanBlit { get; } + void Blit(); + } public interface IOpenGlAwareSkiaGpu : ISkiaGpu diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs index 93afd14286..45572ca40b 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs @@ -97,15 +97,16 @@ namespace Avalonia.Skia new GRGlFramebufferInfo((uint)_fbo, SKColorType.Rgba8888.ToGlSizedFormat())); WriteSurface = SKSurface.Create(_grContext, target, GRSurfaceOrigin.BottomLeft, SKColorType.Rgba8888); + CanBlit = gl.BlitFramebuffer != null; } public void Dispose() { using (_glContext.EnsureCurrent()) { - WriteSurface.Dispose(); + WriteSurface?.Dispose(); WriteSurface = null; - ReadSurface.Dispose(); + ReadSurface?.Dispose(); ReadSurface = null; var gl = _glContext.GlInterface; if (_fbo != 0) @@ -120,5 +121,15 @@ namespace Avalonia.Skia public SKSurface WriteSurface { get; private set; } public SKSurface ReadSurface { get; private set; } + public bool CanBlit { get; } + public void Blit() + { + var gl = _glContext.GlInterface; + gl.GetIntegerv(GL_READ_FRAMEBUFFER_BINDING, out var oldRead); + gl.BindFramebuffer(GL_READ_FRAMEBUFFER, _fbo); + gl.BlitFramebuffer(0, 0, _pixelSize.Width, _pixelSize.Height, 0, 0, _pixelSize.Width, _pixelSize.Height, + GL_COLOR_BUFFER_BIT, GL_LINEAR); + gl.BindFramebuffer(GL_READ_FRAMEBUFFER, oldRead); + } } } diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index d6ea229089..9cf0642e99 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -23,6 +23,8 @@ namespace Avalonia.Skia { public SKSurface WriteSurface { get; private set; } public SKSurface ReadSurface { get; private set; } + public bool CanBlit => false; + public void Blit() => throw new NotSupportedException(); public SkiaSurfaceWrapper(SKSurface surface) { @@ -136,8 +138,12 @@ namespace Avalonia.Skia { if (sourceRect.Left == 0 && sourceRect.Top == 0 && sourceRect.Size == destRect.Size) { - _surface.ReadSurface.Canvas.Flush(); - _surface.ReadSurface.Draw(context.Canvas, destRect.Left, destRect.Top, paint); + _surface.WriteSurface.Canvas.Flush(); + if (context.Canvas.TotalMatrix.IsIdentity && _surface.CanBlit && destRect.Top == 0 && + destRect.Left == 0) + _surface.Blit(); + else + _surface.ReadSurface.Draw(context.Canvas, destRect.Left, destRect.Top, paint); } else using (var image = SnapshotImage()) From 75395bc8f548354a4c50dd2b809aaf826e6390a5 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 26 Sep 2020 09:25:07 +0200 Subject: [PATCH 03/71] Don't create extra read surface --- src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs | 3 +-- .../Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs | 14 ++++---------- src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs | 18 +++++++++--------- 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs index f394fe7ac1..c06b4af52f 100644 --- a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs @@ -26,8 +26,7 @@ namespace Avalonia.Skia public interface ISkiaSurface : IDisposable { - SKSurface WriteSurface { get; } - SKSurface ReadSurface { get; } + SKSurface Surface { get; } bool CanBlit { get; } void Blit(); diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs index 45572ca40b..40347e2cd1 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs @@ -90,12 +90,9 @@ namespace Avalonia.Skia throw new OpenGlException("Unable to create FBO with stencil"); } - ReadSurface = SKSurface.Create(_grContext, new GRBackendTexture(pixelSize.Width, pixelSize.Height, false, - new GRGlTextureInfo(GL_TEXTURE_2D, (uint)_texture, (uint)InternalFormat)), SKColorType.Rgba8888); - var target = new GRBackendRenderTarget(pixelSize.Width, pixelSize.Height, 0, 0, new GRGlFramebufferInfo((uint)_fbo, SKColorType.Rgba8888.ToGlSizedFormat())); - WriteSurface = SKSurface.Create(_grContext, target, + Surface = SKSurface.Create(_grContext, target, GRSurfaceOrigin.BottomLeft, SKColorType.Rgba8888); CanBlit = gl.BlitFramebuffer != null; } @@ -104,10 +101,8 @@ namespace Avalonia.Skia { using (_glContext.EnsureCurrent()) { - WriteSurface?.Dispose(); - WriteSurface = null; - ReadSurface?.Dispose(); - ReadSurface = null; + Surface?.Dispose(); + Surface = null; var gl = _glContext.GlInterface; if (_fbo != 0) { @@ -119,8 +114,7 @@ namespace Avalonia.Skia } } - public SKSurface WriteSurface { get; private set; } - public SKSurface ReadSurface { get; private set; } + public SKSurface Surface { get; private set; } public bool CanBlit { get; } public void Blit() { diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index 9cf0642e99..d0bd8ff150 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -21,20 +21,20 @@ namespace Avalonia.Skia class SkiaSurfaceWrapper : ISkiaSurface { - public SKSurface WriteSurface { get; private set; } + public SKSurface Surface { get; private set; } public SKSurface ReadSurface { get; private set; } public bool CanBlit => false; public void Blit() => throw new NotSupportedException(); public SkiaSurfaceWrapper(SKSurface surface) { - WriteSurface = ReadSurface = surface; + Surface = ReadSurface = surface; } public void Dispose() { - WriteSurface?.Dispose(); - WriteSurface = null; + Surface?.Dispose(); + Surface = null; ReadSurface = null; } } @@ -57,7 +57,7 @@ namespace Avalonia.Skia _surface = new SkiaSurfaceWrapper(CreateSurface(createInfo.GrContext, PixelSize.Width, PixelSize.Height, createInfo.Format)); - _canvas = _surface?.WriteSurface.Canvas; + _canvas = _surface?.Surface.Canvas; if (_surface == null || _canvas == null) { @@ -96,7 +96,7 @@ namespace Avalonia.Skia var createInfo = new DrawingContextImpl.CreateInfo { - Surface = _surface.WriteSurface, + Surface = _surface.Surface, Dpi = Dpi, VisualBrushRenderer = visualBrushRenderer, DisableTextLcdRendering = _disableLcdRendering, @@ -138,12 +138,12 @@ namespace Avalonia.Skia { if (sourceRect.Left == 0 && sourceRect.Top == 0 && sourceRect.Size == destRect.Size) { - _surface.WriteSurface.Canvas.Flush(); + _surface.Surface.Canvas.Flush(); if (context.Canvas.TotalMatrix.IsIdentity && _surface.CanBlit && destRect.Top == 0 && destRect.Left == 0) _surface.Blit(); else - _surface.ReadSurface.Draw(context.Canvas, destRect.Left, destRect.Top, paint); + _surface.Surface.Draw(context.Canvas, destRect.Left, destRect.Top, paint); } else using (var image = SnapshotImage()) @@ -158,7 +158,7 @@ namespace Avalonia.Skia /// Image snapshot. public SKImage SnapshotImage() { - return _surface.ReadSurface.Snapshot(); + return _surface.Surface.Snapshot(); } /// From 8555fed86b7070218a07f329a90da4501b2adb42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Komosi=C5=84ski?= Date: Sat, 26 Sep 2020 16:13:03 +0200 Subject: [PATCH 04/71] Add failing tests for #4733 and fix property accessor. --- .../Plugins/InpcPropertyAccessorPlugin.cs | 44 ++++++++++++++++--- .../Core/ExpressionObserverTests_Property.cs | 29 ++++++++++++ 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index 8fc2a7b77c..3bf6842cd6 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Reflection; using Avalonia.Utilities; @@ -11,8 +12,11 @@ namespace Avalonia.Data.Core.Plugins /// public class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin { + private readonly Dictionary<(Type, string), PropertyInfo> _propertyLookup = + new Dictionary<(Type, string), PropertyInfo>(); + /// - public bool Match(object obj, string propertyName) => GetPropertyWithName(obj.GetType(), propertyName) != null; + public bool Match(object obj, string propertyName) => GetFirstPropertyWithName(obj.GetType(), propertyName) != null; /// /// Starts monitoring the value of a property on an object. @@ -30,7 +34,7 @@ namespace Avalonia.Data.Core.Plugins reference.TryGetTarget(out object instance); - var p = GetPropertyWithName(instance.GetType(), propertyName); + var p = GetFirstPropertyWithName(instance.GetType(), propertyName); if (p != null) { @@ -44,12 +48,40 @@ namespace Avalonia.Data.Core.Plugins } } - private static PropertyInfo GetPropertyWithName(Type type, string propertyName) + private PropertyInfo GetFirstPropertyWithName(Type type, string propertyName) { - const BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | - BindingFlags.Static | BindingFlags.Instance; + var key = (type, propertyName); + + if (!_propertyLookup.TryGetValue(key, out PropertyInfo propertyInfo)) + { + propertyInfo = TryFindAndCacheProperty(type, propertyName); + } + + return propertyInfo; + } + + private PropertyInfo TryFindAndCacheProperty(Type type, string propertyName) + { + PropertyInfo found = null; + + const BindingFlags bindingFlags = + BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance; + + var properties = type.GetProperties(bindingFlags); + + foreach (PropertyInfo propertyInfo in properties) + { + if (propertyInfo.Name == propertyName) + { + found = propertyInfo; + + break; + } + } + + _propertyLookup.Add((type, propertyName), found); - return type.GetProperty(propertyName, bindingFlags); + return found; } private class Accessor : PropertyAccessorBase, IWeakSubscriber diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs index 784046ac0b..cf98293d6d 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs @@ -600,6 +600,24 @@ namespace Avalonia.Base.UnitTests.Data.Core result); } + [Fact] + public void Should_Not_Throw_Exception_On_Duplicate_Properties() + { + // Repro of https://github.com/AvaloniaUI/Avalonia/issues/4733. + var source = new MyViewModel(); + var target = new PropertyAccessorNode("Name", false); + + target.Target = new WeakReference(source); + + var result = new List(); + + target.Subscribe(x => result.Add(x)); + } + + public class MyViewModelBase { public object Name => "Name"; } + + public class MyViewModel : MyViewModelBase { public new string Name => "NewName"; } + private interface INext { int PropertyChangedSubscriptionCount { get; } @@ -664,6 +682,17 @@ namespace Avalonia.Base.UnitTests.Data.Core } } + private class WithNewBar : Class1 + { + private string _bar; + + public new string Bar + { + get { return _bar; } + set { _bar = value; } + } + } + private class Class3 : Class1 { } From 00dbf12cee54609d0b94d0449d2affdeab93ba18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Komosi=C5=84ski?= Date: Sat, 26 Sep 2020 16:26:39 +0200 Subject: [PATCH 05/71] Remove not needed code. --- .../Data/Core/ExpressionObserverTests_Property.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs index cf98293d6d..32cdd21e04 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs @@ -682,17 +682,6 @@ namespace Avalonia.Base.UnitTests.Data.Core } } - private class WithNewBar : Class1 - { - private string _bar; - - public new string Bar - { - get { return _bar; } - set { _bar = value; } - } - } - private class Class3 : Class1 { } From 38a6ea99acd0f9bdfdce92944b67b907ec53e254 Mon Sep 17 00:00:00 2001 From: "oliver.holliday@lontra.co.uk" Date: Sat, 26 Sep 2020 23:33:38 +0100 Subject: [PATCH 06/71] Add new tests for RadialGradient with GradientOffset handling. Fix implementation of Skia renderer to match Direct2d version. --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 10 +- .../Media/RadialGradientBrushTests.cs | 108 +++++++++++++++++- 2 files changed, 112 insertions(+), 6 deletions(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index a155fd863b..b794c5e00a 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -583,13 +583,13 @@ namespace Avalonia.Skia var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint(); var radius = (float)(radialGradient.Radius * targetSize.Width); - // TODO: There is no SetAlpha in SkiaSharp - //paint.setAlpha(128); + var origin = radialGradient.GradientOrigin.ToPixels(targetSize).ToSKPoint(); // would be nice to cache these shaders possibly? - using (var shader = - SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode)) - { + using (var shader = SKShader.CreateCompose( + SKShader.CreateColor(stopColors.Last()), + SKShader.CreateTwoPointConicalGradient(center, radius, origin, 0, stopColors.Reverse().ToArray(), stopOffsets, tileMode)) + ) { paintWrapper.Paint.Shader = shader; } diff --git a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs index 9beb860bee..63d4d9992d 100644 --- a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs @@ -18,7 +18,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media public RadialGradientBrushTests() : base(@"Media\RadialGradientBrush") { } - + [Fact] public async Task RadialGradientBrush_RedBlue() { @@ -43,5 +43,111 @@ namespace Avalonia.Direct2D1.RenderTests.Media await RenderToFile(target); CompareImages(); } + + [Fact] + public async Task RadialGradientBrush_RedBlue_Offset_Inside() + { + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 200, + Height = 200, + Child = new Border + { + Background = new RadialGradientBrush + { + GradientStops = + { + new GradientStop { Color = Colors.Red, Offset = 0 }, + new GradientStop { Color = Colors.Blue, Offset = 1 } + }, + GradientOrigin = new RelativePoint(0.25, 0.25, RelativeUnit.Relative) + } + } + }; + + await RenderToFile(target); + CompareImages(); + } + + [Fact] + public async Task RadialGradientBrush_RedBlue_Offset_Outside() + { + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 200, + Height = 200, + Child = new Border + { + Background = new RadialGradientBrush + { + GradientStops = + { + new GradientStop { Color = Colors.Red, Offset = 0 }, + new GradientStop { Color = Colors.Blue, Offset = 1 } + }, + GradientOrigin = new RelativePoint(0.1, 0.1, RelativeUnit.Relative) + } + } + }; + + await RenderToFile(target); + CompareImages(); + } + + [Fact] + public async Task RadialGradientBrush_RedGreenBlue_Offset_Inside() + { + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 200, + Height = 200, + Child = new Border + { + Background = new RadialGradientBrush + { + GradientStops = + { + new GradientStop { Color = Colors.Red, Offset = 0 }, + new GradientStop { Color = Colors.Green, Offset = 0.5 }, + new GradientStop { Color = Colors.Blue, Offset = 1 } + }, + GradientOrigin = new RelativePoint(0.25, 0.25, RelativeUnit.Relative) + } + } + }; + + await RenderToFile(target); + CompareImages(); + } + + [Fact] + public async Task RadialGradientBrush_RedGreenBlue_Offset_Outside() + { + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 200, + Height = 200, + Child = new Border + { + Background = new RadialGradientBrush + { + GradientStops = + { + new GradientStop { Color = Colors.Red, Offset = 0 }, + new GradientStop { Color = Colors.Green, Offset = 0.5 }, + new GradientStop { Color = Colors.Blue, Offset = 1 } + }, + GradientOrigin = new RelativePoint(0.1, 0.1, RelativeUnit.Relative) + } + } + }; + + await RenderToFile(target); + CompareImages(); + } } } From 0d9905c0515c666ba7ede4c2583df3d229931d6e Mon Sep 17 00:00:00 2001 From: "oliver.holliday@lontra.co.uk" Date: Sat, 26 Sep 2020 23:50:22 +0100 Subject: [PATCH 07/71] Add expected test files --- ...ientBrush_RedBlue_Offset_Inside.expected.png | Bin 0 -> 9768 bytes ...entBrush_RedBlue_Offset_Outside.expected.png | Bin 0 -> 9840 bytes ...rush_RedGreenBlue_Offset_Inside.expected.png | Bin 0 -> 15084 bytes ...ush_RedGreenBlue_Offset_Outside.expected.png | Bin 0 -> 15227 bytes ...ientBrush_RedBlue_Offset_Inside.expected.png | Bin 0 -> 9768 bytes ...entBrush_RedBlue_Offset_Outside.expected.png | Bin 0 -> 9840 bytes ...rush_RedGreenBlue_Offset_Inside.expected.png | Bin 0 -> 15084 bytes ...ush_RedGreenBlue_Offset_Outside.expected.png | Bin 0 -> 15227 bytes 8 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_RedBlue_Offset_Inside.expected.png create mode 100644 tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_RedBlue_Offset_Outside.expected.png create mode 100644 tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_RedGreenBlue_Offset_Inside.expected.png create mode 100644 tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_RedGreenBlue_Offset_Outside.expected.png create mode 100644 tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedBlue_Offset_Inside.expected.png create mode 100644 tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedBlue_Offset_Outside.expected.png create mode 100644 tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedGreenBlue_Offset_Inside.expected.png create mode 100644 tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedGreenBlue_Offset_Outside.expected.png diff --git a/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_RedBlue_Offset_Inside.expected.png b/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_RedBlue_Offset_Inside.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..4d690abee5fe85b6f61b36f0d754bf590ab6a444 GIT binary patch literal 9768 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 literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_RedBlue_Offset_Outside.expected.png b/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_RedBlue_Offset_Outside.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..667aa9151e8d97b711ce1dad1f05c7f5f6f83b89 GIT binary patch literal 9840 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 literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_RedGreenBlue_Offset_Inside.expected.png b/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_RedGreenBlue_Offset_Inside.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..1d660f79d1aa8d0f6bf42fd80bc8546e03a43327 GIT binary patch literal 15084 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�#q)yoM}zR(et5z0jK7Mc$&~6V zXPk+(%IgvVLDf99Xu! zqqk?Zx8OH@4^-!4+)pma>o@CiUwKX5k z)bMpsLn^sZD8hwt8Ah>(aS>P+J{^QCG;s=f;MN(;h7rqCHK}0|O3HFP6BdN4Ccu`_ zc^i7IRdpL4-vgmF;EPuQ@3jY-dwxuMDf*UvwrsHMgo2n66|RYBNqIC5bk%`*Kq_rV zC0cEinXSeGr}+S})smF0g(+PoS2z54;DFpQ$y}z_waZiSea|9#cQ*rmI0+Cuku|d2 z{5Dyc)PG;$O2J(VVOLROvh;mfL$PyXU^Y7fg-V8+vFy*4MS#G=jkHPuzLf+GgW7kz zaBSwVW`WX&sOmnt-PMfS%HKTChhu^^l@xkRY5f<=d~TrwZo3!4?HLESiwwI!T>K}6 zI?y9b$$S?yryZrf&p7pvLlHFMlz$LqfuMh}qGm9??YKIthq>p-VX~U9z3Yi$MaBgD z_{o{xb7J^yQ1UFcrHQnMM6*eExTJF%z3HK~EihEjYdFP!^9iAfzET5%TSPQjA& z>VHj={O^2+V&(V_in_^DQSgtGTl3m2xTvs-15maweI(58<%D&c41GAcp{~aqZLgfF zjtX(7t8Ni@#4Ylxybh@U4zZ!Sc4%3#I!W7D@|i>Cq%)Od_s6SsO|$Kw0L!-3pBP>zM4I}3JNpJD7BCP0a0O&1u(x5NxLisTqfKpz?k6X3VWsjY$uYf)gi5~{WB;kTeJj@@pT}tMPNo%1%kB~wXXA8< zzHM2WdLdO zHIuB|Eacg03Tp8Oi;Y~fCgb<()|fjT6j&CAB!A}7OC5Ff()9Ln%hAuMUznPYsUpa| zL76MSX2MlqEve7i84Ut!zI)>bCb#Z!|KX2M^OaTJ)Dzg6YUnTpcg+iQv+ICyi+CaN zbBq{znEibxO&`D4@DoKQ@;x*S@W^{}~fX z9C0m>BW(;kA>UhQ>0j#i&-l1icxvWWe^UEgMuXQ(g;>~%`gnB>1j7gA1etT-x>oS; zO}IZuWhvUr6yp+E@&l|vBQWmAR6QW>qWTZ#Sy~uXKXHmx??1zWPUZ}uZ~efsMc38( zAD7wvfkig1`jZj(SYL4Kr*CB4?Ylu7xuRhX>|hFC4t~ELPHT!77n<_TM%Ndhfh$uS z^{H13?JS)H1#^_>)q0&IP%$-W`1A0Sv)p_p1OJCWWTIW=ho99+d<1PJ?0vjsjHxSd zVSw2Nm%V~kvkHmOfw~RoqzMmDXk}@^7X4q3SK5Mx7vMPizBw9k=l>#4fi2HunU^2b zU%QPQ%`dTa?k$wXdY@rlw!B}jwo{s^L(#c9fLb+2K|h2b-|2UyxN5X_wdXj)ThMB% z0}9hG`5PJXwy^E=KCEMY)Z#YKyz!`0A$9g|kUYw!EK(-uZCoas`pp*h8{p$l#hy}L zefhR?u|&nDy3IPeog#pwYO1t07j2$4?<);h6i)CX{4R&`9>yX35?q8O$5*@Q7D85S zEK?F3s!>rQWAJ5ykM|$)Y9*9nK|RB0gL_#hgZuh_J9|Yl6$FWlMkNJfHx^AKG|hY7 z`Htb8fQQ7HZc}a=bsZ^P-P_9a-tydgtG{JaDhS;D5uJJRk0m~teW&B%I^t0qR%zf9x$ zGd{UhqWfFWiwQc73_L1}L^rZM9%Dw7Y9ozA`*5ID5yHcry<(}fHt{ETNDeoJFG2

4gnSxr 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 Date: Sun, 27 Sep 2020 09:36:41 +0100 Subject: [PATCH 08/71] Fix stop-offsets being relative to the wrong point after reversal. Copy arrays manually instead of using Linq to resolve PR comment. --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 43 +++++++++++++++--- .../Media/RadialGradientBrushTests.cs | 22 +++++++-- ...h_RedGreenBlue_Offset_Outside.expected.png | Bin 15227 -> 12528 bytes ...h_RedGreenBlue_Offset_Outside.expected.png | Bin 15227 -> 12528 bytes 4 files changed, 56 insertions(+), 9 deletions(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index b794c5e00a..65989610b8 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -585,12 +585,43 @@ namespace Avalonia.Skia var origin = radialGradient.GradientOrigin.ToPixels(targetSize).ToSKPoint(); - // would be nice to cache these shaders possibly? - using (var shader = SKShader.CreateCompose( - SKShader.CreateColor(stopColors.Last()), - SKShader.CreateTwoPointConicalGradient(center, radius, origin, 0, stopColors.Reverse().ToArray(), stopOffsets, tileMode)) - ) { - paintWrapper.Paint.Shader = shader; + if (origin.Equals(center)) + { + // when the origin is the same as the center the Skia RadialGradient acts the same as D2D + using (var shader = + SKShader.CreateRadialGradient(center, radius, stopColors, stopOffsets, tileMode)) + { + paintWrapper.Paint.Shader = shader; + } + } + else + { + // when the origin is different to the center use a two point ConicalGradient to match the behaviour of D2D + + // reverse the order of the stops to match D2D + var reversedColors = new SKColor[stopColors.Length]; + Array.Copy(stopColors, reversedColors, stopColors.Length); + Array.Reverse(reversedColors); + + // and then reverse the reference point of the stops + var reversedStops = new float[stopOffsets.Length]; + for (var i = 0; i < stopOffsets.Length; i++) + { + reversedStops[i] = stopOffsets[i]; + if (reversedStops[i] > 0 && reversedStops[i] < 1) + { + reversedStops[i] = Math.Abs(1 - stopOffsets[i]); + } + } + + // compose with a background colour of the final stop to match D2D's behaviour of filling with the final color + using (var shader = SKShader.CreateCompose( + SKShader.CreateColor(reversedColors[0]), + SKShader.CreateTwoPointConicalGradient(center, radius, origin, 0, reversedColors, reversedStops, tileMode) + )) + { + paintWrapper.Paint.Shader = shader; + } } break; diff --git a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs index 63d4d9992d..2941b8dc34 100644 --- a/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs +++ b/tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs @@ -44,6 +44,9 @@ namespace Avalonia.Direct2D1.RenderTests.Media CompareImages(); } + ///

+ /// Tests using a GradientOrigin that falls inside of the circle described by Center/Radius. + /// [Fact] public async Task RadialGradientBrush_RedBlue_Offset_Inside() { @@ -70,6 +73,9 @@ namespace Avalonia.Direct2D1.RenderTests.Media CompareImages(); } + /// + /// Tests using a GradientOrigin that falls outside of the circle described by Center/Radius. + /// [Fact] public async Task RadialGradientBrush_RedBlue_Offset_Outside() { @@ -96,6 +102,9 @@ namespace Avalonia.Direct2D1.RenderTests.Media CompareImages(); } + /// + /// Tests using a GradientOrigin that falls inside of the circle described by Center/Radius. + /// [Fact] public async Task RadialGradientBrush_RedGreenBlue_Offset_Inside() { @@ -114,7 +123,9 @@ namespace Avalonia.Direct2D1.RenderTests.Media new GradientStop { Color = Colors.Green, Offset = 0.5 }, new GradientStop { Color = Colors.Blue, Offset = 1 } }, - GradientOrigin = new RelativePoint(0.25, 0.25, RelativeUnit.Relative) + GradientOrigin = new RelativePoint(0.25, 0.25, RelativeUnit.Relative), + Center = new RelativePoint(0.5, 0.5, RelativeUnit.Relative), + Radius = 0.5 } } }; @@ -123,6 +134,9 @@ namespace Avalonia.Direct2D1.RenderTests.Media CompareImages(); } + /// + /// Tests using a GradientOrigin that falls outside of the circle described by Center/Radius. + /// [Fact] public async Task RadialGradientBrush_RedGreenBlue_Offset_Outside() { @@ -138,10 +152,12 @@ namespace Avalonia.Direct2D1.RenderTests.Media GradientStops = { new GradientStop { Color = Colors.Red, Offset = 0 }, - new GradientStop { Color = Colors.Green, Offset = 0.5 }, + new GradientStop { Color = Colors.Green, Offset = 0.25 }, new GradientStop { Color = Colors.Blue, Offset = 1 } }, - GradientOrigin = new RelativePoint(0.1, 0.1, RelativeUnit.Relative) + GradientOrigin = new RelativePoint(0.1, 0.1, RelativeUnit.Relative), + Center = new RelativePoint(0.5, 0.5, RelativeUnit.Relative), + Radius = 0.5 } } }; diff --git a/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_RedGreenBlue_Offset_Outside.expected.png b/tests/TestFiles/Direct2D1/Media/RadialGradientBrush/RadialGradientBrush_RedGreenBlue_Offset_Outside.expected.png index 91068ca1cfeed8ec85d62d35d8747eea08673478..ea1009b54a6f41f71c850149bf6b1c0d37726eda 100644 GIT binary patch literal 12528 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 Date: Tue, 29 Sep 2020 14:42:38 +0300 Subject: [PATCH 09/71] allow to create Skia drawing context from existing canvas --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 98528a128a..fd6ccd2eca 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -41,6 +41,11 @@ namespace Avalonia.Skia /// public struct CreateInfo { + /// + /// Canvas to draw to. + /// + public SKCanvas Canvas; + /// /// Surface to draw to. /// @@ -82,7 +87,7 @@ namespace Avalonia.Skia if (_grContext != null) Monitor.Enter(_grContext); Surface = createInfo.Surface; - Canvas = createInfo.Surface.Canvas; + Canvas = createInfo.Canvas ?? createInfo.Surface?.Canvas; if (Canvas == null) { From f97d43e76507f35871d4d1ed0b6064b7a5ef6f39 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 30 Sep 2020 23:51:00 +0100 Subject: [PATCH 10/71] add failing unit test. --- .../TreeViewTests.cs | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 7022fbf4c1..f1a346e6aa 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Presenters; @@ -13,6 +14,7 @@ using Avalonia.Interactivity; using Avalonia.LogicalTree; using Avalonia.Styling; using Avalonia.UnitTests; +using ReactiveUI; using Xunit; namespace Avalonia.Controls.UnitTests @@ -454,6 +456,43 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Bound_SelectedItem_Should_Not_Be_Cleared_when_Changing_Selection() + { + using (Application()) + { + var dataContext = new TestDataContext(); + + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + DataContext = dataContext + }; + + target.Bind(TreeView.ItemsProperty, new Binding("Items")); + target.Bind(TreeView.SelectedItemProperty, new Binding("SelectedItem")); + + var visualRoot = new TestRoot(); + visualRoot.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + + var children = target.GetLogicalChildren().ToList(); + + var selectedValues = new List(); + + dataContext.WhenAnyValue(x => x.SelectedItem) + .Subscribe(x => selectedValues.Add(x)); + + _mouse.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Left); + _mouse.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Left); + + Assert.Equal(4, selectedValues.Count); + Assert.Equal(new[] { null, "Item 0", "Item 2" }, selectedValues.ToArray()); + } + } + [Fact] public void LogicalChildren_Should_Be_Set() { @@ -1288,5 +1327,29 @@ namespace Avalonia.Controls.UnitTests private class DerivedTreeView : TreeView { } + + private class TestDataContext : ReactiveObject + { + private int _counter; + private string _selectedItem; + + public TestDataContext() + { + Items = new ObservableCollection(Enumerable.Range(1, 5).Select(i => GenerateItem())); + } + + private string GenerateItem() => $"Item {_counter++.ToString()}"; + + public ObservableCollection Items { get; } + + public string SelectedItem + { + get { return _selectedItem; } + set + { + this.RaiseAndSetIfChanged(ref _selectedItem, value); + } + } + } } } From 0bb91da4959e46a0a35988dc0583185143f1b1f6 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 1 Oct 2020 10:45:02 +0100 Subject: [PATCH 11/71] fix test. --- tests/Avalonia.Controls.UnitTests/TreeViewTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index f1a346e6aa..9e22c44f44 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -488,7 +488,7 @@ namespace Avalonia.Controls.UnitTests _mouse.Click((Interactive)target.Presenter.Panel.Children[0], MouseButton.Left); _mouse.Click((Interactive)target.Presenter.Panel.Children[2], MouseButton.Left); - Assert.Equal(4, selectedValues.Count); + Assert.Equal(3, selectedValues.Count); Assert.Equal(new[] { null, "Item 0", "Item 2" }, selectedValues.ToArray()); } } From ec7ae1fefb8cbe3fae2d228981cec7132dddc0e0 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 1 Oct 2020 10:45:33 +0100 Subject: [PATCH 12/71] [TreeView] dont raise change events on the clear stage of setting a single selection. --- src/Avalonia.Controls/TreeView.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index b4c30e0149..b9e30620bc 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -219,7 +219,9 @@ namespace Avalonia.Controls private void SelectSingleItem(object item) { + _syncingSelectedItems = true; SelectedItems.Clear(); + _syncingSelectedItems = false; SelectedItems.Add(item); } @@ -353,7 +355,7 @@ namespace Avalonia.Controls MarkItemSelected(item, true); } - if (SelectedItem == null && !_syncingSelectedItems) + if (!_syncingSelectedItems) { SetAndRaise(SelectedItemProperty, ref _selectedItem, items[0]); } From 0416b234b74469f9cde05ae478b16a9a11bca41c Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 1 Oct 2020 12:52:50 +0300 Subject: [PATCH 13/71] fix #4786 invalidatevisual when foreground changes in TextPresenter --- src/Avalonia.Controls/Presenters/TextPresenter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index f5115a2f7c..8efbe1719e 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -77,7 +77,7 @@ namespace Avalonia.Controls.Presenters static TextPresenter() { - AffectsRender(SelectionBrushProperty); + AffectsRender(SelectionBrushProperty, TextBlock.ForegroundProperty); AffectsMeasure(TextProperty, PasswordCharProperty, RevealPasswordProperty, TextAlignmentProperty, TextWrappingProperty, TextBlock.FontSizeProperty, TextBlock.FontStyleProperty, TextBlock.FontWeightProperty, TextBlock.FontFamilyProperty); From 4ca07974af61aa0e363aef8570433a9f7124eafc Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 1 Oct 2020 11:06:32 +0100 Subject: [PATCH 14/71] remove line that doesnt do anything. --- tests/Avalonia.Controls.UnitTests/TreeViewTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 9e22c44f44..d5b3e6350c 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -478,8 +478,6 @@ namespace Avalonia.Controls.UnitTests CreateNodeDataTemplate(target); ApplyTemplates(target); - var children = target.GetLogicalChildren().ToList(); - var selectedValues = new List(); dataContext.WhenAnyValue(x => x.SelectedItem) From 5d43f8f69907191f4ec2a109f43d70dd32267479 Mon Sep 17 00:00:00 2001 From: artyom Date: Thu, 1 Oct 2020 20:23:43 +0300 Subject: [PATCH 15/71] Add a CompositeTransition --- .../Animation/CompositeTransition.cs | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/Avalonia.Visuals/Animation/CompositeTransition.cs diff --git a/src/Avalonia.Visuals/Animation/CompositeTransition.cs b/src/Avalonia.Visuals/Animation/CompositeTransition.cs new file mode 100644 index 0000000000..62ed9d5bc1 --- /dev/null +++ b/src/Avalonia.Visuals/Animation/CompositeTransition.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Metadata; + +namespace Avalonia.Animation +{ + /// + /// Defines a composite transition that can be used to combine multiple transitions into one. + /// + /// + /// + /// Instantiate the in XAML and initialize the + /// property in order to have many animations triggered at once. + /// For example, you can combine and . + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// ]]> + /// + /// + /// + public class CompositeTransition : IPageTransition + { + /// + /// Gets or sets the transitions to be executed. Can be defined from XAML. + /// + [Content] + public List Transitions { get; set; } = new List(); + + /// + /// Starts the animation. + /// + /// + /// The control that is being transitioned away from. May be null. + /// + /// + /// The control that is being transitioned to. May be null. + /// + /// + /// Defines the direction of the transition. + /// + /// + /// A that tracks the progress of the animation. + /// + public Task Start(Visual from, Visual to, bool forward) + { + var transitionTasks = Transitions + .Select(transition => transition.Start(from, to, forward)) + .ToList(); + return Task.WhenAll(transitionTasks); + } + } +} From c3693a01cc1d6aa7cb692e92b46ed7298fcdfaae Mon Sep 17 00:00:00 2001 From: artyom Date: Fri, 2 Oct 2020 00:28:13 +0300 Subject: [PATCH 16/71] Ability to set easings --- ...ansition.cs => CompositePageTransition.cs} | 10 ++--- src/Avalonia.Visuals/Animation/CrossFade.cs | 29 ++++++++---- src/Avalonia.Visuals/Animation/PageSlide.cs | 44 +++++++++---------- 3 files changed, 47 insertions(+), 36 deletions(-) rename src/Avalonia.Visuals/Animation/{CompositeTransition.cs => CompositePageTransition.cs} (85%) diff --git a/src/Avalonia.Visuals/Animation/CompositeTransition.cs b/src/Avalonia.Visuals/Animation/CompositePageTransition.cs similarity index 85% rename from src/Avalonia.Visuals/Animation/CompositeTransition.cs rename to src/Avalonia.Visuals/Animation/CompositePageTransition.cs index 62ed9d5bc1..46bbb0bed2 100644 --- a/src/Avalonia.Visuals/Animation/CompositeTransition.cs +++ b/src/Avalonia.Visuals/Animation/CompositePageTransition.cs @@ -6,28 +6,28 @@ using Avalonia.Metadata; namespace Avalonia.Animation { /// - /// Defines a composite transition that can be used to combine multiple transitions into one. + /// Defines a composite page transition that can be used to combine multiple transitions. /// /// /// - /// Instantiate the in XAML and initialize the + /// Instantiate the in XAML and initialize the /// property in order to have many animations triggered at once. /// For example, you can combine and . /// /// /// - /// + /// /// /// - /// + /// /// /// /// ]]> /// /// /// - public class CompositeTransition : IPageTransition + public class CompositePageTransition : IPageTransition { /// /// Gets or sets the transitions to be executed. Can be defined from XAML. diff --git a/src/Avalonia.Visuals/Animation/CrossFade.cs b/src/Avalonia.Visuals/Animation/CrossFade.cs index 640f401418..2ba9f6aeba 100644 --- a/src/Avalonia.Visuals/Animation/CrossFade.cs +++ b/src/Avalonia.Visuals/Animation/CrossFade.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Avalonia.Animation.Easings; using Avalonia.Styling; using Avalonia.VisualTree; @@ -74,14 +75,26 @@ namespace Avalonia.Animation /// public TimeSpan Duration { - get - { - return _fadeOutAnimation.Duration; - } - set - { - _fadeOutAnimation.Duration = _fadeInAnimation.Duration = value; - } + get => _fadeOutAnimation.Duration; + set => _fadeOutAnimation.Duration = _fadeInAnimation.Duration = value; + } + + /// + /// Gets or sets element entrance easing. + /// + public Easing EntranceEasing + { + get => _fadeInAnimation.Easing; + set => _fadeInAnimation.Easing = value; + } + + /// + /// Gets or sets element exit easing. + /// + public Easing ExitEasing + { + get => _fadeOutAnimation.Easing; + set => _fadeOutAnimation.Easing = value; } /// diff --git a/src/Avalonia.Visuals/Animation/PageSlide.cs b/src/Avalonia.Visuals/Animation/PageSlide.cs index 501c8c0ba4..b545385ad6 100644 --- a/src/Avalonia.Visuals/Animation/PageSlide.cs +++ b/src/Avalonia.Visuals/Animation/PageSlide.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Avalonia.Animation.Easings; using Avalonia.Media; using Avalonia.Styling; using Avalonia.VisualTree; @@ -48,6 +49,16 @@ namespace Avalonia.Animation /// Gets the duration of the animation. /// public SlideAxis Orientation { get; set; } + + /// + /// Gets or sets element entrance easing. + /// + public Easing EntranceEasing { get; set; } = new LinearEasing(); + + /// + /// Gets or sets element exit easing. + /// + public Easing ExitEasing { get; set; } = new LinearEasing(); /// /// Starts the animation. @@ -75,18 +86,12 @@ namespace Avalonia.Animation { var animation = new Animation { - Children = + Easing = ExitEasing, + Children = { new KeyFrame { - Setters = - { - new Setter - { - Property = translateProperty, - Value = 0d - } - }, + Setters = { new Setter { Property = translateProperty, Value = 0d } }, Cue = new Cue(0d) }, new KeyFrame @@ -100,10 +105,10 @@ namespace Avalonia.Animation } }, Cue = new Cue(1d) - } - } + } + }, + Duration = Duration }; - animation.Duration = Duration; tasks.Add(animation.RunAsync(from)); } @@ -112,9 +117,9 @@ namespace Avalonia.Animation to.IsVisible = true; var animation = new Animation { + Easing = EntranceEasing, Children = { - new KeyFrame { Setters = @@ -129,19 +134,12 @@ namespace Avalonia.Animation }, new KeyFrame { - Setters = - { - new Setter - { - Property = translateProperty, - Value = 0d - } - }, + Setters = { new Setter { Property = translateProperty, Value = 0d } }, Cue = new Cue(1d) } - } + }, + Duration = Duration }; - animation.Duration = Duration; tasks.Add(animation.RunAsync(to)); } From 4aecb6b99ae29c926bc715cfec84370cb4b0949c Mon Sep 17 00:00:00 2001 From: artyom Date: Fri, 2 Oct 2020 00:29:06 +0300 Subject: [PATCH 17/71] Better naming for easings --- src/Avalonia.Visuals/Animation/CrossFade.cs | 4 ++-- src/Avalonia.Visuals/Animation/PageSlide.cs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Visuals/Animation/CrossFade.cs b/src/Avalonia.Visuals/Animation/CrossFade.cs index 2ba9f6aeba..0615b854da 100644 --- a/src/Avalonia.Visuals/Animation/CrossFade.cs +++ b/src/Avalonia.Visuals/Animation/CrossFade.cs @@ -82,7 +82,7 @@ namespace Avalonia.Animation /// /// Gets or sets element entrance easing. /// - public Easing EntranceEasing + public Easing FadeInEasing { get => _fadeInAnimation.Easing; set => _fadeInAnimation.Easing = value; @@ -91,7 +91,7 @@ namespace Avalonia.Animation /// /// Gets or sets element exit easing. /// - public Easing ExitEasing + public Easing FadeOutEasing { get => _fadeOutAnimation.Easing; set => _fadeOutAnimation.Easing = value; diff --git a/src/Avalonia.Visuals/Animation/PageSlide.cs b/src/Avalonia.Visuals/Animation/PageSlide.cs index b545385ad6..dd5d598e12 100644 --- a/src/Avalonia.Visuals/Animation/PageSlide.cs +++ b/src/Avalonia.Visuals/Animation/PageSlide.cs @@ -53,12 +53,12 @@ namespace Avalonia.Animation /// /// Gets or sets element entrance easing. /// - public Easing EntranceEasing { get; set; } = new LinearEasing(); + public Easing SlideInEasing { get; set; } = new LinearEasing(); /// /// Gets or sets element exit easing. /// - public Easing ExitEasing { get; set; } = new LinearEasing(); + public Easing SlideOutEasing { get; set; } = new LinearEasing(); /// /// Starts the animation. @@ -86,7 +86,7 @@ namespace Avalonia.Animation { var animation = new Animation { - Easing = ExitEasing, + Easing = SlideOutEasing, Children = { new KeyFrame @@ -117,7 +117,7 @@ namespace Avalonia.Animation to.IsVisible = true; var animation = new Animation { - Easing = EntranceEasing, + Easing = SlideInEasing, Children = { new KeyFrame From 06fc7dbc6c21fd0530061ab56e40108ab7261fa3 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 2 Oct 2020 10:48:50 +0300 Subject: [PATCH 18/71] add SelectionForegroundBrush, CaretBrush as AffectRenderer to TextPresenter --- src/Avalonia.Controls/Presenters/TextPresenter.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 8efbe1719e..6a6d37605d 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -77,7 +77,8 @@ namespace Avalonia.Controls.Presenters static TextPresenter() { - AffectsRender(SelectionBrushProperty, TextBlock.ForegroundProperty); + AffectsRender(SelectionBrushProperty, TextBlock.ForegroundProperty, + SelectionForegroundBrushProperty, CaretBrushProperty); AffectsMeasure(TextProperty, PasswordCharProperty, RevealPasswordProperty, TextAlignmentProperty, TextWrappingProperty, TextBlock.FontSizeProperty, TextBlock.FontStyleProperty, TextBlock.FontWeightProperty, TextBlock.FontFamilyProperty); From 3fd7892cc231e488163f10709d230ae266097dde Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 2 Oct 2020 09:33:18 +0100 Subject: [PATCH 19/71] use suggested implementation. --- src/Avalonia.Controls/TreeView.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index b9e30620bc..560cd9e810 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -117,10 +117,8 @@ namespace Avalonia.Controls if (value != null) { if (selectedItems.Count != 1 || selectedItems[0] != value) - { - _syncingSelectedItems = true; - SelectSingleItem(value); - _syncingSelectedItems = false; + { + SelectSingleItem(value); } } else if (SelectedItems.Count > 0) @@ -219,10 +217,13 @@ namespace Avalonia.Controls private void SelectSingleItem(object item) { + var oldValue = SelectedItem; _syncingSelectedItems = true; - SelectedItems.Clear(); - _syncingSelectedItems = false; + SelectedItems.Clear(); SelectedItems.Add(item); + _syncingSelectedItems = false; + + this.RaisePropertyChanged(SelectedItemProperty, oldValue, item); } /// @@ -355,7 +356,7 @@ namespace Avalonia.Controls MarkItemSelected(item, true); } - if (!_syncingSelectedItems) + if (SelectedItem == null && !_syncingSelectedItems) { SetAndRaise(SelectedItemProperty, ref _selectedItem, items[0]); } From 09069e28b22350e2338d5065848e89a8366a36f6 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 2 Oct 2020 10:00:38 +0100 Subject: [PATCH 20/71] fix implementation. --- src/Avalonia.Controls/TreeView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 560cd9e810..f0e00e1d7e 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -223,7 +223,7 @@ namespace Avalonia.Controls SelectedItems.Add(item); _syncingSelectedItems = false; - this.RaisePropertyChanged(SelectedItemProperty, oldValue, item); + SetAndRaise(SelectedItemProperty, ref _selectedItem, item); } /// From e893705823862945c4cfa0316e77f8293817d59c Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 2 Oct 2020 16:07:38 +0300 Subject: [PATCH 21/71] add a way to render to canvas with DrawingContext with public api --- .../ExternalCanvasRenderTarget.cs | 32 +++++++++++++++++++ .../Avalonia.Skia/IExternalCanvasSurface.cs | 11 +++++++ .../Avalonia.Skia/PlatformRenderInterface.cs | 2 ++ 3 files changed, 45 insertions(+) create mode 100644 src/Skia/Avalonia.Skia/ExternalCanvasRenderTarget.cs create mode 100644 src/Skia/Avalonia.Skia/IExternalCanvasSurface.cs diff --git a/src/Skia/Avalonia.Skia/ExternalCanvasRenderTarget.cs b/src/Skia/Avalonia.Skia/ExternalCanvasRenderTarget.cs new file mode 100644 index 0000000000..c9e779797b --- /dev/null +++ b/src/Skia/Avalonia.Skia/ExternalCanvasRenderTarget.cs @@ -0,0 +1,32 @@ +using Avalonia.Platform; +using Avalonia.Rendering; + +namespace Avalonia.Skia +{ + internal class ExternalCanvasRenderTarget : IRenderTarget + { + private IExternalCanvasSurface _surface; + + public ExternalCanvasRenderTarget(IExternalCanvasSurface canvas) + { + _surface = canvas; + } + + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + { + var createInfo = new DrawingContextImpl.CreateInfo + { + Canvas = _surface.Canvas, + Dpi = _surface.Dpi, + VisualBrushRenderer = visualBrushRenderer, + DisableTextLcdRendering = true, + }; + + return new DrawingContextImpl(createInfo); + } + + public void Dispose() + { + } + } +} diff --git a/src/Skia/Avalonia.Skia/IExternalCanvasSurface.cs b/src/Skia/Avalonia.Skia/IExternalCanvasSurface.cs new file mode 100644 index 0000000000..38347f2caa --- /dev/null +++ b/src/Skia/Avalonia.Skia/IExternalCanvasSurface.cs @@ -0,0 +1,11 @@ +using SkiaSharp; + +namespace Avalonia.Skia +{ + public interface IExternalCanvasSurface + { + SKCanvas Canvas { get; } + + Vector Dpi { get; } + } +} diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index b9c1cbc673..397d358ba7 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -145,6 +145,8 @@ namespace Avalonia.Skia { if (surface is IFramebufferPlatformSurface framebufferSurface) return new FramebufferRenderTarget(framebufferSurface); + if (surface is IExternalCanvasSurface canvas) + return new ExternalCanvasRenderTarget(canvas); } throw new NotSupportedException( From 7c250bba7f76f13cc4b3b5f9b6dc93bfdc28a82a Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 2 Oct 2020 18:58:00 +0300 Subject: [PATCH 22/71] Added optimized Blit method for render layers --- .../HeadlessPlatformRenderInterface.cs | 9 ++++- src/Avalonia.Visuals/ApiCompatBaseline.txt | 6 ++- .../Platform/IDrawingContextImpl.cs | 11 +++++- .../Rendering/DeferredRenderer.cs | 8 +++- src/Avalonia.Visuals/Rendering/RenderLayer.cs | 2 +- .../SceneGraph/DeferredDrawingContextImpl.cs | 2 +- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 2 +- .../Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs | 20 +++++++++- src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs | 37 ++++++++++++------- .../ExternalRenderTarget.cs | 2 +- .../Avalonia.Direct2D1/ILayerFactory.cs | 2 +- .../Media/DrawingContextImpl.cs | 4 +- .../Imaging/D2DRenderTargetBitmapImpl.cs | 11 +++++- .../Avalonia.Direct2D1/RenderTarget.cs | 2 +- .../SwapChainRenderTarget.cs | 2 +- tests/Avalonia.UnitTests/TestRoot.cs | 2 +- 16 files changed, 91 insertions(+), 31 deletions(-) diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 3ae6c8c30e..246610b897 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -238,7 +238,7 @@ namespace Avalonia.Headless } } - class HeadlessBitmapStub : IBitmapImpl, IRenderTargetBitmapImpl, IWriteableBitmapImpl + class HeadlessBitmapStub : IBitmapImpl, IDrawingContextLayerImpl, IWriteableBitmapImpl { public Size Size { get; } @@ -267,6 +267,11 @@ namespace Avalonia.Headless return new HeadlessDrawingContextStub(); } + public void Blit(IDrawingContextImpl context) + { + + } + public Vector Dpi { get; } public PixelSize PixelSize { get; } public int Version { get; set; } @@ -307,7 +312,7 @@ namespace Avalonia.Headless } - public IRenderTargetBitmapImpl CreateLayer(Size size) + public IDrawingContextLayerImpl CreateLayer(Size size) { return new HeadlessBitmapStub(size, new Vector(96, 96)); } diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index 5aa497861d..39cf5ee4dd 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -9,7 +9,11 @@ TypesMustExist : Type 'Avalonia.Media.Fonts.FontKey' does not exist in the imple CannotAddAbstractMembers : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.TextLineBreak' is abstract in the implementation but is missing in the contract. MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.LineBreak.get()' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.TextLineBreak.get()' is abstract in the implementation but is missing in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IDrawingContextLayerImpl Avalonia.Platform.IDrawingContextImpl.CreateLayer(Avalonia.Size)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IRenderTargetBitmapImpl Avalonia.Platform.IDrawingContextImpl.CreateLayer(Avalonia.Size)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public Avalonia.Platform.IRenderTargetBitmapImpl Avalonia.Platform.IDrawingContextImpl.CreateLayer(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Fonts.FontKey)' is present in the contract but not in the implementation. MembersMustExist : Member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Fonts.FontKey)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Typeface)' is present in the implementation but not in the contract. -Total Issues: 13 +MembersMustExist : Member 'public Avalonia.Utilities.IRef Avalonia.Rendering.RenderLayer.Bitmap.get()' does not exist in the implementation but it does exist in the contract. +Total Issues: 17 diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index c87946b3ea..9eaef0c616 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -99,7 +99,7 @@ namespace Avalonia.Platform /// has to do a format conversion each time a standard render target bitmap is rendered, /// but a layer created via this method has no such overhead. /// - IRenderTargetBitmapImpl CreateLayer(Size size); + IDrawingContextLayerImpl CreateLayer(Size size); /// /// Pushes a clip rectangle. @@ -156,4 +156,13 @@ namespace Avalonia.Platform /// Custom draw operation void Custom(ICustomDrawOperation custom); } + + public interface IDrawingContextLayerImpl : IRenderTargetBitmapImpl + { + /// + /// Does optimized blit with Src blend mode + /// + /// + void Blit(IDrawingContextImpl context); + } } diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 8c020fc073..71b9553256 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -489,6 +489,7 @@ namespace Avalonia.Rendering var clientRect = new Rect(scene.Size); + var firstLayer = true; foreach (var layer in scene.Layers) { var bitmap = Layers[layer.LayerRoot].Bitmap; @@ -501,7 +502,10 @@ namespace Avalonia.Rendering if (layer.OpacityMask == null) { - context.DrawBitmap(bitmap, layer.Opacity, sourceRect, clientRect); + if (firstLayer) + bitmap.Item.Blit(context); + else + context.DrawBitmap(bitmap, layer.Opacity, sourceRect, clientRect); } else { @@ -512,6 +516,8 @@ namespace Avalonia.Rendering { context.PopGeometryClip(); } + + firstLayer = false; } if (_overlay != null) diff --git a/src/Avalonia.Visuals/Rendering/RenderLayer.cs b/src/Avalonia.Visuals/Rendering/RenderLayer.cs index ddf5f4e5cf..7a79e45716 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLayer.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLayer.cs @@ -20,7 +20,7 @@ namespace Avalonia.Rendering IsEmpty = true; } - public IRef Bitmap { get; private set; } + public IRef Bitmap { get; private set; } public bool IsEmpty { get; set; } public double Scaling { get; private set; } public Size Size { get; private set; } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 4a364998fd..e5d57966e4 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -218,7 +218,7 @@ namespace Avalonia.Rendering.SceneGraph ++_drawOperationindex; } } - public IRenderTargetBitmapImpl CreateLayer(Size size) + public IDrawingContextLayerImpl CreateLayer(Size size) { throw new NotSupportedException("Creating layers on a deferred drawing context not supported"); } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 669da8d03c..231f0ab328 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -422,7 +422,7 @@ namespace Avalonia.Skia } /// - public IRenderTargetBitmapImpl CreateLayer(Size size) + public IDrawingContextLayerImpl CreateLayer(Size size) { return CreateRenderTarget( size); } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs index f606929b76..961b6bb284 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs @@ -1,4 +1,6 @@ +using System; using System.Collections.Generic; +using Avalonia.Logging; using Avalonia.OpenGL; using Avalonia.OpenGL.Imaging; using Avalonia.OpenGL.Surfaces; @@ -10,6 +12,7 @@ namespace Avalonia.Skia { private GRContext _grContext; private IGlContext _glContext; + private bool? _canCreateSurfaces; public GlSkiaGpu(IPlatformOpenGlInterface openGl, long? maxResourceBytes) { @@ -45,7 +48,22 @@ namespace Avalonia.Skia public ISkiaSurface TryCreateSurface(PixelSize size) { - return new FboSkiaSurface(_grContext, _glContext, size); + size = new PixelSize(Math.Max(size.Width, 1), Math.Max(size.Height, 1)); + if (_canCreateSurfaces == false) + return null; + try + { + var surface = new FboSkiaSurface(_grContext, _glContext, size); + _canCreateSurfaces = true; + return surface; + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL") + ?.Log(this, "Unable to create a Skia-compatible FBO manually"); + _canCreateSurfaces ??= false; + return null; + } } public IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi) => new GlOpenGlBitmapImpl(_glContext, size, dpi); diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index d0bd8ff150..314a3604e9 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Reactive.Disposables; +using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Skia.Helpers; @@ -11,7 +12,7 @@ namespace Avalonia.Skia /// /// Skia render target that writes to a surface. /// - internal class SurfaceRenderTarget : IRenderTargetBitmapImpl, IDrawableBitmapImpl + internal class SurfaceRenderTarget : IDrawingContextLayerImpl, IDrawableBitmapImpl { private readonly ISkiaSurface _surface; private readonly SKCanvas _canvas; @@ -133,23 +134,33 @@ namespace Avalonia.Skia } } - /// - public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) + public void Blit(IDrawingContextImpl contextImpl) { - if (sourceRect.Left == 0 && sourceRect.Top == 0 && sourceRect.Size == destRect.Size) + var context = (DrawingContextImpl)contextImpl; + + if (_surface.CanBlit) { _surface.Surface.Canvas.Flush(); - if (context.Canvas.TotalMatrix.IsIdentity && _surface.CanBlit && destRect.Top == 0 && - destRect.Left == 0) - _surface.Blit(); - else - _surface.Surface.Draw(context.Canvas, destRect.Left, destRect.Top, paint); + + // This should set the render target as the current FBO + context.Canvas.Clear(); + context.Canvas.Flush(); + _surface.Blit(); } else - using (var image = SnapshotImage()) - { - context.Canvas.DrawImage(image, sourceRect, destRect, paint); - } + { + var oldMatrix = context.Canvas.TotalMatrix; + context.Canvas.ResetMatrix(); + _surface.Surface.Draw(context.Canvas, 0, 0, null); + context.Canvas.SetMatrix(oldMatrix); + } + } + + /// + public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) + { + using var image = SnapshotImage(); + context.Canvas.DrawImage(image, sourceRect, destRect, paint); } /// diff --git a/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs index aad50331d2..2c0adcac32 100644 --- a/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs @@ -38,7 +38,7 @@ namespace Avalonia.Direct2D1 }); } - public IRenderTargetBitmapImpl CreateLayer(Size size) + public IDrawingContextLayerImpl CreateLayer(Size size) { var renderTarget = _externalRenderTargetProvider.GetOrCreateRenderTarget(); return D2DRenderTargetBitmapImpl.CreateCompatible(renderTarget, size); diff --git a/src/Windows/Avalonia.Direct2D1/ILayerFactory.cs b/src/Windows/Avalonia.Direct2D1/ILayerFactory.cs index 08273f1bb2..a15bc0056a 100644 --- a/src/Windows/Avalonia.Direct2D1/ILayerFactory.cs +++ b/src/Windows/Avalonia.Direct2D1/ILayerFactory.cs @@ -4,6 +4,6 @@ namespace Avalonia.Direct2D1 { public interface ILayerFactory { - IRenderTargetBitmapImpl CreateLayer(Size size); + IDrawingContextLayerImpl CreateLayer(Size size); } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index e0de40525f..cf47668a63 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -334,7 +334,7 @@ namespace Avalonia.Direct2D1.Media } } - public IRenderTargetBitmapImpl CreateLayer(Size size) + public IDrawingContextLayerImpl CreateLayer(Size size) { if (_layerFactory != null) { @@ -345,7 +345,7 @@ namespace Avalonia.Direct2D1.Media var platform = AvaloniaLocator.Current.GetService(); var dpi = new Vector(_deviceContext.DotsPerInch.Width, _deviceContext.DotsPerInch.Height); var pixelSize = PixelSize.FromSizeWithDpi(size, dpi); - return platform.CreateRenderTargetBitmap(pixelSize, dpi); + return (IDrawingContextLayerImpl)platform.CreateRenderTargetBitmap(pixelSize, dpi); } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs index 3a3f9a9f7d..fa56ffb4a7 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs @@ -8,7 +8,7 @@ using D2DBitmap = SharpDX.Direct2D1.Bitmap; namespace Avalonia.Direct2D1.Media.Imaging { - public class D2DRenderTargetBitmapImpl : D2DBitmapImpl, IRenderTargetBitmapImpl, ILayerFactory + public class D2DRenderTargetBitmapImpl : D2DBitmapImpl, IDrawingContextLayerImpl, ILayerFactory { private readonly BitmapRenderTarget _renderTarget; @@ -34,7 +34,14 @@ namespace Avalonia.Direct2D1.Media.Imaging return new DrawingContextImpl(visualBrushRenderer, this, _renderTarget, null, () => Version++); } - public IRenderTargetBitmapImpl CreateLayer(Size size) + public void Blit(IDrawingContextImpl context) + { + var rc = new Rect(0, 0, PixelSize.Width, PixelSize.Height); + context.DrawBitmap(RefCountable.CreateUnownedNotClonable(this), + 1, rc, rc); + } + + public IDrawingContextLayerImpl CreateLayer(Size size) { return CreateCompatible(_renderTarget, size); } diff --git a/src/Windows/Avalonia.Direct2D1/RenderTarget.cs b/src/Windows/Avalonia.Direct2D1/RenderTarget.cs index 4433c8d50a..d04c616bd9 100644 --- a/src/Windows/Avalonia.Direct2D1/RenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/RenderTarget.cs @@ -30,7 +30,7 @@ namespace Avalonia.Direct2D1 return new DrawingContextImpl(visualBrushRenderer, this, _renderTarget); } - public IRenderTargetBitmapImpl CreateLayer(Size size) + public IDrawingContextLayerImpl CreateLayer(Size size) { return D2DRenderTargetBitmapImpl.CreateCompatible(_renderTarget, size); } diff --git a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs index 5f4d06dab0..f319cfae03 100644 --- a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs @@ -35,7 +35,7 @@ namespace Avalonia.Direct2D1 return new DrawingContextImpl(visualBrushRenderer, this, _deviceContext, _swapChain); } - public IRenderTargetBitmapImpl CreateLayer(Size size) + public IDrawingContextLayerImpl CreateLayer(Size size) { if (_deviceContext == null) { diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index b6f3a020e8..b69bf990d9 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -72,7 +72,7 @@ namespace Avalonia.UnitTests dc.Setup(x => x.CreateLayer(It.IsAny())).Returns(() => { var layerDc = new Mock(); - var layer = new Mock(); + var layer = new Mock(); layer.Setup(x => x.CreateDrawingContext(It.IsAny())).Returns(layerDc.Object); return layer.Object; }); From 858b78b59e92abb2809cf7f80b4fa2e4a1db050d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Komosi=C5=84ski?= Date: Fri, 2 Oct 2020 18:18:13 +0200 Subject: [PATCH 23/71] Menu interaction handler fixes. --- src/Avalonia.Controls/MenuItem.cs | 4 ++-- .../Platform/DefaultMenuInteractionHandler.cs | 18 +++++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 3d8ab3ae48..7d4fef009d 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -101,7 +101,7 @@ namespace Avalonia.Controls private ICommand? _command; private bool _commandCanExecute = true; - private Popup _popup; + private Popup? _popup; /// /// Initializes static members of the class. @@ -145,7 +145,7 @@ namespace Avalonia.Controls { var parent = x as Control; return parent?.GetObservable(DefinitionBase.PrivateSharedSizeScopeProperty) ?? - Observable.Return(null); + Observable.Return(null); }); this.Bind(DefinitionBase.PrivateSharedSizeScopeProperty, parentSharedSizeScope); diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 6d6398bcda..a54d1ce308 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -148,6 +148,7 @@ namespace Avalonia.Controls.Platform { case Key.Up: case Key.Down: + { if (item?.IsTopLevel == true) { if (item.HasSubMenu && !item.IsSubMenuOpen) @@ -161,8 +162,10 @@ namespace Avalonia.Controls.Platform goto default; } break; + } case Key.Left: + { if (item?.Parent is IMenuItem parent && !parent.IsTopLevel && parent.IsSubMenuOpen) { parent.Close(); @@ -174,8 +177,10 @@ namespace Avalonia.Controls.Platform goto default; } break; + } case Key.Right: + { if (item != null && !item.IsTopLevel && item.HasSubMenu) { Open(item, true); @@ -186,8 +191,10 @@ namespace Avalonia.Controls.Platform goto default; } break; + } case Key.Enter: + { if (item != null) { if (!item.HasSubMenu) @@ -202,12 +209,14 @@ namespace Avalonia.Controls.Platform e.Handled = true; } break; + } case Key.Escape: - if (item?.Parent != null) + { + if (item?.Parent is IMenuElement parent) { - item.Parent.Close(); - item.Parent.Focus(); + parent.Close(); + parent.Focus(); } else { @@ -216,8 +225,10 @@ namespace Avalonia.Controls.Platform e.Handled = true; break; + } default: + { var direction = e.Key.ToNavigationDirection(); if (direction.HasValue) @@ -246,6 +257,7 @@ namespace Avalonia.Controls.Platform } break; + } } if (!e.Handled && item?.Parent is IMenuItem parentItem) From 05a2fe3d7d468b454ce5f9232e23ca70ca9dd76f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 2 Oct 2020 21:44:45 +0300 Subject: [PATCH 24/71] Fixed FBO stencil info --- src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs index 40347e2cd1..a1fbba3b2e 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs @@ -90,7 +90,7 @@ namespace Avalonia.Skia throw new OpenGlException("Unable to create FBO with stencil"); } - var target = new GRBackendRenderTarget(pixelSize.Width, pixelSize.Height, 0, 0, + var target = new GRBackendRenderTarget(pixelSize.Width, pixelSize.Height, 0, 8, new GRGlFramebufferInfo((uint)_fbo, SKColorType.Rgba8888.ToGlSizedFormat())); Surface = SKSurface.Create(_grContext, target, GRSurfaceOrigin.BottomLeft, SKColorType.Rgba8888); From a4e4d6469d686e58fc52bea1c1462bd927a0c2a8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 2 Oct 2020 22:30:19 +0100 Subject: [PATCH 25/71] fix nit in test --- tests/Avalonia.Controls.UnitTests/TreeViewTests.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index d5b3e6350c..b4b14ba409 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1333,11 +1333,9 @@ namespace Avalonia.Controls.UnitTests public TestDataContext() { - Items = new ObservableCollection(Enumerable.Range(1, 5).Select(i => GenerateItem())); + Items = new ObservableCollection(Enumerable.Range(1, 5).Select(i => $"Item {i}")); } - private string GenerateItem() => $"Item {_counter++.ToString()}"; - public ObservableCollection Items { get; } public string SelectedItem From 5a675a073fbeb471bdec1c67e1ea99cc580cfb7f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 2 Oct 2020 23:36:46 +0100 Subject: [PATCH 26/71] fix test. --- tests/Avalonia.Controls.UnitTests/TreeViewTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index b4b14ba409..b90b74d794 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1333,7 +1333,7 @@ namespace Avalonia.Controls.UnitTests public TestDataContext() { - Items = new ObservableCollection(Enumerable.Range(1, 5).Select(i => $"Item {i}")); + Items = new ObservableCollection(Enumerable.Range(0, 5).Select(i => $"Item {i}")); } public ObservableCollection Items { get; } From 731974b04146d4ffcb1a972cfd623f6f6bb57a0b Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 3 Oct 2020 18:42:03 +0300 Subject: [PATCH 27/71] Only do manual layer fbo management on Windows --- .../HeadlessPlatformRenderInterface.cs | 2 ++ .../Platform/IDrawingContextImpl.cs | 5 +++++ .../Rendering/DeferredRenderer.cs | 2 +- src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs | 3 +-- .../Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs | 7 ++++++- src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs | 9 +++++++++ src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs | 16 ++++++---------- .../Media/Imaging/D2DRenderTargetBitmapImpl.cs | 12 +++++------- 8 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 246610b897..f88310a390 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -272,6 +272,8 @@ namespace Avalonia.Headless } + public bool CanBlit => false; + public Vector Dpi { get; } public PixelSize PixelSize { get; } public int Version { get; set; } diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index 9eaef0c616..17571cb1dd 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -164,5 +164,10 @@ namespace Avalonia.Platform /// /// void Blit(IDrawingContextImpl context); + + /// + /// Returns true if layer supports optimized blit + /// + bool CanBlit { get; } } } diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 71b9553256..15e14935ca 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -502,7 +502,7 @@ namespace Avalonia.Rendering if (layer.OpacityMask == null) { - if (firstLayer) + if (firstLayer && bitmap.Item.CanBlit) bitmap.Item.Blit(context); else context.DrawBitmap(bitmap, layer.Opacity, sourceRect, clientRect); diff --git a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs index c06b4af52f..869c261f1b 100644 --- a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs @@ -28,8 +28,7 @@ namespace Avalonia.Skia { SKSurface Surface { get; } bool CanBlit { get; } - void Blit(); - + void Blit(SKCanvas canvas); } public interface IOpenGlAwareSkiaGpu : ISkiaGpu diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs index a1fbba3b2e..9ee8d698ba 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/FboSkiaSurface.cs @@ -116,8 +116,13 @@ namespace Avalonia.Skia public SKSurface Surface { get; private set; } public bool CanBlit { get; } - public void Blit() + public void Blit(SKCanvas canvas) { + // This should set the render target as the current FBO + // which is definitely not the best method, but it works + canvas.Clear(); + canvas.Flush(); + var gl = _glContext.GlInterface; gl.GetIntegerv(GL_READ_FRAMEBUFFER_BINDING, out var oldRead); gl.BindFramebuffer(GL_READ_FRAMEBUFFER, _fbo); diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs index 961b6bb284..c02d813e24 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.InteropServices; using Avalonia.Logging; using Avalonia.OpenGL; using Avalonia.OpenGL.Imaging; @@ -48,6 +49,14 @@ namespace Avalonia.Skia public ISkiaSurface TryCreateSurface(PixelSize size) { + // Only windows platform needs our FBO trickery + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return null; + + // Blit feature requires glBlitFramebuffer + if (_glContext.GlInterface.BlitFramebuffer == null) + return null; + size = new PixelSize(Math.Max(size.Width, 1), Math.Max(size.Height, 1)); if (_canCreateSurfaces == false) return null; diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index 314a3604e9..61b599a731 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -23,20 +23,18 @@ namespace Avalonia.Skia class SkiaSurfaceWrapper : ISkiaSurface { public SKSurface Surface { get; private set; } - public SKSurface ReadSurface { get; private set; } public bool CanBlit => false; - public void Blit() => throw new NotSupportedException(); + public void Blit(SKCanvas canvas) => throw new NotSupportedException(); public SkiaSurfaceWrapper(SKSurface surface) { - Surface = ReadSurface = surface; + Surface = surface; } public void Dispose() { Surface?.Dispose(); Surface = null; - ReadSurface = null; } } @@ -141,11 +139,7 @@ namespace Avalonia.Skia if (_surface.CanBlit) { _surface.Surface.Canvas.Flush(); - - // This should set the render target as the current FBO - context.Canvas.Clear(); - context.Canvas.Flush(); - _surface.Blit(); + _surface.Blit(context.Canvas); } else { @@ -155,7 +149,9 @@ namespace Avalonia.Skia context.Canvas.SetMatrix(oldMatrix); } } - + + public bool CanBlit => true; + /// public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) { diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs index fa56ffb4a7..9a0e2ec00c 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Utilities; @@ -34,12 +35,9 @@ namespace Avalonia.Direct2D1.Media.Imaging return new DrawingContextImpl(visualBrushRenderer, this, _renderTarget, null, () => Version++); } - public void Blit(IDrawingContextImpl context) - { - var rc = new Rect(0, 0, PixelSize.Width, PixelSize.Height); - context.DrawBitmap(RefCountable.CreateUnownedNotClonable(this), - 1, rc, rc); - } + public void Blit(IDrawingContextImpl context) => throw new NotSupportedException(); + + public bool CanBlit => false; public IDrawingContextLayerImpl CreateLayer(Size size) { From c4bbe3ac5e0becdd7d7ff25eaebb2429b9bba0f3 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 3 Oct 2020 23:15:16 +0300 Subject: [PATCH 28/71] Added WGL support --- samples/RenderDemo/App.xaml.cs | 4 + src/Avalonia.Base/AvaloniaLocator.cs | 17 ++ .../Interop/UnmanagedMethods.cs | 92 +++++++++ .../Avalonia.Win32/OpenGl/WglConsts.cs | 62 ++++++ .../Avalonia.Win32/OpenGl/WglContext.cs | 85 ++++++++ .../Avalonia.Win32/OpenGl/WglDisplay.cs | 183 ++++++++++++++++++ .../OpenGl/WglGlPlatformSurface.cs | 85 ++++++++ .../OpenGl/WglPlatformOpenGlInterface.cs | 39 ++++ .../OpenGl/WglRestoreContext.cs | 39 ++++ src/Windows/Avalonia.Win32/Win32GlManager.cs | 18 +- src/Windows/Avalonia.Win32/Win32Platform.cs | 10 +- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 13 +- src/Windows/Avalonia.Win32/WindowImpl.cs | 9 +- 13 files changed, 635 insertions(+), 21 deletions(-) create mode 100644 src/Windows/Avalonia.Win32/OpenGl/WglConsts.cs create mode 100644 src/Windows/Avalonia.Win32/OpenGl/WglContext.cs create mode 100644 src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs create mode 100644 src/Windows/Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs create mode 100644 src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs create mode 100644 src/Windows/Avalonia.Win32/OpenGl/WglRestoreContext.cs diff --git a/samples/RenderDemo/App.xaml.cs b/samples/RenderDemo/App.xaml.cs index 233160b025..340ccdae19 100644 --- a/samples/RenderDemo/App.xaml.cs +++ b/samples/RenderDemo/App.xaml.cs @@ -18,6 +18,10 @@ namespace RenderDemo // App configuration, used by the entry point and previewer static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() + .With(new Win32PlatformOptions + { + OverlayPopups = true, + }) .UsePlatformDetect() .UseReactiveUI() .LogToDebug(); diff --git a/src/Avalonia.Base/AvaloniaLocator.cs b/src/Avalonia.Base/AvaloniaLocator.cs index f9bbe38bec..3163d15c1b 100644 --- a/src/Avalonia.Base/AvaloniaLocator.cs +++ b/src/Avalonia.Base/AvaloniaLocator.cs @@ -54,6 +54,23 @@ namespace Avalonia return _locator; } + public AvaloniaLocator ToLazy(Func func) where TImlp : TService + { + var constructed = false; + TImlp instance = default; + _locator._registry[typeof (TService)] = () => + { + if (!constructed) + { + instance = func(); + constructed = true; + } + + return instance; + }; + return _locator; + } + public AvaloniaLocator ToSingleton() where TImpl : class, TService, new() { TImpl instance = null; diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index b7c68c4b95..5feb6c9e46 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1011,6 +1011,10 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern bool InvalidateRect(IntPtr hWnd, RECT* lpRect, bool bErase); + + + [DllImport("user32.dll")] + public static extern bool ValidateRect(IntPtr hWnd, IntPtr lpRect); [DllImport("user32.dll")] public static extern bool IsWindowEnabled(IntPtr hWnd); @@ -1292,6 +1296,41 @@ namespace Avalonia.Win32.Interop [DllImport("gdi32.dll")] public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hObject); + [DllImport("gdi32.dll")] + public static extern int ChoosePixelFormat(IntPtr hdc, ref PixelFormatDescriptor pfd); + + [DllImport("gdi32.dll")] + public static extern int DescribePixelFormat(IntPtr hdc, ref PixelFormatDescriptor pfd); + + [DllImport("gdi32.dll")] + public static extern int SetPixelFormat(IntPtr hdc, int iPixelFormat, ref PixelFormatDescriptor pfd); + + + [DllImport("gdi32.dll")] + public static extern int DescribePixelFormat(IntPtr hdc, int iPixelFormat, int bytes, ref PixelFormatDescriptor pfd); + + [DllImport("gdi32.dll")] + public static extern bool SwapBuffers(IntPtr hdc); + + [DllImport("opengl32.dll")] + public static extern IntPtr wglCreateContext(IntPtr hdc); + + [DllImport("opengl32.dll")] + public static extern bool wglDeleteContext(IntPtr context); + + + [DllImport("opengl32.dll")] + public static extern bool wglMakeCurrent(IntPtr hdc, IntPtr context); + + [DllImport("opengl32.dll")] + public static extern IntPtr wglGetCurrentContext(); + + [DllImport("opengl32.dll")] + public static extern IntPtr wglGetCurrentDC(); + + [DllImport("opengl32.dll", CharSet = CharSet.Ansi)] + public static extern IntPtr wglGetProcAddress(string name); + [DllImport("kernel32.dll", SetLastError = true)] public static extern IntPtr CreateFileMapping(IntPtr hFile, IntPtr lpFileMappingAttributes, @@ -2121,4 +2160,57 @@ namespace Avalonia.Win32.Interop public bool fNC; public bool fWide; } + + [Flags] + internal enum PixelFormatDescriptorFlags : uint + { + PFD_DOUBLEBUFFER = 0x00000001, + PFD_STEREO = 0x00000002, + PFD_DRAW_TO_WINDOW = 0x00000004, + PFD_DRAW_TO_BITMAP = 0x00000008, + PFD_SUPPORT_GDI = 0x00000010, + PFD_SUPPORT_OPENGL = 0x00000020, + PFD_GENERIC_FORMAT = 0x00000040, + PFD_NEED_PALETTE = 0x00000080, + PFD_NEED_SYSTEM_PALETTE = 0x00000100, + PFD_SWAP_EXCHANGE = 0x00000200, + PFD_SWAP_COPY = 0x00000400, + PFD_SWAP_LAYER_BUFFERS = 0x00000800, + PFD_GENERIC_ACCELERATED = 0x00001000, + PFD_SUPPORT_DIRECTDRAW = 0x00002000, + PFD_DEPTH_DONTCARE = 0x20000000, + PFD_DOUBLEBUFFER_DONTCARE = 0x40000000, + PFD_STEREO_DONTCARE = 0x80000000, + } + + [StructLayout(LayoutKind.Sequential)] + internal struct PixelFormatDescriptor + { + public ushort Size; + public ushort Version; + public PixelFormatDescriptorFlags Flags; + public byte PixelType; + public byte ColorBits; + public byte RedBits; + public byte RedShift; + public byte GreenBits; + public byte GreenShift; + public byte BlueBits; + public byte BlueShift; + public byte AlphaBits; + public byte AlphaShift; + public byte AccumBits; + public byte AccumRedBits; + public byte AccumGreenBits; + public byte AccumBlueBits; + public byte AccumAlphaBits; + public byte DepthBits; + public byte StencilBits; + public byte AuxBuffers; + public byte LayerType; + private byte Reserved; + public uint LayerMask; + public uint VisibleMask; + public uint DamageMask; + } } diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglConsts.cs b/src/Windows/Avalonia.Win32/OpenGl/WglConsts.cs new file mode 100644 index 0000000000..3cb3dc25bb --- /dev/null +++ b/src/Windows/Avalonia.Win32/OpenGl/WglConsts.cs @@ -0,0 +1,62 @@ +namespace Avalonia.Win32.OpenGl +{ + internal class WglConsts + { + public const int WGL_CONTEXT_MAJOR_VERSION_ARB = 0x2091; + public const int WGL_CONTEXT_MINOR_VERSION_ARB = 0x2092; + public const int WGL_CONTEXT_LAYER_PLANE_ARB = 0x2093; + public const int WGL_CONTEXT_FLAGS_ARB = 0x2094; + public const int WGL_CONTEXT_PROFILE_MASK_ARB = 0x9126; + public const int WGL_NUMBER_PIXEL_FORMATS_ARB = 0x2000; + + public const int WGL_DRAW_TO_WINDOW_ARB = 0x2001; + public const int WGL_DRAW_TO_BITMAP_ARB = 0x2002; + public const int WGL_ACCELERATION_ARB = 0x2003; + public const int WGL_NEED_PALETTE_ARB = 0x2004; + public const int WGL_NEED_SYSTEM_PALETTE_ARB = 0x2005; + public const int WGL_SWAP_LAYER_BUFFERS_ARB = 0x2006; + public const int WGL_SWAP_METHOD_ARB = 0x2007; + public const int WGL_NUMBER_OVERLAYS_ARB = 0x2008; + public const int WGL_NUMBER_UNDERLAYS_ARB = 0x2009; + public const int WGL_TRANSPARENT_ARB = 0x200A; + public const int WGL_TRANSPARENT_RED_VALUE_ARB = 0x2037; + public const int WGL_TRANSPARENT_GREEN_VALUE_ARB = 0x2038; + public const int WGL_TRANSPARENT_BLUE_VALUE_ARB = 0x2039; + public const int WGL_TRANSPARENT_ALPHA_VALUE_ARB = 0x203A; + public const int WGL_TRANSPARENT_INDEX_VALUE_ARB = 0x203B; + public const int WGL_SHARE_DEPTH_ARB = 0x200C; + public const int WGL_SHARE_STENCIL_ARB = 0x200D; + public const int WGL_SHARE_ACCUM_ARB = 0x200E; + public const int WGL_SUPPORT_GDI_ARB = 0x200F; + public const int WGL_SUPPORT_OPENGL_ARB = 0x2010; + public const int WGL_DOUBLE_BUFFER_ARB = 0x2011; + public const int WGL_STEREO_ARB = 0x2012; + public const int WGL_PIXEL_TYPE_ARB = 0x2013; + public const int WGL_COLOR_BITS_ARB = 0x2014; + public const int WGL_RED_BITS_ARB = 0x2015; + public const int WGL_RED_SHIFT_ARB = 0x2016; + public const int WGL_GREEN_BITS_ARB = 0x2017; + public const int WGL_GREEN_SHIFT_ARB = 0x2018; + public const int WGL_BLUE_BITS_ARB = 0x2019; + public const int WGL_BLUE_SHIFT_ARB = 0x201A; + public const int WGL_ALPHA_BITS_ARB = 0x201B; + public const int WGL_ALPHA_SHIFT_ARB = 0x201C; + public const int WGL_ACCUM_BITS_ARB = 0x201D; + public const int WGL_ACCUM_RED_BITS_ARB = 0x201E; + public const int WGL_ACCUM_GREEN_BITS_ARB = 0x201F; + public const int WGL_ACCUM_BLUE_BITS_ARB = 0x2020; + public const int WGL_ACCUM_ALPHA_BITS_ARB = 0x2021; + public const int WGL_DEPTH_BITS_ARB = 0x2022; + public const int WGL_STENCIL_BITS_ARB = 0x2023; + public const int WGL_AUX_BUFFERS_ARB = 0x2024; + public const int WGL_NO_ACCELERATION_ARB = 0x2025; + public const int WGL_GENERIC_ACCELERATION_ARB = 0x2026; + public const int WGL_FULL_ACCELERATION_ARB = 0x2027; + public const int WGL_SWAP_EXCHANGE_ARB = 0x2028; + public const int WGL_SWAP_COPY_ARB = 0x2029; + public const int WGL_SWAP_UNDEFINED_ARB = 0x202A; + public const int WGL_TYPE_RGBA_ARB = 0x202B; + public const int WGL_TYPE_COLORINDEX_ARB = 0x202C; + + } +} diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs b/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs new file mode 100644 index 0000000000..d6633ddb61 --- /dev/null +++ b/src/Windows/Avalonia.Win32/OpenGl/WglContext.cs @@ -0,0 +1,85 @@ +using System; +using System.Reactive.Disposables; +using Avalonia.OpenGL; +using Avalonia.Win32.Interop; +using static Avalonia.Win32.Interop.UnmanagedMethods; +using static Avalonia.Win32.OpenGl.WglConsts; + +namespace Avalonia.Win32.OpenGl +{ + class WglContext : IGlContext + { + private object _lock = new object(); + private readonly WglContext _sharedWith; + private readonly IntPtr _context; + private readonly IntPtr _hWnd; + private readonly IntPtr _dc; + private readonly int _pixelFormat; + private readonly PixelFormatDescriptor _formatDescriptor; + public IntPtr Handle => _context; + + public WglContext(WglContext sharedWith, GlVersion version, IntPtr context, IntPtr hWnd, IntPtr dc, int pixelFormat, + PixelFormatDescriptor formatDescriptor) + { + Version = version; + _sharedWith = sharedWith; + _context = context; + _hWnd = hWnd; + _dc = dc; + _pixelFormat = pixelFormat; + _formatDescriptor = formatDescriptor; + StencilSize = formatDescriptor.StencilBits; + using (MakeCurrent()) + GlInterface = new GlInterface(version, proc => + { + var ext = wglGetProcAddress(proc); + if (ext != IntPtr.Zero) + return ext; + return GetProcAddress(WglDisplay.OpenGl32Handle, proc); + }); + + } + + public void Dispose() + { + wglDeleteContext(_context); + ReleaseDC(_hWnd, _dc); + DestroyWindow(_hWnd); + } + + public GlVersion Version { get; } + public GlInterface GlInterface { get; } + public int SampleCount { get; } + public int StencilSize { get; } + + private bool IsCurrent => wglGetCurrentContext() == _context && wglGetCurrentDC() == _dc; + public IDisposable MakeCurrent() + { + if(IsCurrent) + return Disposable.Empty; + return new WglRestoreContext(_dc, _context, _lock); + } + + public IDisposable EnsureCurrent() => MakeCurrent(); + + + public IntPtr CreateConfiguredDeviceContext(IntPtr hWnd) + { + var dc = GetDC(hWnd); + var fmt = _formatDescriptor; + SetPixelFormat(dc, _pixelFormat, ref fmt); + return dc; + } + + public IDisposable MakeCurrent(IntPtr hdc) => new WglRestoreContext(hdc, _context, _lock); + + public bool IsSharedWith(IGlContext context) + { + var c = (WglContext)context; + return c == this + || c._sharedWith == this + || _sharedWith == context + || _sharedWith != null && _sharedWith == c._sharedWith; + } + } +} diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs b/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs new file mode 100644 index 0000000000..0d387bdb2c --- /dev/null +++ b/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs @@ -0,0 +1,183 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.OpenGL; +using Avalonia.Win32.Interop; +using static Avalonia.Win32.Interop.UnmanagedMethods; +using static Avalonia.Win32.OpenGl.WglConsts; +namespace Avalonia.Win32.OpenGl +{ + internal class WglDisplay + { + private static bool? _initialized; + private static ushort _windowClass; + private static readonly WndProc _wndProcDelegate = WndProc; + private static readonly DebugCallbackDelegate _debugCallback = DebugCallback; + + private static IntPtr _bootstrapContext; + private static IntPtr _bootstrapWindow; + private static IntPtr _bootstrapDc; + private static PixelFormatDescriptor _defaultPfd; + private static int _defaultPixelFormat; + public static IntPtr OpenGl32Handle = LoadLibrary("opengl32"); + + private delegate bool WglChoosePixelFormatARBDelegate(IntPtr hdc, int[] piAttribIList, float[] pfAttribFList, + int nMaxFormats, int[] piFormats, out int nNumFormats); + + private static WglChoosePixelFormatARBDelegate WglChoosePixelFormatArb; + + private delegate IntPtr WglCreateContextAttribsARBDelegate(IntPtr hDC, IntPtr hShareContext, int[] attribList); + + private static WglCreateContextAttribsARBDelegate WglCreateContextAttribsArb; + + private delegate void GlDebugMessageCallbackDelegate(IntPtr callback, IntPtr userParam); + + private static GlDebugMessageCallbackDelegate GlDebugMessageCallback; + + private delegate void DebugCallbackDelegate(int source, int type, int id, int severity, int len, IntPtr message, + IntPtr userParam); + + static bool Initialize() + { + if (!_initialized.HasValue) + _initialized = InitializeCore(); + return _initialized.Value; + } + static bool InitializeCore() + { + var wndClassEx = new WNDCLASSEX + { + cbSize = Marshal.SizeOf(), + hInstance = GetModuleHandle(null), + lpfnWndProc = _wndProcDelegate, + lpszClassName = "AvaloniaGlWindow-" + Guid.NewGuid(), + style = (int)ClassStyles.CS_OWNDC + }; + + _windowClass = RegisterClassEx(ref wndClassEx); + _bootstrapWindow = CreateOffscreenWindow(); + _bootstrapDc = GetDC(_bootstrapWindow); + _defaultPfd = new PixelFormatDescriptor + { + Size = (ushort)Marshal.SizeOf(), + Version = 1, + Flags = PixelFormatDescriptorFlags.PFD_DRAW_TO_WINDOW | + PixelFormatDescriptorFlags.PFD_SUPPORT_OPENGL | PixelFormatDescriptorFlags.PFD_DOUBLEBUFFER, + DepthBits = 24, + StencilBits = 8, + ColorBits = 32 + }; + _defaultPixelFormat = ChoosePixelFormat(_bootstrapDc, ref _defaultPfd); + SetPixelFormat(_bootstrapDc, _defaultPixelFormat, ref _defaultPfd); + + _bootstrapContext = wglCreateContext(_bootstrapDc); + if (_bootstrapContext == IntPtr.Zero) + return false; + + wglMakeCurrent(_bootstrapDc, _bootstrapContext); + WglCreateContextAttribsArb = Marshal.GetDelegateForFunctionPointer( + wglGetProcAddress("wglCreateContextAttribsARB")); + + WglChoosePixelFormatArb = + Marshal.GetDelegateForFunctionPointer( + wglGetProcAddress("wglChoosePixelFormatARB")); + + GlDebugMessageCallback = + Marshal.GetDelegateForFunctionPointer( + wglGetProcAddress("glDebugMessageCallback")); + + + var formats = new int[1]; + WglChoosePixelFormatArb(_bootstrapDc, new int[] + { + WGL_DRAW_TO_WINDOW_ARB, 1, + WGL_ACCELERATION_ARB, WGL_FULL_ACCELERATION_ARB, + WGL_SUPPORT_OPENGL_ARB, 1, + WGL_DOUBLE_BUFFER_ARB, 1, + WGL_PIXEL_TYPE_ARB, WGL_TYPE_RGBA_ARB, + WGL_COLOR_BITS_ARB, 32, + WGL_DEPTH_BITS_ARB, 0, + WGL_STENCIL_BITS_ARB, 0, + 0, // End + }, null, 1, formats, out int numFormats); + if (numFormats != 0) + { + DescribePixelFormat(_bootstrapDc, formats[0], Marshal.SizeOf(), ref _defaultPfd); + _defaultPixelFormat = formats[0]; + } + + + wglMakeCurrent(IntPtr.Zero, IntPtr.Zero); + return true; + } + + static IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + return DefWindowProc(hWnd, msg, wParam, lParam); + } + + private static void DebugCallback(int source, int type, int id, int severity, int len, IntPtr message, IntPtr userparam) + { + var err = Marshal.PtrToStringAnsi(message, len); + Console.Error.WriteLine(err); + } + + public static WglContext CreateContext(GlVersion[] versions, IGlContext share) + { + if (!Initialize()) + return null; + + var shareContext = (WglContext)share; + + using (new WglRestoreContext(_bootstrapDc, _bootstrapContext, null)) + { + var window = CreateOffscreenWindow(); + var dc = GetDC(window); + SetPixelFormat(dc, _defaultPixelFormat, ref _defaultPfd); + foreach (var version in versions) + { + if(version.Type != GlProfileType.OpenGL) + continue; + var context = WglCreateContextAttribsArb(dc, shareContext?.Handle ?? IntPtr.Zero, + new[] + { + // major + WGL_CONTEXT_MAJOR_VERSION_ARB, version.Major, + // minor + WGL_CONTEXT_MINOR_VERSION_ARB, version.Minor, + // core profile + WGL_CONTEXT_PROFILE_MASK_ARB, 1, + // debug + WGL_CONTEXT_FLAGS_ARB, 1, + // end + 0, 0 + }); + using(new WglRestoreContext(dc, context, null)) + GlDebugMessageCallback(Marshal.GetFunctionPointerForDelegate(_debugCallback), IntPtr.Zero); + if (context != IntPtr.Zero) + return new WglContext(shareContext, version, context, window, dc, + _defaultPixelFormat, _defaultPfd); + } + + ReleaseDC(window, dc); + DestroyWindow(window); + return null; + } + } + + + static IntPtr CreateOffscreenWindow() => + CreateWindowEx( + 0, + _windowClass, + null, + (int)WindowStyles.WS_OVERLAPPEDWINDOW, + 0, + 0, + 640, + 480, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero); + } +} diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs b/src/Windows/Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs new file mode 100644 index 0000000000..72bcffd447 --- /dev/null +++ b/src/Windows/Avalonia.Win32/OpenGl/WglGlPlatformSurface.cs @@ -0,0 +1,85 @@ +using System; +using System.Diagnostics; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Egl; +using Avalonia.OpenGL.Surfaces; +using Avalonia.Win32.Interop; +using static Avalonia.OpenGL.GlConsts; +using static Avalonia.Win32.Interop.UnmanagedMethods; +namespace Avalonia.Win32.OpenGl +{ + class WglGlPlatformSurface: IGlPlatformSurface + { + + private readonly WglContext _context; + private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _info; + + public WglGlPlatformSurface(WglContext context, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) + { + _context = context; + _info = info; + } + + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + { + return new RenderTarget(_context, _info); + } + + class RenderTarget : IGlPlatformSurfaceRenderTarget + { + private readonly WglContext _context; + private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _info; + private IntPtr _hdc; + public RenderTarget(WglContext context, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info) + { + _context = context; + _info = info; + _hdc = context.CreateConfiguredDeviceContext(info.Handle); + } + + public void Dispose() + { + UnmanagedMethods.ReleaseDC(_hdc, _info.Handle); + } + + public IGlPlatformSurfaceRenderingSession BeginDraw() + { + var oldContext = _context.MakeCurrent(_hdc); + + // Reset to default FBO first + _context.GlInterface.BindFramebuffer(GL_FRAMEBUFFER, 0); + + return new Session(_context, _hdc, _info, oldContext); + } + + class Session : IGlPlatformSurfaceRenderingSession + { + private readonly WglContext _context; + private readonly IntPtr _hdc; + private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _info; + private readonly IDisposable _clearContext; + public IGlContext Context => _context; + + public Session(WglContext context, IntPtr hdc, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info, + IDisposable clearContext) + { + _context = context; + _hdc = hdc; + _info = info; + _clearContext = clearContext; + } + + public void Dispose() + { + _context.GlInterface.Flush(); + UnmanagedMethods.SwapBuffers(_hdc); + _clearContext.Dispose(); + } + + public PixelSize Size => _info.Size; + public double Scaling => _info.Scaling; + public bool IsYFlipped { get; } + } + } + } +} diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs b/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs new file mode 100644 index 0000000000..b948495b99 --- /dev/null +++ b/src/Windows/Avalonia.Win32/OpenGl/WglPlatformOpenGlInterface.cs @@ -0,0 +1,39 @@ +using System; +using System.Linq; +using Avalonia.Logging; +using Avalonia.OpenGL; + +namespace Avalonia.Win32.OpenGl +{ + class WglPlatformOpenGlInterface : IPlatformOpenGlInterface + { + public WglContext PrimaryContext { get; } + IGlContext IPlatformOpenGlInterface.PrimaryContext => PrimaryContext; + public IGlContext CreateSharedContext() => WglDisplay.CreateContext(new[] { PrimaryContext.Version }, PrimaryContext); + + public bool CanShareContexts => true; + public bool CanCreateContexts => true; + public IGlContext CreateContext() => WglDisplay.CreateContext(new[] { PrimaryContext.Version }, null); + + private WglPlatformOpenGlInterface(WglContext primary) + { + PrimaryContext = primary; + } + + public static WglPlatformOpenGlInterface TryCreate() + { + try + { + var opts = AvaloniaLocator.Current.GetService() ?? new Win32PlatformOptions(); + var primary = WglDisplay.CreateContext(opts.WglProfiles.ToArray(), null); + return new WglPlatformOpenGlInterface(primary); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("WGL", "Unable to initialize WGL: " + e); + } + + return null; + } + } +} diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglRestoreContext.cs b/src/Windows/Avalonia.Win32/OpenGl/WglRestoreContext.cs new file mode 100644 index 0000000000..265f078a5c --- /dev/null +++ b/src/Windows/Avalonia.Win32/OpenGl/WglRestoreContext.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading; +using Avalonia.OpenGL; +using static Avalonia.Win32.Interop.UnmanagedMethods; + +namespace Avalonia.Win32.OpenGl +{ + internal class WglRestoreContext : IDisposable + { + private readonly object _monitor; + private readonly IntPtr _oldDc; + private readonly IntPtr _oldContext; + + public WglRestoreContext(IntPtr gc, IntPtr context, object monitor, bool takeMonitor = true) + { + _monitor = monitor; + _oldDc = wglGetCurrentDC(); + _oldContext = wglGetCurrentContext(); + + if (monitor != null && takeMonitor) + Monitor.Enter(monitor); + + if (!wglMakeCurrent(gc, context)) + { + if(monitor != null && takeMonitor) + Monitor.Exit(monitor); + throw new OpenGlException("Unable to make the context current"); + } + } + + public void Dispose() + { + if (!wglMakeCurrent(_oldDc, _oldContext)) + wglMakeCurrent(IntPtr.Zero, IntPtr.Zero); + if (_monitor != null) + Monitor.Exit(_monitor); + } + } +} diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index fbc56e7703..523a059e0e 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -1,27 +1,29 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Angle; using Avalonia.OpenGL.Egl; +using Avalonia.Win32.OpenGl; namespace Avalonia.Win32 { static class Win32GlManager { - /// This property is initialized if drawing platform requests OpenGL support - public static EglPlatformOpenGlInterface EglPlatformInterface { get; private set; } - private static bool s_attemptedToInitialize; public static void Initialize() { - AvaloniaLocator.CurrentMutable.Bind().ToFunc(() => + AvaloniaLocator.CurrentMutable.Bind().ToLazy(() => { - if (!s_attemptedToInitialize) + var opts = AvaloniaLocator.Current.GetService(); + if (opts?.UseWgl == true) { - EglPlatformInterface = EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); - s_attemptedToInitialize = true; + var wgl = WglPlatformOpenGlInterface.TryCreate(); + return wgl; } + + if (opts?.AllowEglInitialization == true) + return EglPlatformOpenGlInterface.TryCreate(() => new AngleWin32EglDisplay()); - return EglPlatformInterface; + return null; }); } } diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index af6058d197..61c1a4f45e 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -11,6 +11,7 @@ using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Platform; +using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; @@ -39,6 +40,12 @@ namespace Avalonia public bool AllowEglInitialization { get; set; } = true; public bool? EnableMultitouch { get; set; } public bool OverlayPopups { get; set; } + public bool UseWgl { get; set; } + public IList WglProfiles { get; set; } = new List + { + new GlVersion(GlProfileType.OpenGL, 4, 0), + new GlVersion(GlProfileType.OpenGL, 3, 2), + }; } } @@ -96,8 +103,7 @@ namespace Avalonia.Win32 .Bind().ToConstant(new NonPumpingWaitProvider()) .Bind().ToConstant(new WindowsMountedVolumeInfoProvider()); - if (options.AllowEglInitialization) - Win32GlManager.Initialize(); + Win32GlManager.Initialize(); _uiThread = Thread.CurrentThread; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 25a34561fc..655dfa5c48 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -338,19 +338,14 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_PAINT: - { + { using (_rendererLock.Lock()) { - if (BeginPaint(_hwnd, out PAINTSTRUCT ps) != IntPtr.Zero) - { - var f = RenderScaling; - var r = ps.rcPaint; - Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f, - (r.bottom - r.top) / f)); - EndPaint(_hwnd, ref ps); - } + Paint?.Invoke(default); } + ValidateRect(hWnd, IntPtr.Zero); + return IntPtr.Zero; } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index cb85e14e5a..7079a0120c 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -13,6 +13,7 @@ using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Win32.Input; using Avalonia.Win32.Interop; +using Avalonia.Win32.OpenGl; using static Avalonia.Win32.Interop.UnmanagedMethods; namespace Avalonia.Win32 @@ -105,8 +106,12 @@ namespace Avalonia.Win32 CreateWindow(); _framebuffer = new FramebufferManager(_hwnd); - if (Win32GlManager.EglPlatformInterface != null) - _gl = new EglGlPlatformSurface(Win32GlManager.EglPlatformInterface, this); + var glPlatform = AvaloniaLocator.Current.GetService(); + + if(glPlatform is EglPlatformOpenGlInterface egl) + _gl = new EglGlPlatformSurface(egl, this); + else if (glPlatform is WglPlatformOpenGlInterface wgl) + _gl = new WglGlPlatformSurface(wgl.PrimaryContext, this); Screen = new ScreenImpl(); From d8ae3fda43ab392aa0caf7bd062e5ec3958324a2 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 3 Oct 2020 23:25:17 +0300 Subject: [PATCH 29/71] Finally fixed D2D tests --- .../Media/Imaging/WicRenderTargetBitmapImpl.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs index e8e27f3f5d..1265a7bdf0 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs @@ -5,7 +5,7 @@ using SharpDX.Direct2D1; namespace Avalonia.Direct2D1.Media { - public class WicRenderTargetBitmapImpl : WicBitmapImpl, IRenderTargetBitmapImpl + public class WicRenderTargetBitmapImpl : WicBitmapImpl, IDrawingContextLayerImpl { private readonly WicRenderTarget _renderTarget; @@ -45,5 +45,8 @@ namespace Avalonia.Direct2D1.Media finishedCallback?.Invoke(); }); } + + public void Blit(IDrawingContextImpl context) => throw new NotSupportedException(); + public bool CanBlit => false; } } From abc540b5a6e5e813ed0611916387bed1df7f31ae Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 4 Oct 2020 11:58:49 +0300 Subject: [PATCH 30/71] Disable debug mode for wgl context --- src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs b/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs index 0d387bdb2c..dc3c16b4a3 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/WglDisplay.cs @@ -147,7 +147,7 @@ namespace Avalonia.Win32.OpenGl // core profile WGL_CONTEXT_PROFILE_MASK_ARB, 1, // debug - WGL_CONTEXT_FLAGS_ARB, 1, + // WGL_CONTEXT_FLAGS_ARB, 1, // end 0, 0 }); From 40941043f37d34145576bf405f90ec190b44730a Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Mon, 5 Oct 2020 12:14:10 +0300 Subject: [PATCH 31/71] Revert "add a way to render to canvas with DrawingContext with public api" This reverts commit e893705823862945c4cfa0316e77f8293817d59c. --- .../ExternalCanvasRenderTarget.cs | 32 ------------------- .../Avalonia.Skia/IExternalCanvasSurface.cs | 11 ------- .../Avalonia.Skia/PlatformRenderInterface.cs | 2 -- 3 files changed, 45 deletions(-) delete mode 100644 src/Skia/Avalonia.Skia/ExternalCanvasRenderTarget.cs delete mode 100644 src/Skia/Avalonia.Skia/IExternalCanvasSurface.cs diff --git a/src/Skia/Avalonia.Skia/ExternalCanvasRenderTarget.cs b/src/Skia/Avalonia.Skia/ExternalCanvasRenderTarget.cs deleted file mode 100644 index c9e779797b..0000000000 --- a/src/Skia/Avalonia.Skia/ExternalCanvasRenderTarget.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Avalonia.Platform; -using Avalonia.Rendering; - -namespace Avalonia.Skia -{ - internal class ExternalCanvasRenderTarget : IRenderTarget - { - private IExternalCanvasSurface _surface; - - public ExternalCanvasRenderTarget(IExternalCanvasSurface canvas) - { - _surface = canvas; - } - - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) - { - var createInfo = new DrawingContextImpl.CreateInfo - { - Canvas = _surface.Canvas, - Dpi = _surface.Dpi, - VisualBrushRenderer = visualBrushRenderer, - DisableTextLcdRendering = true, - }; - - return new DrawingContextImpl(createInfo); - } - - public void Dispose() - { - } - } -} diff --git a/src/Skia/Avalonia.Skia/IExternalCanvasSurface.cs b/src/Skia/Avalonia.Skia/IExternalCanvasSurface.cs deleted file mode 100644 index 38347f2caa..0000000000 --- a/src/Skia/Avalonia.Skia/IExternalCanvasSurface.cs +++ /dev/null @@ -1,11 +0,0 @@ -using SkiaSharp; - -namespace Avalonia.Skia -{ - public interface IExternalCanvasSurface - { - SKCanvas Canvas { get; } - - Vector Dpi { get; } - } -} diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 397d358ba7..b9c1cbc673 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -145,8 +145,6 @@ namespace Avalonia.Skia { if (surface is IFramebufferPlatformSurface framebufferSurface) return new FramebufferRenderTarget(framebufferSurface); - if (surface is IExternalCanvasSurface canvas) - return new ExternalCanvasRenderTarget(canvas); } throw new NotSupportedException( From 9c1e31aeced7791e6a7aba251dbc9ad9ff2cf076 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Mon, 5 Oct 2020 12:22:31 +0300 Subject: [PATCH 32/71] add public api to create skia drawing context from canvas --- .../Helpers/DrawingContextHelper.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs diff --git a/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs new file mode 100644 index 0000000000..72438609d5 --- /dev/null +++ b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs @@ -0,0 +1,31 @@ +using Avalonia.Platform; +using Avalonia.Rendering; +using SkiaSharp; + +namespace Avalonia.Skia.Helpers +{ + public class DrawingContextHelper + { + /// + /// Wrap Skia canvas in drawing context so we can use Avalonia api to render to external skia canvas + /// this is useful in scenarios where canvas is not controlled by application, but received from another non avalonia api + /// like: SKCanvas canvas = SKDocument.BeginPage(...); + /// + /// + /// + /// + /// DrawingContext + public static IDrawingContextImpl WrapSkiaCanvas(SKCanvas canvas, Vector dpi, IVisualBrushRenderer visualBrushRenderer = null) + { + var createInfo = new DrawingContextImpl.CreateInfo + { + Canvas = canvas, + Dpi = dpi, + VisualBrushRenderer = visualBrushRenderer, + DisableTextLcdRendering = true, + }; + + return new DrawingContextImpl(createInfo); + } + } +} From 773055c78bb0bb19d0bb15ef330e51895ce3af78 Mon Sep 17 00:00:00 2001 From: "Artyom V. Gorchakov" Date: Mon, 5 Oct 2020 13:25:31 +0300 Subject: [PATCH 33/71] Transitions -> PageTransitions --- src/Avalonia.Visuals/Animation/CompositePageTransition.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Animation/CompositePageTransition.cs b/src/Avalonia.Visuals/Animation/CompositePageTransition.cs index 46bbb0bed2..9489914c97 100644 --- a/src/Avalonia.Visuals/Animation/CompositePageTransition.cs +++ b/src/Avalonia.Visuals/Animation/CompositePageTransition.cs @@ -33,7 +33,7 @@ namespace Avalonia.Animation /// Gets or sets the transitions to be executed. Can be defined from XAML. /// [Content] - public List Transitions { get; set; } = new List(); + public List PageTransitions { get; set; } = new List(); /// /// Starts the animation. @@ -52,7 +52,7 @@ namespace Avalonia.Animation /// public Task Start(Visual from, Visual to, bool forward) { - var transitionTasks = Transitions + var transitionTasks = PageTransitions .Select(transition => transition.Start(from, to, forward)) .ToList(); return Task.WhenAll(transitionTasks); From 07b48075cec97c0c40fc05d326776f869d6dc885 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Mon, 5 Oct 2020 20:11:32 +0300 Subject: [PATCH 34/71] add failing test for #4806 (skip it for now as it's deadlock) --- .../Media/TextFormatting/TextLayoutTests.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index f3e1c37705..4785f84133 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -575,6 +575,24 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact(Skip = "Infinity loop issue #4806")] + public void Should_Wrap_Min_OneCharacter_EveryLine() + { + using (Start()) + { + var layout = new TextLayout( + s_singleLineText, + Typeface.Default, + 12, + Brushes.Black, + textWrapping: TextWrapping.Wrap, + maxWidth: 3); + + //every character should be new line as there not enough space for even one character + Assert.Equal(s_singleLineText.Length, layout.TextLines.Count); + } + } + private static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface From 0f81b0fc9302e846d59845787e0bb23833ece5e1 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Mon, 5 Oct 2020 20:14:55 +0300 Subject: [PATCH 35/71] add failing test for formatter for #4806 --- .../TextFormatting/TextFormatterTests.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index adcc79e029..7f9713930a 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -293,6 +293,34 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Wrap_Should_Not_Produce_Empty_Lines() + { + using (Start()) + { + const string text = "012345"; + + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + var paragraphProperties = new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap); + var textSource = new SingleBufferTextSource(text, defaultProperties); + var formatter = new TextFormatterImpl(); + + var textSourceIndex = 0; + + while (textSourceIndex < text.Length) + { + var textLine = + formatter.FormatLine(textSource, textSourceIndex, 3, paragraphProperties); + + Assert.NotEqual(0, textLine.TextRange.Length); + + textSourceIndex += textLine.TextRange.Length; + } + + Assert.Equal(text.Length, textSourceIndex); + } + } + public static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface From 37c2d0afb9b4b878aa2383b0c3f5d6ccdc56ed4d Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Mon, 5 Oct 2020 20:15:27 +0300 Subject: [PATCH 36/71] possible fix for #4806 for text formatting with wrap --- .../Media/TextFormatting/TextFormatterImpl.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index 3e85f0f6f0..7e31d7e6ee 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -391,6 +391,10 @@ namespace Avalonia.Media.TextFormatting measuredLength = currentBreakPosition + lineBreaker.Current.PositionWrap; } } + else if (measuredLength == 0 && currentLength == 0) + { + measuredLength = 1; + } } currentLength += measuredLength; From 14b26187987493b5ac048dac826e5453fcd79d9a Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Mon, 5 Oct 2020 20:17:37 +0300 Subject: [PATCH 37/71] enable back the test for text Wrapping --- .../Media/TextFormatting/TextLayoutTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 4785f84133..26e8ce4797 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -575,7 +575,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } - [Fact(Skip = "Infinity loop issue #4806")] + [Fact] public void Should_Wrap_Min_OneCharacter_EveryLine() { using (Start()) From c941a3d1b99839ad517178f6e88235d2ec4d0cd3 Mon Sep 17 00:00:00 2001 From: amwx Date: Mon, 5 Oct 2020 15:18:26 -0500 Subject: [PATCH 38/71] Return focus to PlacementTarget on close --- src/Avalonia.Controls/Primitives/Popup.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 1e5e80d144..69fa2dc709 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -586,6 +586,19 @@ namespace Avalonia.Controls.Primitives } Closed?.Invoke(this, EventArgs.Empty); + + if(PlacementTarget != null) + { + FocusManager.Instance?.Focus(PlacementTarget); + } + else + { + var anc = this.GetLogicalAncestors().OfType().FirstOrDefault(); + if (anc != null) + { + FocusManager.Instance?.Focus(anc); + } + } } private void ListenForNonClientClick(RawInputEventArgs e) From cd6fa1bf08db5b9a6a7742572121f675e0fa4145 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 6 Oct 2020 11:29:15 +0300 Subject: [PATCH 39/71] [Win32] Drop mouse capture on WM_CANCELMODE --- src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 25a34561fc..a03e1ffc22 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -200,6 +200,10 @@ namespace Avalonia.Win32 DipFromLParam(lParam), GetMouseModifiers(wParam)); break; } + // Mouse capture is lost + case WindowsMessage.WM_CANCELMODE: + _mouseDevice.Capture(null); + break; case WindowsMessage.WM_MOUSEMOVE: { From 103a72cfd3d24f0be6eed8fe705c3d772a65fbeb Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 5 Oct 2020 15:27:26 +0300 Subject: [PATCH 40/71] Removed mouse capture shenanigans from the calendar --- .../Calendar/CalendarItem.cs | 79 +------------------ 1 file changed, 4 insertions(+), 75 deletions(-) diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index e9ea942142..e830717b95 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -36,11 +36,7 @@ namespace Avalonia.Controls.Primitives private Button _headerButton; private Button _nextButton; private Button _previousButton; - private Grid _monthView; - private Grid _yearView; private ITemplate _dayTitleTemplate; - private CalendarButton _lastCalendarButton; - private CalendarDayButton _lastCalendarDayButton; private DateTime _currentMonth; private bool _isMouseLeftButtonDown = false; @@ -160,38 +156,12 @@ namespace Avalonia.Controls.Primitives /// /// Gets the Grid that hosts the content when in month mode. /// - internal Grid MonthView - { - get { return _monthView; } - private set - { - if (_monthView != null) - _monthView.PointerLeave -= MonthView_MouseLeave; - - _monthView = value; - - if (_monthView != null) - _monthView.PointerLeave += MonthView_MouseLeave; - } - } + internal Grid MonthView { get; set; } /// /// Gets the Grid that hosts the content when in year or decade mode. /// - internal Grid YearView - { - get { return _yearView; } - private set - { - if (_yearView != null) - _yearView.PointerLeave -= YearView_MouseLeave; - - _yearView = value; - - if (_yearView != null) - _yearView.PointerLeave += YearView_MouseLeave; - } - } - + internal Grid YearView { get; set; } + private void PopulateGrids() { if (MonthView != null) @@ -226,7 +196,6 @@ namespace Avalonia.Controls.Primitives cell.CalendarDayButtonMouseDown += Cell_MouseLeftButtonDown; cell.CalendarDayButtonMouseUp += Cell_MouseLeftButtonUp; cell.PointerEnter += Cell_MouseEnter; - cell.PointerLeave += Cell_MouseLeave; cell.Click += Cell_Click; children.Add(cell); } @@ -256,7 +225,6 @@ namespace Avalonia.Controls.Primitives month.CalendarLeftMouseButtonDown += Month_CalendarButtonMouseDown; month.CalendarLeftMouseButtonUp += Month_CalendarButtonMouseUp; month.PointerEnter += Month_MouseEnter; - month.PointerLeave += Month_MouseLeave; children.Add(month); } } @@ -937,17 +905,7 @@ namespace Avalonia.Controls.Primitives } } } - internal void Cell_MouseLeave(object sender, PointerEventArgs e) - { - if (_isMouseLeftButtonDown) - { - CalendarDayButton b = (CalendarDayButton)sender; - // The button is in Pressed state. Change the state to normal. - if (e.Pointer.Captured == b) - e.Pointer.Capture(null); - _lastCalendarDayButton = b; - } - } + internal void Cell_MouseLeftButtonDown(object sender, PointerPressedEventArgs e) { if (Owner != null) @@ -1207,35 +1165,6 @@ namespace Avalonia.Controls.Primitives } } - private void Month_MouseLeave(object sender, PointerEventArgs e) - { - if (_isMouseLeftButtonDownYearView) - { - CalendarButton b = (CalendarButton)sender; - // The button is in Pressed state. Change the state to normal. - if (e.Pointer.Captured == b) - e.Pointer.Capture(null); - //b.ReleaseMouseCapture(); - - _lastCalendarButton = b; - } - } - private void MonthView_MouseLeave(object sender, PointerEventArgs e) - { - if (_lastCalendarDayButton != null) - { - e.Pointer.Capture(_lastCalendarDayButton); - } - } - - private void YearView_MouseLeave(object sender, PointerEventArgs e) - { - if (_lastCalendarButton != null) - { - e.Pointer.Capture(_lastCalendarButton); - } - } - internal void UpdateDisabled(bool isEnabled) { PseudoClasses.Set(":calendardisabled", !isEnabled); From 4a0f517ecc2632d6f9768de4577d3e374c478b5f Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 6 Oct 2020 16:16:39 +0300 Subject: [PATCH 41/71] Revert "possible fix for #4806 for text formatting with wrap" This reverts commit 37c2d0afb9b4b878aa2383b0c3f5d6ccdc56ed4d. --- .../Media/TextFormatting/TextFormatterImpl.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index 7e31d7e6ee..3e85f0f6f0 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -391,10 +391,6 @@ namespace Avalonia.Media.TextFormatting measuredLength = currentBreakPosition + lineBreaker.Current.PositionWrap; } } - else if (measuredLength == 0 && currentLength == 0) - { - measuredLength = 1; - } } currentLength += measuredLength; From c2d58f5a0b1402741a23552c6e9a0da467ca434e Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 6 Oct 2020 16:26:30 +0300 Subject: [PATCH 42/71] fix #4806 ensure at least one character is returned on the first text run measure --- .../Media/TextFormatting/TextFormatterImpl.cs | 12 ++++++++++-- .../Media/TextFormatting/TextLineImpl.cs | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index 3e85f0f6f0..e65f96bf61 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -47,8 +47,9 @@ namespace Avalonia.Media.TextFormatting /// /// The text run. /// The available width. + /// Index of the textCharacters parent TextRun /// - internal static int MeasureCharacters(ShapedTextCharacters textCharacters, double availableWidth) + internal static int MeasureCharacters(ShapedTextCharacters textCharacters, double availableWidth, int textRunIndex) { var glyphRun = textCharacters.GlyphRun; @@ -73,6 +74,13 @@ namespace Avalonia.Media.TextFormatting if (currentWidth + advance > availableWidth) { + if(glyphCount == 0 && textRunIndex == 0) + { + //we need to return at least one characted on the first run + //or we risk to get a infinity loop when width is less than one character width + //issue #4806 + glyphCount = 1; + } break; } @@ -350,7 +358,7 @@ namespace Avalonia.Media.TextFormatting if (currentWidth + currentRun.Size.Width > availableWidth) { - var measuredLength = MeasureCharacters(currentRun, paragraphWidth - currentWidth); + var measuredLength = MeasureCharacters(currentRun, paragraphWidth - currentWidth, runIndex); var breakFound = false; diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs index f5e87d097b..aa7759fc16 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs @@ -75,7 +75,7 @@ namespace Avalonia.Media.TextFormatting if (currentWidth > availableWidth) { - var measuredLength = TextFormatterImpl.MeasureCharacters(currentRun, availableWidth); + var measuredLength = TextFormatterImpl.MeasureCharacters(currentRun, availableWidth, runIndex); var currentBreakPosition = 0; From a19d327629ab93ddbacb64ab191af9e8ecef745c Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 6 Oct 2020 23:03:54 +0300 Subject: [PATCH 43/71] Revert "fix #4806 ensure at least one character is returned on the first text run measure" This reverts commit c2d58f5a0b1402741a23552c6e9a0da467ca434e. --- .../Media/TextFormatting/TextFormatterImpl.cs | 12 ++---------- .../Media/TextFormatting/TextLineImpl.cs | 2 +- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index e65f96bf61..3e85f0f6f0 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -47,9 +47,8 @@ namespace Avalonia.Media.TextFormatting /// /// The text run. /// The available width. - /// Index of the textCharacters parent TextRun /// - internal static int MeasureCharacters(ShapedTextCharacters textCharacters, double availableWidth, int textRunIndex) + internal static int MeasureCharacters(ShapedTextCharacters textCharacters, double availableWidth) { var glyphRun = textCharacters.GlyphRun; @@ -74,13 +73,6 @@ namespace Avalonia.Media.TextFormatting if (currentWidth + advance > availableWidth) { - if(glyphCount == 0 && textRunIndex == 0) - { - //we need to return at least one characted on the first run - //or we risk to get a infinity loop when width is less than one character width - //issue #4806 - glyphCount = 1; - } break; } @@ -358,7 +350,7 @@ namespace Avalonia.Media.TextFormatting if (currentWidth + currentRun.Size.Width > availableWidth) { - var measuredLength = MeasureCharacters(currentRun, paragraphWidth - currentWidth, runIndex); + var measuredLength = MeasureCharacters(currentRun, paragraphWidth - currentWidth); var breakFound = false; diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs index aa7759fc16..f5e87d097b 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs @@ -75,7 +75,7 @@ namespace Avalonia.Media.TextFormatting if (currentWidth > availableWidth) { - var measuredLength = TextFormatterImpl.MeasureCharacters(currentRun, availableWidth, runIndex); + var measuredLength = TextFormatterImpl.MeasureCharacters(currentRun, availableWidth); var currentBreakPosition = 0; From 40d74cf682b3ad6e88ff99ace2fbbae2ac2e3fe3 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Tue, 6 Oct 2020 19:00:53 +0200 Subject: [PATCH 44/71] Fix baseline alignment of multiple runs Make sure we always wrap at least one character --- .../ControlCatalog/Pages/TextBlockPage.xaml | 4 +- src/Avalonia.Visuals/Media/GlyphRun.cs | 17 +---- .../TextFormatting/ShapedTextCharacters.cs | 2 +- .../Media/TextFormatting/TextFormatterImpl.cs | 67 +++++++++++++------ .../Media/TextFormatting/TextLineImpl.cs | 44 ++++++------ 5 files changed, 74 insertions(+), 60 deletions(-) diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml index 4a1c196917..d4f72f161a 100644 --- a/samples/ControlCatalog/Pages/TextBlockPage.xaml +++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml @@ -18,8 +18,8 @@ - - + + diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs index 14ab083b4f..155339b985 100644 --- a/src/Avalonia.Visuals/Media/GlyphRun.cs +++ b/src/Avalonia.Visuals/Media/GlyphRun.cs @@ -18,7 +18,7 @@ namespace Avalonia.Media private double _fontRenderingEmSize; private Size? _size; private int _biDiLevel; - private Point? _baselineOrigin; + private Point _baselineOrigin; private ReadOnlySlice _glyphIndices; private ReadOnlySlice _glyphAdvances; @@ -97,9 +97,7 @@ namespace Avalonia.Media { get { - _baselineOrigin ??= CalculateBaselineOrigin(); - - return _baselineOrigin.Value; + return _baselineOrigin; } set => Set(ref _baselineOrigin, value); } @@ -540,15 +538,6 @@ namespace Avalonia.Media return GlyphAdvances[index]; } - /// - /// Calculates the default baseline origin of the . - /// - /// The baseline origin. - private Point CalculateBaselineOrigin() - { - return new Point(0, -GlyphTypeface.Ascent * Scale); - } - /// /// Calculates the size of the . /// @@ -611,8 +600,6 @@ namespace Avalonia.Media throw new InvalidOperationException(); } - _baselineOrigin = new Point(0, -GlyphTypeface.Ascent * Scale); - var platformRenderInterface = AvaloniaLocator.Current.GetService(); _glyphRunImpl = platformRenderInterface.CreateGlyphRun(this, out var width); diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs index 09ecc0a026..9f6f2b2f43 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs @@ -52,7 +52,7 @@ namespace Avalonia.Media.TextFormatting return; } - if (Properties.Typeface == null) + if (Properties.Typeface == default) { return; } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index 3e85f0f6f0..4a7282af27 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -43,18 +43,23 @@ namespace Avalonia.Media.TextFormatting } /// - /// Measures the number of characters that fits into available width. + /// Measures the number of characters that fit into available width. /// /// The text run. /// The available width. - /// - internal static int MeasureCharacters(ShapedTextCharacters textCharacters, double availableWidth) + /// The count of fitting characters. + /// + /// true if characters fit into the available width; otherwise, false. + /// + internal static bool TryMeasureCharacters(ShapedTextCharacters textCharacters, double availableWidth, out int count) { var glyphRun = textCharacters.GlyphRun; if (glyphRun.Size.Width < availableWidth) { - return glyphRun.Characters.Length; + count = glyphRun.Characters.Length; + + return true; } var glyphCount = 0; @@ -96,21 +101,34 @@ namespace Avalonia.Media.TextFormatting } } + if (glyphCount == 0) + { + count = 0; + + return false; + } + if (glyphCount == glyphRun.GlyphIndices.Length) { - return glyphRun.Characters.Length; + count = glyphRun.Characters.Length; + + return true; } if (glyphRun.GlyphClusters.IsEmpty) { - return glyphCount; + count = glyphCount; + + return true; } var firstCluster = glyphRun.GlyphClusters[0]; var lastCluster = glyphRun.GlyphClusters[glyphCount]; - return lastCluster - firstCluster; + count = lastCluster - firstCluster; + + return count > 0; } /// @@ -350,29 +368,38 @@ namespace Avalonia.Media.TextFormatting if (currentWidth + currentRun.Size.Width > availableWidth) { - var measuredLength = MeasureCharacters(currentRun, paragraphWidth - currentWidth); - var breakFound = false; var currentBreakPosition = 0; - if (measuredLength < currentRun.Text.Length) + if (TryMeasureCharacters(currentRun, paragraphWidth - currentWidth, out var measuredLength)) { - var lineBreaker = new LineBreakEnumerator(currentRun.Text); - - while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) + if (measuredLength < currentRun.Text.Length) { - var nextBreakPosition = lineBreaker.Current.PositionWrap; + var lineBreaker = new LineBreakEnumerator(currentRun.Text); - if (nextBreakPosition == 0 || nextBreakPosition > measuredLength) + while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) { - break; - } + var nextBreakPosition = lineBreaker.Current.PositionWrap; + + if (nextBreakPosition == 0 || nextBreakPosition > measuredLength) + { + break; + } - breakFound = lineBreaker.Current.Required || - lineBreaker.Current.PositionWrap != currentRun.Text.Length; + breakFound = lineBreaker.Current.Required || + lineBreaker.Current.PositionWrap != currentRun.Text.Length; - currentBreakPosition = nextBreakPosition; + currentBreakPosition = nextBreakPosition; + } + } + } + else + { + // Make sure we wrap at least one character. + if (currentLength == 0) + { + measuredLength = 1; } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs index f5e87d097b..d13b4836ea 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs @@ -39,7 +39,9 @@ namespace Avalonia.Media.TextFormatting foreach (var textRun in _textRuns) { - using (drawingContext.PushPostTransform(Matrix.CreateTranslation(currentX, 0))) + var offsetY = LineMetrics.TextBaseline; + + using (drawingContext.PushPostTransform(Matrix.CreateTranslation(currentX, offsetY))) { textRun.Draw(drawingContext); } @@ -75,37 +77,35 @@ namespace Avalonia.Media.TextFormatting if (currentWidth > availableWidth) { - var measuredLength = TextFormatterImpl.MeasureCharacters(currentRun, availableWidth); - - var currentBreakPosition = 0; - - if (measuredLength < textRange.End) + if (TextFormatterImpl.TryMeasureCharacters(currentRun, availableWidth, out var measuredLength)) { - var lineBreaker = new LineBreakEnumerator(currentRun.Text); - - while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) + if (collapsingProperties.Style == TextCollapsingStyle.TrailingWord && measuredLength < textRange.End) { - var nextBreakPosition = lineBreaker.Current.PositionWrap; + var currentBreakPosition = 0; - if (nextBreakPosition == 0) - { - break; - } + var lineBreaker = new LineBreakEnumerator(currentRun.Text); - if (nextBreakPosition > measuredLength) + while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) { - break; + var nextBreakPosition = lineBreaker.Current.PositionWrap; + + if (nextBreakPosition == 0) + { + break; + } + + if (nextBreakPosition > measuredLength) + { + break; + } + + currentBreakPosition = nextBreakPosition; } - currentBreakPosition = nextBreakPosition; + measuredLength = currentBreakPosition; } } - if (collapsingProperties.Style == TextCollapsingStyle.TrailingWord) - { - measuredLength = currentBreakPosition; - } - collapsedLength += measuredLength; var splitResult = TextFormatterImpl.SplitTextRuns(_textRuns, collapsedLength); From 273f296b47d0a0e8dd39a6ccf922fa8e075f4dbd Mon Sep 17 00:00:00 2001 From: amwx Date: Tue, 6 Oct 2020 15:39:21 -0500 Subject: [PATCH 45/71] Small fix --- src/Avalonia.Controls/Primitives/Popup.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 69fa2dc709..5d911d7727 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -358,7 +358,7 @@ namespace Avalonia.Controls.Primitives return; } - var placementTarget = PlacementTarget ?? this.GetLogicalAncestors().OfType().FirstOrDefault(); + var placementTarget = PlacementTarget ?? this.FindLogicalAncestorOfType(); if (placementTarget == null) { @@ -587,13 +587,13 @@ namespace Avalonia.Controls.Primitives Closed?.Invoke(this, EventArgs.Empty); - if(PlacementTarget != null) + if (PlacementTarget != null) { FocusManager.Instance?.Focus(PlacementTarget); } else { - var anc = this.GetLogicalAncestors().OfType().FirstOrDefault(); + var anc = this.FindLogicalAncestorOfType(); if (anc != null) { FocusManager.Instance?.Focus(anc); From 047d734e94adfd22e9df45e5cf9e20a358b10965 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 6 Oct 2020 21:16:41 -0400 Subject: [PATCH 46/71] Add :empty pseudoclass for ContentPresenter --- .../Presenters/ContentPresenter.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 8837901816..3fd927afa3 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -1,9 +1,9 @@ using System; + +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; -using Avalonia.Data; -using Avalonia.Input; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Media; @@ -14,6 +14,7 @@ namespace Avalonia.Controls.Presenters /// /// Presents a single item of data inside a template. /// + [PseudoClasses(":empty")] public class ContentPresenter : Control, IContentPresenter { /// @@ -102,6 +103,11 @@ namespace Avalonia.Controls.Presenters TemplatedParentProperty.Changed.AddClassHandler((x, e) => x.TemplatedParentChanged(e)); } + public ContentPresenter() + { + UpdatePseudoClasses(); + } + /// /// Gets or sets a brush with which to paint the background. /// @@ -424,9 +430,15 @@ namespace Avalonia.Controls.Presenters _recyclingDataTemplate = null; } + UpdatePseudoClasses(); InvalidateMeasure(); } + private void UpdatePseudoClasses() + { + PseudoClasses.Set(":empty", Content is null); + } + private double GetLayoutScale() { var result = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1.0; From 8ef4b2f7922611f6568a0fd1aaa5c91433365b22 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 6 Oct 2020 21:19:13 -0400 Subject: [PATCH 47/71] ToggleSwitch: add margin only when Content header is set --- src/Avalonia.Themes.Default/ToggleSwitch.xaml | 8 +++++++- src/Avalonia.Themes.Fluent/ToggleSwitch.xaml | 10 ++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Themes.Default/ToggleSwitch.xaml b/src/Avalonia.Themes.Default/ToggleSwitch.xaml index 9ce4da0873..9d1c024eb9 100644 --- a/src/Avalonia.Themes.Default/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Default/ToggleSwitch.xaml @@ -87,7 +87,6 @@ Grid.Row="0" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" - Margin="{DynamicResource ToggleSwitchTopHeaderMargin}" VerticalAlignment="Top"/> + + + + + + - - - - - - + + + + + + + From 7f7db22004fd45f136bf7307c93ce2d52f0542d3 Mon Sep 17 00:00:00 2001 From: amwx Date: Tue, 6 Oct 2020 22:23:03 -0500 Subject: [PATCH 53/71] Only move focus if Popup had it --- src/Avalonia.Controls/Primitives/Popup.cs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 5d911d7727..becb489557 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -587,16 +587,23 @@ namespace Avalonia.Controls.Primitives Closed?.Invoke(this, EventArgs.Empty); - if (PlacementTarget != null) - { - FocusManager.Instance?.Focus(PlacementTarget); - } - else + var focusCheck = FocusManager.Instance?.Current; + + // Focus is set to null as part of popup closing, so we only want to + // set focus to PlacementTarget if this is the case + if (focusCheck == null) { - var anc = this.FindLogicalAncestorOfType(); - if (anc != null) + if (PlacementTarget != null) { - FocusManager.Instance?.Focus(anc); + FocusManager.Instance?.Focus(PlacementTarget); + } + else + { + var anc = this.FindLogicalAncestorOfType(); + if (anc != null) + { + FocusManager.Instance?.Focus(anc); + } } } } From b58691957f9cd974743b9d2c343a7896a3a264e9 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 7 Oct 2020 03:35:23 -0400 Subject: [PATCH 54/71] Fluent non-transparent notification background --- .../Accents/FluentControlResourcesDark.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index 4d21068492..abc02a6371 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -439,7 +439,7 @@ - + From feed5103abd8034568ece7d12c59ba60ef2455b3 Mon Sep 17 00:00:00 2001 From: Yoh Deadfall Date: Wed, 7 Oct 2020 12:45:44 +0300 Subject: [PATCH 55/71] Removed redundant method --- .../Primitives/SelectingItemsControl.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index e34b3b145f..4317d795f1 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -848,21 +848,6 @@ namespace Avalonia.Controls.Primitives } } - /// - /// Sets an item container's 'selected' class or . - /// - /// The index of the item. - /// Whether the item should be selected or deselected. - private void MarkItemSelected(int index, bool selected) - { - var container = ItemContainerGenerator?.ContainerFromIndex(index); - - if (container != null) - { - MarkContainerSelected(container, selected); - } - } - private void UpdateContainerSelection() { if (Presenter?.Panel is IPanel panel) From 952c53cdff26960392bb5e65b1f07afa060853f1 Mon Sep 17 00:00:00 2001 From: amwx Date: Wed, 7 Oct 2020 23:06:29 -0500 Subject: [PATCH 56/71] Add tests --- .../Primitives/PopupTests.cs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index f032186bcd..9223b08c81 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -485,6 +485,85 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void Closing_Popup_Sets_Focus_On_PlacementTarget() + { + using (CreateServicesWithFocus()) + { + var window = PreparedWindow(); + + var tb = new TextBox(); + var p = new Popup + { + PlacementTarget = window, + Child = tb + }; + ((ISetLogicalParent)p).SetParent(p.PlacementTarget); + window.Show(); + + p.Open(); + + if (p.Host is OverlayPopupHost host) + { + //Need to measure/arrange for visual children to show up + //in OverlayPopupHost + host.Measure(Size.Infinity); + host.Arrange(new Rect(host.DesiredSize)); + } + + tb.Focus(); + + p.Close(); + + var focus = FocusManager.Instance?.Current; + Assert.True(focus == window); + } + } + + [Fact] + public void Prog_Close_Popup_NoLightDismiss_Doesnt_Move_Focus_To_PlacementTarget() + { + using (CreateServicesWithFocus()) + { + var window = PreparedWindow(); + + var windowTB = new TextBox(); + window.Content = windowTB; + + var popupTB = new TextBox(); + var p = new Popup + { + PlacementTarget = window, + IsLightDismissEnabled = false, + Child = popupTB + }; + ((ISetLogicalParent)p).SetParent(p.PlacementTarget); + window.Show(); + + p.Open(); + + if (p.Host is OverlayPopupHost host) + { + //Need to measure/arrange for visual children to show up + //in OverlayPopupHost + host.Measure(Size.Infinity); + host.Arrange(new Rect(host.DesiredSize)); + } + + popupTB.Focus(); + + windowTB.Focus(); + + var focus = FocusManager.Instance?.Current; + + Assert.True(focus == windowTB); + + p.Close(); + + Assert.True(focus == windowTB); + } + } + private IDisposable CreateServices() { return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: From 7ad03026f2793b45d1ef607f84bb42197e8cddbf Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 8 Oct 2020 11:42:36 +0100 Subject: [PATCH 57/71] update apicompat. --- src/Avalonia.Visuals/ApiCompatBaseline.txt | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt index 39cf5ee4dd..7fb44152c9 100644 --- a/src/Avalonia.Visuals/ApiCompatBaseline.txt +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -1,19 +1,37 @@ Compat issues with assembly Avalonia.Visuals: +MembersMustExist : Member 'public void Avalonia.Media.DrawingContext.DrawGlyphRun(Avalonia.Media.IBrush, Avalonia.Media.GlyphRun, Avalonia.Point)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.Media.Typeface Avalonia.Media.FontManager.GetOrAddTypeface(Avalonia.Media.FontFamily, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.Media.Typeface Avalonia.Media.FontManager.MatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Rect Avalonia.Media.GlyphRun.Bounds.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.StyledProperty Avalonia.StyledProperty Avalonia.Media.GlyphRunDrawing.BaselineOriginProperty' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public Avalonia.Point Avalonia.Media.GlyphRunDrawing.BaselineOrigin.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Media.GlyphRunDrawing.BaselineOrigin.set(Avalonia.Point)' does not exist in the implementation but it does exist in the contract. CannotSealType : Type 'Avalonia.Media.Typeface' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. TypeCannotChangeClassification : Type 'Avalonia.Media.Typeface' is a 'struct' in the implementation but is a 'class' in the contract. CannotMakeMemberNonVirtual : Member 'public System.Boolean Avalonia.Media.Typeface.Equals(System.Object)' is non-virtual in the implementation but is virtual in the contract. CannotMakeMemberNonVirtual : Member 'public System.Int32 Avalonia.Media.Typeface.GetHashCode()' is non-virtual in the implementation but is virtual in the contract. TypesMustExist : Type 'Avalonia.Media.Fonts.FontKey' does not exist in the implementation but it does exist in the contract. +CannotAddAbstractMembers : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.DrawableTextRun.Size' is abstract in the implementation but is missing in the contract. +MembersMustExist : Member 'public Avalonia.Rect Avalonia.Media.TextFormatting.DrawableTextRun.Bounds.get()' does not exist in the implementation but it does exist in the contract. +CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext)' is abstract in the implementation but is missing in the contract. +MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.DrawableTextRun.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' does not exist in the implementation but it does exist in the contract. +CannotAddAbstractMembers : Member 'public Avalonia.Size Avalonia.Media.TextFormatting.DrawableTextRun.Size.get()' is abstract in the implementation but is missing in the contract. +MembersMustExist : Member 'public Avalonia.Rect Avalonia.Media.TextFormatting.ShapedTextCharacters.Bounds.get()' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.ShapedTextCharacters.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLayout.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.TextLineBreak' is abstract in the implementation but is missing in the contract. +CannotAddAbstractMembers : Member 'public void Avalonia.Media.TextFormatting.TextLine.Draw(Avalonia.Media.DrawingContext)' is abstract in the implementation but is missing in the contract. +MembersMustExist : Member 'public void Avalonia.Media.TextFormatting.TextLine.Draw(Avalonia.Media.DrawingContext, Avalonia.Point)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.LineBreak.get()' does not exist in the implementation but it does exist in the contract. CannotAddAbstractMembers : Member 'public Avalonia.Media.TextFormatting.TextLineBreak Avalonia.Media.TextFormatting.TextLine.TextLineBreak.get()' is abstract in the implementation but is missing in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IDrawingContextLayerImpl Avalonia.Platform.IDrawingContextImpl.CreateLayer(Avalonia.Size)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IRenderTargetBitmapImpl Avalonia.Platform.IDrawingContextImpl.CreateLayer(Avalonia.Size)' is present in the contract but not in the implementation. MembersMustExist : Member 'public Avalonia.Platform.IRenderTargetBitmapImpl Avalonia.Platform.IDrawingContextImpl.CreateLayer(Avalonia.Size)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.DrawGlyphRun(Avalonia.Media.IBrush, Avalonia.Media.GlyphRun)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.DrawGlyphRun(Avalonia.Media.IBrush, Avalonia.Media.GlyphRun, Avalonia.Point)' is present in the contract but not in the implementation. +MembersMustExist : Member 'public void Avalonia.Platform.IDrawingContextImpl.DrawGlyphRun(Avalonia.Media.IBrush, Avalonia.Media.GlyphRun, Avalonia.Point)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Fonts.FontKey)' is present in the contract but not in the implementation. MembersMustExist : Member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Fonts.FontKey)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IFontManagerImpl.TryMatchCharacter(System.Int32, Avalonia.Media.FontStyle, Avalonia.Media.FontWeight, Avalonia.Media.FontFamily, System.Globalization.CultureInfo, Avalonia.Media.Typeface)' is present in the implementation but not in the contract. MembersMustExist : Member 'public Avalonia.Utilities.IRef Avalonia.Rendering.RenderLayer.Bitmap.get()' does not exist in the implementation but it does exist in the contract. -Total Issues: 17 +Total Issues: 35 From 8f4059654636d52eb5ff3975ebdc5788b3e60652 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 8 Oct 2020 11:59:28 +0100 Subject: [PATCH 58/71] fix nits --- src/Avalonia.Controls/TreeView.cs | 1 - tests/Avalonia.Controls.UnitTests/TreeViewTests.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index f0e00e1d7e..b2bd5ab2e5 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -217,7 +217,6 @@ namespace Avalonia.Controls private void SelectSingleItem(object item) { - var oldValue = SelectedItem; _syncingSelectedItems = true; SelectedItems.Clear(); SelectedItems.Add(item); diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index b90b74d794..9253d8a07f 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1328,7 +1328,6 @@ namespace Avalonia.Controls.UnitTests private class TestDataContext : ReactiveObject { - private int _counter; private string _selectedItem; public TestDataContext() From 53eeb8e57885a795bfba9272d75c25796457f5e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20Tar=C4=B1k=20G=C3=BCnayd=C4=B1n?= Date: Thu, 8 Oct 2020 15:43:03 +0300 Subject: [PATCH 59/71] Fix ApiDiff.props for Turkish locale. --- build/ApiDiff.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/ApiDiff.props b/build/ApiDiff.props index da82fbcc51..3d322f56d5 100644 --- a/build/ApiDiff.props +++ b/build/ApiDiff.props @@ -7,6 +7,6 @@ - + From 7d28b510d7c792a118a30bdc05a3d330774f02c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20So=C3=B3s?= Date: Fri, 9 Oct 2020 11:13:30 +0200 Subject: [PATCH 60/71] Update TextBox.xaml Set CornerRadius via style on TextBox --- src/Avalonia.Themes.Fluent/TextBox.xaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Fluent/TextBox.xaml b/src/Avalonia.Themes.Fluent/TextBox.xaml index 85364bffea..162835ae10 100644 --- a/src/Avalonia.Themes.Fluent/TextBox.xaml +++ b/src/Avalonia.Themes.Fluent/TextBox.xaml @@ -52,7 +52,6 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" - CornerRadius="{DynamicResource ControlCornerRadius}" MinWidth="{TemplateBinding MinWidth}" MinHeight="{TemplateBinding MinHeight}"> @@ -156,6 +155,10 @@ + + From 9eed4c974e98d840b883e3534588c09eaab333d6 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 9 Oct 2020 18:29:02 +0200 Subject: [PATCH 61/71] Make sure a GlyphRun's bounds are properly calculated --- src/Avalonia.Visuals/Media/GlyphRun.cs | 15 +++++++++++++-- .../Media/TextFormatting/TextLineImpl.cs | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs index 155339b985..66a8c1dd0c 100644 --- a/src/Avalonia.Visuals/Media/GlyphRun.cs +++ b/src/Avalonia.Visuals/Media/GlyphRun.cs @@ -18,7 +18,7 @@ namespace Avalonia.Media private double _fontRenderingEmSize; private Size? _size; private int _biDiLevel; - private Point _baselineOrigin; + private Point? _baselineOrigin; private ReadOnlySlice _glyphIndices; private ReadOnlySlice _glyphAdvances; @@ -97,7 +97,9 @@ namespace Avalonia.Media { get { - return _baselineOrigin; + _baselineOrigin ??= CalculateBaselineOrigin(); + + return _baselineOrigin.Value; } set => Set(ref _baselineOrigin, value); } @@ -538,6 +540,15 @@ namespace Avalonia.Media return GlyphAdvances[index]; } + /// + /// Calculates the default baseline origin of the . + /// + /// The baseline origin. + private Point CalculateBaselineOrigin() + { + return new Point(0, -GlyphTypeface.Ascent * Scale); + } + /// /// Calculates the size of the . /// diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs index d13b4836ea..fc98e9f6f8 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs @@ -39,7 +39,7 @@ namespace Avalonia.Media.TextFormatting foreach (var textRun in _textRuns) { - var offsetY = LineMetrics.TextBaseline; + var offsetY = LineMetrics.TextBaseline - textRun.GlyphRun.BaselineOrigin.Y; using (drawingContext.PushPostTransform(Matrix.CreateTranslation(currentX, offsetY))) { From b9ff1b23c3e09acb9161116907cb37a4756e19e9 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Fri, 9 Oct 2020 18:39:09 +0200 Subject: [PATCH 62/71] Make sure we don't crash when RTL text is wrapped --- .../Media/TextFormatting/TextFormatterImpl.cs | 10 +++++++++- src/Skia/Avalonia.Skia/TextShaperImpl.cs | 3 ++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index 4a7282af27..394dae8253 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -126,7 +126,15 @@ namespace Avalonia.Media.TextFormatting var lastCluster = glyphRun.GlyphClusters[glyphCount]; - count = lastCluster - firstCluster; + if (glyphRun.IsLeftToRight) + { + count = lastCluster - firstCluster; + } + else + { + count = firstCluster - lastCluster; + } + return count > 0; } diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index 61075171fe..31724bfee9 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -64,7 +64,8 @@ namespace Avalonia.Skia new ReadOnlySlice(glyphAdvances), new ReadOnlySlice(glyphOffsets), text, - new ReadOnlySlice(clusters)); + new ReadOnlySlice(clusters), + buffer.Direction == Direction.LeftToRight ? 0 : 1); } } From 64c1bf890e04385b601c516132346e4da12e8ef8 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 11 Oct 2020 15:55:21 +0300 Subject: [PATCH 63/71] Fixed DRM output --- src/Avalonia.OpenGL/Egl/EglContext.cs | 3 ++- src/Avalonia.OpenGL/Egl/EglDisplay.cs | 22 ++++++++++++------- .../Output/DrmOutput.cs | 12 +--------- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.OpenGL/Egl/EglContext.cs b/src/Avalonia.OpenGL/Egl/EglContext.cs index 5365354418..249b4d547f 100644 --- a/src/Avalonia.OpenGL/Egl/EglContext.cs +++ b/src/Avalonia.OpenGL/Egl/EglContext.cs @@ -73,7 +73,8 @@ namespace Avalonia.OpenGL.Egl var old = new RestoreContext(_egl, _disp.Handle, _lock); var surf = surface ?? OffscreenSurface; _egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); - if (!_egl.MakeCurrent(_disp.Handle, surf.DangerousGetHandle(), surf.DangerousGetHandle(), Context)) + if (!_egl.MakeCurrent(_disp.Handle, surf?.DangerousGetHandle() ?? IntPtr.Zero, + surf?.DangerousGetHandle() ?? IntPtr.Zero, Context)) throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl); success = true; return old; diff --git a/src/Avalonia.OpenGL/Egl/EglDisplay.cs b/src/Avalonia.OpenGL/Egl/EglDisplay.cs index fd3de854f5..623364866b 100644 --- a/src/Avalonia.OpenGL/Egl/EglDisplay.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplay.cs @@ -158,15 +158,21 @@ namespace Avalonia.OpenGL.Egl var ctx = _egl.CreateContext(_display, _config, shareCtx?.Context ?? IntPtr.Zero, _contextAttributes); if (ctx == IntPtr.Zero) throw OpenGlException.GetFormattedException("eglCreateContext", _egl); - var surf = _egl.CreatePBufferSurface(_display, _config, new[] + + var extensions = _egl.QueryString(Handle, EGL_EXTENSIONS); + + IntPtr surf = IntPtr.Zero; + if (extensions?.Contains("EGL_KHR_surfaceless_context") != true) { - EGL_WIDTH, 1, - EGL_HEIGHT, 1, - EGL_NONE - }); - if (surf == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglCreatePBufferSurface", _egl); - var rv = new EglContext(this, _egl, shareCtx, ctx, context => new EglSurface(this, context, surf), + surf = _egl.CreatePBufferSurface(_display, _config, + new[] { EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE }); + if (surf == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreatePBufferSurface", _egl); + } + + var rv = new EglContext(this, _egl, shareCtx, ctx, + context => + surf == IntPtr.Zero ? null : new EglSurface(this, context, surf), _version, _sampleCount, _stencilSize); return rv; } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index 72eed9e543..dc44d2d55f 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -141,17 +141,7 @@ namespace Avalonia.LinuxFramebuffer.Output _platformGl = new EglPlatformOpenGlInterface(_eglDisplay); _eglSurface = _platformGl.CreateWindowSurface(_gbmTargetSurface); - - EglContext CreateContext(EglContext share) - { - var offSurf = gbm_surface_create(device, 1, 1, GbmColorFormats.GBM_FORMAT_XRGB8888, - GbmBoFlags.GBM_BO_USE_RENDERING); - if (offSurf == null) - throw new InvalidOperationException("Unable to create 1x1 sized GBM surface"); - return _eglDisplay.CreateContext(share, _platformGl.CreateWindowSurface(offSurf)); - } - - _deferredContext = CreateContext(null); + _deferredContext = _platformGl.PrimaryEglContext; using (_deferredContext.MakeCurrent(_eglSurface)) { From 31d8ada74b25d85374d439a9222a0646962df16a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20So=C3=B3s?= Date: Mon, 12 Oct 2020 11:21:02 +0200 Subject: [PATCH 64/71] Update ComboBox.xaml --- src/Avalonia.Themes.Fluent/ComboBox.xaml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/ComboBox.xaml b/src/Avalonia.Themes.Fluent/ComboBox.xaml index f77f1b582b..8155264f18 100644 --- a/src/Avalonia.Themes.Fluent/ComboBox.xaml +++ b/src/Avalonia.Themes.Fluent/ComboBox.xaml @@ -61,7 +61,6 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" - CornerRadius="{DynamicResource ControlCornerRadius}" MinWidth="{DynamicResource ComboBoxThemeMinWidth}" /> + BorderThickness="{TemplateBinding BorderThickness}" /> + + + + From 09fd6650ac13a728210fc35b71664265c6b30fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20So=C3=B3s?= Date: Mon, 12 Oct 2020 11:32:04 +0200 Subject: [PATCH 65/71] Update Button.xaml CornerRadius via style --- src/Avalonia.Themes.Fluent/Button.xaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Fluent/Button.xaml b/src/Avalonia.Themes.Fluent/Button.xaml index 8522c933ae..573b9f337f 100644 --- a/src/Avalonia.Themes.Fluent/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Button.xaml @@ -32,7 +32,6 @@ BorderThickness="{TemplateBinding BorderThickness}" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" - CornerRadius="{DynamicResource ControlCornerRadius}" Padding="{TemplateBinding Padding}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" /> @@ -95,4 +94,8 @@ + + From 4aa9264daabf5e81b05b05decce7a7dc5e807a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20So=C3=B3s?= Date: Mon, 12 Oct 2020 11:36:26 +0200 Subject: [PATCH 66/71] CheckBox CornerRadius via style --- src/Avalonia.Themes.Fluent/CheckBox.xaml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/CheckBox.xaml b/src/Avalonia.Themes.Fluent/CheckBox.xaml index 678ae5c5a3..83d2779872 100644 --- a/src/Avalonia.Themes.Fluent/CheckBox.xaml +++ b/src/Avalonia.Themes.Fluent/CheckBox.xaml @@ -22,16 +22,14 @@ Grid.ColumnSpan="2" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" - BorderThickness="{TemplateBinding BorderThickness}" - CornerRadius="{DynamicResource ControlCornerRadius}" /> + BorderThickness="{TemplateBinding BorderThickness}" /> + Width="20" /> @@ -52,6 +50,14 @@ + + + + + + + + + From 1d2cf0280e71356ff4ed1176bff694ceb7851e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20So=C3=B3s?= Date: Mon, 12 Oct 2020 11:48:35 +0200 Subject: [PATCH 70/71] TabItem CornerRadius via style --- src/Avalonia.Themes.Fluent/TabItem.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Fluent/TabItem.xaml b/src/Avalonia.Themes.Fluent/TabItem.xaml index 2b0a0c1ea0..ca8d4dfeb5 100644 --- a/src/Avalonia.Themes.Fluent/TabItem.xaml +++ b/src/Avalonia.Themes.Fluent/TabItem.xaml @@ -39,7 +39,6 @@ TextBlock.FontSize="{TemplateBinding FontSize}" TextBlock.FontWeight="{TemplateBinding FontWeight}" /> @@ -53,6 +52,7 @@ From 0329cf5d9f2177a7c23b4164a24958e02cb81982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20So=C3=B3s?= Date: Mon, 12 Oct 2020 11:55:49 +0200 Subject: [PATCH 71/71] format --- src/Avalonia.Themes.Fluent/TabItem.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Fluent/TabItem.xaml b/src/Avalonia.Themes.Fluent/TabItem.xaml index ca8d4dfeb5..1c9574f169 100644 --- a/src/Avalonia.Themes.Fluent/TabItem.xaml +++ b/src/Avalonia.Themes.Fluent/TabItem.xaml @@ -52,7 +52,7 @@

4gnSxr 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 literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedBlue_Offset_Outside.expected.png b/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedBlue_Offset_Outside.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..667aa9151e8d97b711ce1dad1f05c7f5f6f83b89 GIT binary patch literal 9840 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 literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedGreenBlue_Offset_Inside.expected.png b/tests/TestFiles/Skia/Media/RadialGradientBrush/RadialGradientBrush_RedGreenBlue_Offset_Inside.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..1d660f79d1aa8d0f6bf42fd80bc8546e03a43327 GIT binary patch literal 15084 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�#q)yoM}zR(et5z0jK7Mc$&~6V zXPk+(%IgvVLDf99Xu! zqqk?Zx8OH@4^-!4+)pma>o@CiUwKX5k z)bMpsLn^sZD8hwt8Ah>(aS>P+J{^QCG;s=f;MN(;h7rqCHK}0|O3HFP6BdN4Ccu`_ zc^i7IRdpL4-vgmF;EPuQ@3jY-dwxuMDf*UvwrsHMgo2n66|RYBNqIC5bk%`*Kq_rV zC0cEinXSeGr}+S})smF0g(+PoS2z54;DFpQ$y}z_waZiSea|9#cQ*rmI0+Cuku|d2 z{5Dyc)PG;$O2J(VVOLROvh;mfL$PyXU^Y7fg-V8+vFy*4MS#G=jkHPuzLf+GgW7kz zaBSwVW`WX&sOmnt-PMfS%HKTChhu^^l@xkRY5f<=d~TrwZo3!4?HLESiwwI!T>K}6 zI?y9b$$S?yryZrf&p7pvLlHFMlz$LqfuMh}qGm9??YKIthq>p-VX~U9z3Yi$MaBgD z_{o{xb7J^yQ1UFcrHQnMM6*eExTJF%z3HK~EihEjYdFP!^9iAfzET5%TSPQjA& z>VHj={O^2+V&(V_in_^DQSgtGTl3m2xTvs-15maweI(58<%D&c41GAcp{~aqZLgfF zjtX(7t8Ni@#4Ylxybh@U4zZ!Sc4%3#I!W7D@|i>Cq%)Od_s6SsO|$Kw0L!-3pBP>zM4I}3JNpJD7BCP0a0O&1u(x5NxLisTqfKpz?k6X3VWsjY$uYf)gi5~{WB;kTeJj@@pT}tMPNo%1%kB~wXXA8< zzHM2WdLdO zHIuB|Eacg03Tp8Oi;Y~fCgb<()|fjT6j&CAB!A}7OC5Ff()9Ln%hAuMUznPYsUpa| zL76MSX2MlqEve7i84Ut!zI)>bCb#Z!|KX2M^OaTJ)Dzg6YUnTpcg+iQv+ICyi+CaN zbBq{znEibxO&`D4@DoKQ@;x*S@W^{}~fX z9C0m>BW(;kA>UhQ>0j#i&-l1icxvWWe^UEgMuXQ(g;>~%`gnB>1j7gA1etT-x>oS; zO}IZuWhvUr6yp+E@&l|vBQWmAR6QW>qWTZ#Sy~uXKXHmx??1zWPUZ}uZ~efsMc38( zAD7wvfkig1`jZj(SYL4Kr*CB4?Ylu7xuRhX>|hFC4t~ELPHT!77n<_TM%Ndhfh$uS z^{H13?JS)H1#^_>)q0&IP%$-W`1A0Sv)p_p1OJCWWTIW=ho99+d<1PJ?0vjsjHxSd zVSw2Nm%V~kvkHmOfw~RoqzMmDXk}@^7X4q3SK5Mx7vMPizBw9k=l>#4fi2HunU^2b zU%QPQ%`dTa?k$wXdY@rlw!B}jwo{s^L(#c9fLb+2K|h2b-|2UyxN5X_wdXj)ThMB% z0}9hG`5PJXwy^E=KCEMY)Z#YKyz!`0A$9g|kUYw!EK(-uZCoas`pp*h8{p$l#hy}L zefhR?u|&nDy3IPeog#pwYO1t07j2$4?<);h6i)CX{4R&`9>yX35?q8O$5*@Q7D85S zEK?F3s!>rQWAJ5ykM|$)Y9*9nK|RB0gL_#hgZuh_J9|Yl6$FWlMkNJfHx^AKG|hY7 z`Htb8fQQ7HZc}a=bsZ^P-P_9a-tydgtG{JaDhS;D5uJJRk0m~teW&B%I^t0qR%zf9x$ zGd{UhqWfFWiwQc73_L1}L^rZM9%Dw7Y9ozA`*5ID5yHcry<(}fHt{ETNDeoJFG2