From a2c736de46b9fb93b1d6e0482d21c0b8e0310902 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 27 Sep 2020 14:39:32 +0300 Subject: [PATCH 01/54] 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/54] 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/54] 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/54] 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/54] 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 c02d97cf84f2f2516114540f5aa8f4d281558094 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 29 Sep 2020 14:42:38 +0300 Subject: [PATCH 06/54] 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 07/54] 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 08/54] 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 09/54] [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 4ca07974af61aa0e363aef8570433a9f7124eafc Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 1 Oct 2020 11:06:32 +0100 Subject: [PATCH 10/54] 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 3fd7892cc231e488163f10709d230ae266097dde Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 2 Oct 2020 09:33:18 +0100 Subject: [PATCH 11/54] 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 12/54] 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 13/54] 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 14/54] 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 05a2fe3d7d468b454ce5f9232e23ca70ca9dd76f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 2 Oct 2020 21:44:45 +0300 Subject: [PATCH 15/54] 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 16/54] 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 17/54] 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 18/54] 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 19/54] 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 20/54] 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 21/54] 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 22/54] 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 23/54] 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 12d8daefaf3e55267ffed30bf4bf606cd940ed45 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 5 Oct 2020 18:29:07 +0200 Subject: [PATCH 24/54] Added nullable annotations. To `ScrollContentPresenter` and `IScrollAnchorProvider`. --- .../IScrollAnchorProvider.cs | 6 ++-- .../Presenters/ScrollContentPresenter.cs | 31 ++++++++----------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Controls/IScrollAnchorProvider.cs b/src/Avalonia.Controls/IScrollAnchorProvider.cs index 93f3a0abb8..7ba02e99ea 100644 --- a/src/Avalonia.Controls/IScrollAnchorProvider.cs +++ b/src/Avalonia.Controls/IScrollAnchorProvider.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Controls +#nullable enable + +namespace Avalonia.Controls { /// /// Specifies a contract for a scrolling control that supports scroll anchoring. @@ -8,7 +10,7 @@ /// /// The currently chosen anchor element to use for scroll anchoring. /// - IControl CurrentAnchor { get; } + IControl? CurrentAnchor { get; } /// /// Registers a control as a potential scroll anchor candidate. diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 5fcb14c858..2a04558fc8 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -7,6 +7,8 @@ using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.VisualTree; +#nullable enable + namespace Avalonia.Controls.Presenters { /// @@ -64,11 +66,11 @@ namespace Avalonia.Controls.Presenters private bool _arranging; private Size _extent; private Vector _offset; - private IDisposable _logicalScrollSubscription; + private IDisposable? _logicalScrollSubscription; private Size _viewport; - private Dictionary _activeLogicalGestureScrolls; - private List _anchorCandidates; - private (IControl control, Rect bounds) _anchor; + private Dictionary? _activeLogicalGestureScrolls; + private List? _anchorCandidates; + private (IControl? control, Rect bounds) _anchor; /// /// Initializes static members of the class. @@ -90,8 +92,6 @@ namespace Avalonia.Controls.Presenters this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription); } - internal event EventHandler PreArrange; - /// /// Gets or sets a value indicating whether the content can be scrolled horizontally. /// @@ -138,7 +138,7 @@ namespace Avalonia.Controls.Presenters } /// - IControl IScrollAnchorProvider.CurrentAnchor => _anchor.control; + IControl? IScrollAnchorProvider.CurrentAnchor => _anchor.control; /// /// Attempts to bring a portion of the target visual into view by scrolling the content. @@ -247,11 +247,6 @@ namespace Avalonia.Controls.Presenters /// protected override Size ArrangeOverride(Size finalSize) { - PreArrange?.Invoke(this, new VectorEventArgs - { - Vector = new Vector(finalSize.Width, finalSize.Height), - }); - if (_logicalScrollSubscription != null || Child == null) { return base.ArrangeOverride(finalSize); @@ -350,7 +345,7 @@ namespace Avalonia.Controls.Presenters { var logicalUnits = delta.Y / LogicalScrollItemSize; delta = delta.WithY(delta.Y - logicalUnits * LogicalScrollItemSize); - dy = logicalUnits * scrollable.ScrollSize.Height; + dy = logicalUnits * scrollable!.ScrollSize.Height; } else dy = delta.Y; @@ -368,7 +363,7 @@ namespace Avalonia.Controls.Presenters { var logicalUnits = delta.X / LogicalScrollItemSize; delta = delta.WithX(delta.X - logicalUnits * LogicalScrollItemSize); - dx = logicalUnits * scrollable.ScrollSize.Width; + dx = logicalUnits * scrollable!.ScrollSize.Width; } else dx = delta.X; @@ -405,7 +400,7 @@ namespace Avalonia.Controls.Presenters if (Extent.Height > Viewport.Height) { - double height = isLogical ? scrollable.ScrollSize.Height : 50; + double height = isLogical ? scrollable!.ScrollSize.Height : 50; y += -e.Delta.Y * height; y = Math.Max(y, 0); y = Math.Min(y, Extent.Height - Viewport.Height); @@ -413,7 +408,7 @@ namespace Avalonia.Controls.Presenters if (Extent.Width > Viewport.Width) { - double width = isLogical ? scrollable.ScrollSize.Width : 50; + double width = isLogical ? scrollable!.ScrollSize.Width : 50; x += -e.Delta.X * width; x = Math.Max(x, 0); x = Math.Min(x, Extent.Width - Viewport.Width); @@ -441,7 +436,7 @@ namespace Avalonia.Controls.Presenters private void ChildChanged(AvaloniaPropertyChangedEventArgs e) { - UpdateScrollableSubscription((IControl)e.NewValue); + UpdateScrollableSubscription((IControl?)e.NewValue); if (e.OldValue != null) { @@ -449,7 +444,7 @@ namespace Avalonia.Controls.Presenters } } - private void UpdateScrollableSubscription(IControl child) + private void UpdateScrollableSubscription(IControl? child) { var scrollable = child as ILogicalScrollable; From c941a3d1b99839ad517178f6e88235d2ec4d0cd3 Mon Sep 17 00:00:00 2001 From: amwx Date: Mon, 5 Oct 2020 15:18:26 -0500 Subject: [PATCH 25/54] 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 ce3c8316b5cd90db1e293e7cb4bf4a97cae8c823 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Oct 2020 10:46:15 +0200 Subject: [PATCH 26/54] Don't invalidate ItemsRepeater measure on children change. --- src/Avalonia.Controls/Panel.cs | 5 +++++ src/Avalonia.Controls/Repeater/ItemsRepeater.cs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index ccb92dc497..b7eeb065da 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -137,6 +137,11 @@ namespace Avalonia.Controls throw new NotSupportedException(); } + InvalidateMeasureOnChildrenChanged(); + } + + private protected virtual void InvalidateMeasureOnChildrenChanged() + { InvalidateMeasure(); } diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 40f1b8dbb9..9ed09df7db 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -267,6 +267,11 @@ namespace Avalonia.Controls return result; } + private protected override void InvalidateMeasureOnChildrenChanged() + { + // Don't invalidate measure when children change. + } + protected override Size MeasureOverride(Size availableSize) { if (_isLayoutInProgress) From e9c990d9082bd9b43d014b61612ba1baf3ab9b52 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Oct 2020 11:23:21 +0200 Subject: [PATCH 27/54] Tweak ScrollContentPresenter anchoring. Using WinUI's `ScrollPresenter` implementation to guide us. --- .../Presenters/ScrollContentPresenter.cs | 110 +++++++++++------- 1 file changed, 68 insertions(+), 42 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index 2a04558fc8..b0b52812b9 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -16,6 +16,8 @@ namespace Avalonia.Controls.Presenters /// public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable, IScrollAnchorProvider { + private const double EdgeDetectionTolerance = 0.1; + /// /// Defines the property. /// @@ -70,7 +72,9 @@ namespace Avalonia.Controls.Presenters private Size _viewport; private Dictionary? _activeLogicalGestureScrolls; private List? _anchorCandidates; - private (IControl? control, Rect bounds) _anchor; + private IControl? _anchorElement; + private Rect _anchorElementBounds; + private bool _isAnchorElementDirty; /// /// Initializes static members of the class. @@ -138,7 +142,14 @@ namespace Avalonia.Controls.Presenters } /// - IControl? IScrollAnchorProvider.CurrentAnchor => _anchor.control; + IControl? IScrollAnchorProvider.CurrentAnchor + { + get + { + EnsureAnchorElementSelection(); + return _anchorElement; + } + } /// /// Attempts to bring a portion of the target visual into view by scrolling the content. @@ -215,16 +226,18 @@ namespace Avalonia.Controls.Presenters _anchorCandidates ??= new List(); _anchorCandidates.Add(element); + _isAnchorElementDirty = true; } /// void IScrollAnchorProvider.UnregisterAnchorCandidate(IControl element) { _anchorCandidates?.Remove(element); + _isAnchorElementDirty = true; - if (_anchor.control == element) + if (_anchorElement == element) { - _anchor = default; + _anchorElement = null; } } @@ -266,59 +279,69 @@ namespace Avalonia.Controls.Presenters // If we have an anchor and its position relative to Child has changed during the // arrange then that change wasn't just due to scrolling (as scrolling doesn't adjust // relative positions within Child). - if (_anchor.control != null && - TranslateBounds(_anchor.control, Child, out var updatedBounds) && - updatedBounds.Position != _anchor.bounds.Position) + if (_anchorElement != null && + TranslateBounds(_anchorElement, Child, out var updatedBounds) && + updatedBounds.Position != _anchorElementBounds.Position) { - var offset = updatedBounds.Position - _anchor.bounds.Position; + var offset = updatedBounds.Position - _anchorElementBounds.Position; return offset; } return default; } - // Calculate the new anchor element. - _anchor = CalculateCurrentAnchor(); + var isAnchoring = Offset.X >= EdgeDetectionTolerance || Offset.Y >= EdgeDetectionTolerance; - // Do the arrange. - ArrangeOverrideImpl(size, -Offset); + if (isAnchoring) + { + // Calculate the new anchor element if necessary. + EnsureAnchorElementSelection(); - // If the anchor moved during the arrange, we need to adjust the offset and do another arrange. - var anchorShift = TrackAnchor(); + // Do the arrange. + ArrangeOverrideImpl(size, -Offset); - if (anchorShift != default) - { - var newOffset = Offset + anchorShift; - var newExtent = Extent; - var maxOffset = new Vector(Extent.Width - Viewport.Width, Extent.Height - Viewport.Height); + // If the anchor moved during the arrange, we need to adjust the offset and do another arrange. + var anchorShift = TrackAnchor(); - if (newOffset.X > maxOffset.X) + if (anchorShift != default) { - newExtent = newExtent.WithWidth(newOffset.X + Viewport.Width); - } + var newOffset = Offset + anchorShift; + var newExtent = Extent; + var maxOffset = new Vector(Extent.Width - Viewport.Width, Extent.Height - Viewport.Height); - if (newOffset.Y > maxOffset.Y) - { - newExtent = newExtent.WithHeight(newOffset.Y + Viewport.Height); - } + if (newOffset.X > maxOffset.X) + { + newExtent = newExtent.WithWidth(newOffset.X + Viewport.Width); + } - Extent = newExtent; + if (newOffset.Y > maxOffset.Y) + { + newExtent = newExtent.WithHeight(newOffset.Y + Viewport.Height); + } - try - { - _arranging = true; - Offset = newOffset; - } - finally - { - _arranging = false; + Extent = newExtent; + + try + { + _arranging = true; + Offset = newOffset; + } + finally + { + _arranging = false; + } + + ArrangeOverrideImpl(size, -Offset); } - + } + else + { ArrangeOverrideImpl(size, -Offset); } Viewport = finalSize; Extent = Child.Bounds.Size.Inflate(Child.Margin); + _isAnchorElementDirty = true; return finalSize; } @@ -493,13 +516,17 @@ namespace Avalonia.Controls.Presenters } } - private (IControl, Rect) CalculateCurrentAnchor() + private void EnsureAnchorElementSelection() { - if (_anchorCandidates == null) + if (!_isAnchorElementDirty || _anchorCandidates is null) { - return default; + return; } + _anchorElement = null; + _anchorElementBounds = default; + _isAnchorElementDirty = false; + var bestCandidate = default(IControl); var bestCandidateDistance = double.MaxValue; @@ -526,10 +553,9 @@ namespace Avalonia.Controls.Presenters // bounds aren't relative to the ScrollContentPresenter itself, if they change // then we know it wasn't just due to scrolling. var unscrolledBounds = TranslateBounds(bestCandidate, Child); - return (bestCandidate, unscrolledBounds); + _anchorElement = bestCandidate; + _anchorElementBounds = unscrolledBounds; } - - return default; } private bool GetViewportBounds(IControl element, out Rect bounds) From d5224d4bb0a98f55c1da1cae9eaab6a57d4b3697 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Oct 2020 11:43:56 +0200 Subject: [PATCH 28/54] Ensure valid index before calculating offset. --- src/Avalonia.Layout/StackLayout.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs index 909c7bc7eb..4a93c8344f 100644 --- a/src/Avalonia.Layout/StackLayout.cs +++ b/src/Avalonia.Layout/StackLayout.cs @@ -249,8 +249,8 @@ namespace Avalonia.Layout realizationWindowOffsetInExtent + _orientation.MajorSize(realizationRect) >= 0 && realizationWindowOffsetInExtent <= majorSize) { anchorIndex = (int) (realizationWindowOffsetInExtent / averageElementSize); - offset = anchorIndex* averageElementSize + _orientation.MajorStart(lastExtent); anchorIndex = Math.Max(0, Math.Min(itemsCount - 1, anchorIndex)); + offset = anchorIndex* averageElementSize + _orientation.MajorStart(lastExtent); } } From bba2cafcc1efe44af5e7cf286a2bedaa389cdc20 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 6 Oct 2020 16:13:21 +0200 Subject: [PATCH 29/54] Don't register anchor candidates until they're laid out. --- src/Avalonia.Controls/Repeater/ItemsRepeater.cs | 13 +++++++++++-- src/Avalonia.Controls/Repeater/ViewManager.cs | 2 +- src/Avalonia.Controls/Repeater/ViewportManager.cs | 14 ++++++++++++-- .../Repeater/VirtualizationInfo.cs | 1 + 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 9ed09df7db..fb2da09e73 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -369,6 +369,12 @@ namespace Avalonia.Controls { var newBounds = element.Bounds; virtInfo.ArrangeBounds = newBounds; + + if (!virtInfo.IsRegisteredAsAnchorCandidate) + { + _viewportManager.RegisterScrollAnchorCandidate(element); + virtInfo.IsRegisteredAsAnchorCandidate = true; + } } } @@ -520,11 +526,14 @@ namespace Avalonia.Controls return element; } - internal void OnElementPrepared(IControl element, int index) + internal void OnElementPrepared(IControl element, VirtualizationInfo virtInfo) { - _viewportManager.OnElementPrepared(element); + _viewportManager.OnElementPrepared(element, virtInfo); + if (ElementPrepared != null) { + var index = virtInfo.Index; + if (_elementPreparedArgs == null) { _elementPreparedArgs = new ItemsRepeaterElementPreparedEventArgs(element, index); diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs index 416b1e2824..cf2066b373 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -661,7 +661,7 @@ namespace Avalonia.Controls children.Add(element); } - repeater.OnElementPrepared(element, index); + repeater.OnElementPrepared(element, virtInfo); // Update realized indices _firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index); diff --git a/src/Avalonia.Controls/Repeater/ViewportManager.cs b/src/Avalonia.Controls/Repeater/ViewportManager.cs index bdb0fa3270..6e24408aa9 100644 --- a/src/Avalonia.Controls/Repeater/ViewportManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewportManager.cs @@ -240,9 +240,14 @@ namespace Avalonia.Controls } } - public void OnElementPrepared(IControl element) + public void OnElementPrepared(IControl element, VirtualizationInfo virtInfo) { - _scroller?.RegisterAnchorCandidate(element); + // WinUI registers the element as an anchor candidate here, but I feel that's in error: + // at this point the element has not yet been positioned by the arrange pass so it will + // have its previous position, meaning that when the arrange pass moves it into its new + // position, an incorrect scroll anchoring will occur. Instead signal that it's not yet + // registered as a scroll anchor candidate. + virtInfo.IsRegisteredAsAnchorCandidate = false; } public void OnElementCleared(IControl element) @@ -373,6 +378,11 @@ namespace Avalonia.Controls } } + public void RegisterScrollAnchorCandidate(IControl element) + { + _scroller?.RegisterAnchorCandidate(element); + } + private IControl GetImmediateChildOfRepeater(IControl descendant) { var targetChild = descendant; diff --git a/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs index 7a639419c1..f8cfde609e 100644 --- a/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs +++ b/src/Avalonia.Controls/Repeater/VirtualizationInfo.cs @@ -38,6 +38,7 @@ namespace Avalonia.Controls public bool IsInUniqueIdResetPool => Owner == ElementOwner.UniqueIdResetPool; public bool MustClearDataContext { get; set; } public bool KeepAlive { get; set; } + public bool IsRegisteredAsAnchorCandidate { get; set; } public ElementOwner Owner { get; private set; } = ElementOwner.ElementFactory; public string UniqueId { get; private set; } From 273f296b47d0a0e8dd39a6ccf922fa8e075f4dbd Mon Sep 17 00:00:00 2001 From: amwx Date: Tue, 6 Oct 2020 15:39:21 -0500 Subject: [PATCH 30/54] 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 f8dcb8e8ee39ce04aa0c35529eb2945f982c609f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 6 Oct 2020 21:16:41 -0400 Subject: [PATCH 31/54] 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 d47b49526c1621e2661c27c5f14713d62cdbabc9 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 6 Oct 2020 21:19:13 -0400 Subject: [PATCH 32/54] 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 34/54] 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 952c53cdff26960392bb5e65b1f07afa060853f1 Mon Sep 17 00:00:00 2001 From: amwx Date: Wed, 7 Oct 2020 23:06:29 -0500 Subject: [PATCH 35/54] 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 36/54] 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 37/54] 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 38/54] 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 39/54] 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 40/54] 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 41/54] 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 d57ad558d2dd2bc86fa2271c8ec02881e3dc40ac Mon Sep 17 00:00:00 2001 From: Will Kennedy Date: Sun, 11 Oct 2020 00:27:07 -0400 Subject: [PATCH 42/54] Fix IsChecked property differing from :checked pseudoclass --- src/Avalonia.Controls/Primitives/ToggleButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index f96ca9310d..6b2c566422 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -94,7 +94,7 @@ namespace Avalonia.Controls.Primitives set { SetAndRaise(IsCheckedProperty, ref _isChecked, value); - UpdatePseudoClasses(value); + UpdatePseudoClasses(IsChecked); } } From 64c1bf890e04385b601c516132346e4da12e8ef8 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 11 Oct 2020 15:55:21 +0300 Subject: [PATCH 43/54] 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 44/54] 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 45/54] 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 46/54] 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 50/54] 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 51/54] 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 @@ From cb6785e7007e35d1df99775fec86b9d916a545b1 Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Mon, 12 Oct 2020 14:51:10 +0200 Subject: [PATCH 52/54] Properly handle multiple line breaks --- src/Avalonia.Visuals/Media/GlyphRun.cs | 4 ++-- .../Media/TextFormatting/TextFormatterImpl.cs | 8 ------- .../Media/TextFormatting/TextLayoutTests.cs | 22 ++++++++++++++++++- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs index 66a8c1dd0c..af228ec57b 100644 --- a/src/Avalonia.Visuals/Media/GlyphRun.cs +++ b/src/Avalonia.Visuals/Media/GlyphRun.cs @@ -399,14 +399,14 @@ namespace Avalonia.Media if (characterIndex > GlyphClusters[GlyphClusters.Length - 1]) { - return _glyphClusters.End; + return _glyphClusters.Length - 1; } } else { if (characterIndex < GlyphClusters[GlyphClusters.Length - 1]) { - return _glyphClusters.End; + return _glyphClusters.Length - 1; } if (characterIndex > GlyphClusters[0]) diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index 394dae8253..6ae5258323 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -339,14 +339,6 @@ namespace Avalonia.Media.TextFormatting return true; } - //The line breaker isn't treating \n\r as a pair so we have to fix that here. - if (textRun.Text[lineBreak.PositionMeasure] == '\n' - && textRun.Text[lineBreak.PositionWrap] == '\r') - { - lineBreak = new LineBreak(lineBreak.PositionMeasure, lineBreak.PositionWrap + 1, - lineBreak.Required); - } - return true; } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 26e8ce4797..f7bc75c05d 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -417,7 +417,6 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting [Theory] [InlineData("abcde\r\n", 7)] // Carriage Return + Line Feed - [InlineData("abcde\n\r", 7)] // This isn't valid but we somehow have to support it. [InlineData("abcde\u000A", 6)] // Line Feed [InlineData("abcde\u000B", 6)] // Vertical Tab [InlineData("abcde\u000C", 6)] // Form Feed @@ -575,6 +574,27 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_Process_Multiple_NewLines_Properly() + { + using (Start()) + { + var text = "123\r\n\r\n456\r\n\r\n"; + var layout = new TextLayout( + text, + Typeface.Default, + 12.0f, + Brushes.Black); + + Assert.Equal(5, layout.TextLines.Count); + + Assert.Equal("123\r\n", layout.TextLines[0].TextRuns[0].Text); + Assert.Equal("\r\n", layout.TextLines[1].TextRuns[0].Text); + Assert.Equal("456\r\n", layout.TextLines[2].TextRuns[0].Text); + Assert.Equal("\r\n", layout.TextLines[3].TextRuns[0].Text); + } + } + [Fact] public void Should_Wrap_Min_OneCharacter_EveryLine() { From d631474ddf75f6ec5a41488aac4339fe4eb34a10 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Mon, 12 Oct 2020 15:10:06 +0300 Subject: [PATCH 53/54] add failing test for autoscroll to selected item in listbox #4855 --- .../ListBoxTests.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 2e2ccf7326..145fce4fed 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -407,6 +407,53 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(1, raised); } + [Fact] + public void Adding_And_Selecting_Item_With_AutoScrollToSelectedItem_Should_NotHide_FirstItem() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var items = new AvaloniaList(); + + var wnd = new Window() { Width = 100, Height = 100, IsVisible = true }; + + var target = new ListBox() + { + VerticalAlignment = Layout.VerticalAlignment.Top, + AutoScrollToSelectedItem = true, + Width = 50, + VirtualizationMode = ItemVirtualizationMode.Simple, + ItemTemplate = new FuncDataTemplate((c, _) => new Border() { Height = 10 }), + Items = items, + }; + wnd.Content = target; + + var lm = wnd.LayoutManager; + + lm.ExecuteInitialLayoutPass(); + + var panel = target.Presenter.Panel; + + items.Add("Item 1"); + target.Selection.Select(0); + lm.ExecuteLayoutPass(); + + Assert.Equal(1, panel.Children.Count); + + items.Add("Item 2"); + target.Selection.Select(1); + lm.ExecuteLayoutPass(); + + Assert.Equal(2, panel.Children.Count); + + //make sure we have enough space to show all items + Assert.True(panel.Bounds.Height >= panel.Children.Sum(c => c.Bounds.Height)); + + //make sure we show items and they completelly visible, not only partially + Assert.True(panel.Children[0].Bounds.Top >= 0 && panel.Children[0].Bounds.Bottom <= panel.Bounds.Height, "first item is not completelly visible!"); + Assert.True(panel.Children[1].Bounds.Top >= 0 && panel.Children[1].Bounds.Bottom <= panel.Bounds.Height, "second item is not completelly visible!"); + } + } + private FuncControlTemplate ListBoxTemplate() { return new FuncControlTemplate((parent, scope) => From b894a812d356d13da07c57ec597fd0b58ae8812c Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Mon, 12 Oct 2020 23:59:07 +0300 Subject: [PATCH 54/54] fix #4855 before try to scroll invalidate panel --- src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 3fac440c40..bdc68bee7e 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -512,6 +512,14 @@ namespace Avalonia.Controls.Presenters var generator = Owner.ItemContainerGenerator; var newOffset = -1.0; + if (!panel.IsMeasureValid && panel.PreviousMeasure.HasValue) + { + //before any kind of scrolling we need to make sure panel measure is valid + //or we risk get panel into not valid state + //we make a preemptive quick measure so scrolling is valid + panel.Measure(panel.PreviousMeasure.Value); + } + if (index >= 0 && index < ItemCount) { if (index <= FirstIndex)