diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index 95f61422cb..af8c53cb33 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -121,7 +121,7 @@ void WindowImpl::BringToFront() { if(Window != nullptr) { - if (![Window isMiniaturized]) + if ([Window isVisible] && ![Window isMiniaturized]) { if(IsDialog()) { diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 9e180b12c5..1dab74dc9e 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -99,6 +99,7 @@ namespace IntegrationTestApp foreach (var window in lifetime.Windows) { + window.Show(); if (window.WindowState == WindowState.Minimized) window.WindowState = WindowState.Normal; } diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml b/samples/IntegrationTestApp/ShowWindowTest.axaml index 40c1642e91..4001bac7e2 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml @@ -3,7 +3,7 @@ x:Class="IntegrationTestApp.ShowWindowTest" Name="SecondaryWindow" Title="Show Window Test"> - + @@ -31,5 +31,6 @@ Maximized Fullscreen + diff --git a/samples/RenderDemo/Pages/CustomSkiaPage.cs b/samples/RenderDemo/Pages/CustomSkiaPage.cs index 9c524a7932..bf27747154 100644 --- a/samples/RenderDemo/Pages/CustomSkiaPage.cs +++ b/samples/RenderDemo/Pages/CustomSkiaPage.cs @@ -40,14 +40,16 @@ namespace RenderDemo.Pages static Stopwatch St = Stopwatch.StartNew(); public void Render(IDrawingContextImpl context) { - var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas; - if (canvas == null) + var leaseFeature = context.GetFeature(); + if (leaseFeature == null) using (var c = new DrawingContext(context, false)) { c.DrawText(_noSkia, new Point()); } else { + using var lease = leaseFeature.Lease(); + var canvas = lease.SkCanvas; canvas.Save(); // create the first shader var colors = new SKColor[] { diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 0c72c389dc..ed5b46d398 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -8,7 +8,8 @@ using Avalonia.Input.Platform; using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Rendering; -using Avalonia.Skia; +using Avalonia.Rendering.Composition; +using Avalonia.OpenGL; namespace Avalonia { @@ -16,10 +17,8 @@ namespace Avalonia { public static T UseAndroid(this T builder) where T : AppBuilderBase, new() { - var options = AvaloniaLocator.Current.GetService() ?? new AndroidPlatformOptions(); - return builder - .UseWindowingSubsystem(() => AndroidPlatform.Initialize(options), "Android") + .UseWindowingSubsystem(() => AndroidPlatform.Initialize(), "Android") .UseSkia(); } } @@ -42,9 +41,11 @@ namespace Avalonia.Android public TimeSpan DoubleClickTime => TimeSpan.FromMilliseconds(500); - public static void Initialize(AndroidPlatformOptions options) + internal static Compositor Compositor { get; private set; } + + public static void Initialize() { - Options = options; + Options = AvaloniaLocator.Current.GetService() ?? new AndroidPlatformOptions(); AvaloniaLocator.CurrentMutable .Bind().ToTransient() @@ -58,16 +59,24 @@ namespace Avalonia.Android .Bind().ToConstant(new RenderLoop()) .Bind().ToSingleton(); - if (options.UseGpu) + if (Options.UseGpu) { EglPlatformOpenGlInterface.TryInitialize(); } + + if (Options.UseCompositor) + { + Compositor = new Compositor( + AvaloniaLocator.Current.GetRequiredService(), + AvaloniaLocator.Current.GetService()); + } } } public sealed class AndroidPlatformOptions { - public bool UseDeferredRendering { get; set; } = true; + public bool UseDeferredRendering { get; set; } = false; public bool UseGpu { get; set; } = true; + public bool UseCompositor { get; set; } = true; } } diff --git a/src/Android/Avalonia.Android/AndroidThreadingInterface.cs b/src/Android/Avalonia.Android/AndroidThreadingInterface.cs index e72f0aed90..42f75a27e1 100644 --- a/src/Android/Avalonia.Android/AndroidThreadingInterface.cs +++ b/src/Android/Avalonia.Android/AndroidThreadingInterface.cs @@ -14,6 +14,7 @@ namespace Avalonia.Android internal sealed class AndroidThreadingInterface : IPlatformThreadingInterface { private Handler _handler; + private static Thread s_uiThread; public AndroidThreadingInterface() { @@ -76,7 +77,25 @@ namespace Avalonia.Android EnsureInvokeOnMainThread(() => Signaled?.Invoke(null)); } - public bool CurrentThreadIsLoopThread => Looper.MainLooper.Thread.Equals(Java.Lang.Thread.CurrentThread()); + public bool CurrentThreadIsLoopThread + { + get + { + if (s_uiThread != null) + return s_uiThread == Thread.CurrentThread; + + var isOnMainThread = OperatingSystem.IsAndroidVersionAtLeast(23) + ? Looper.MainLooper.IsCurrentThread + : Looper.MainLooper.Thread.Equals(Java.Lang.Thread.CurrentThread()); + if (isOnMainThread) + { + s_uiThread = Thread.CurrentThread; + return true; + } + + return false; + } + } public event Action Signaled; } } diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index fd97e293f9..dc74214170 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -19,6 +19,7 @@ using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Rendering; +using Avalonia.Rendering.Composition; namespace Avalonia.Android.Platform.SkiaPlatform { @@ -84,9 +85,11 @@ namespace Avalonia.Android.Platform.SkiaPlatform public IEnumerable Surfaces => new object[] { _gl, _framebuffer, Handle }; public IRenderer CreateRenderer(IRenderRoot root) => - AndroidPlatform.Options.UseDeferredRendering - ? new DeferredRenderer(root, AvaloniaLocator.Current.GetService()) { RenderOnlyOnRenderThread = true } - : new ImmediateRenderer(root); + AndroidPlatform.Options.UseCompositor + ? new CompositingRenderer(root, AndroidPlatform.Compositor) + : AndroidPlatform.Options.UseDeferredRendering + ? new DeferredRenderer(root, AvaloniaLocator.Current.GetRequiredService()) { RenderOnlyOnRenderThread = true } + : new ImmediateRenderer(root); public virtual void Hide() { diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index 603bb1c1c1..e71f568207 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -228,6 +228,8 @@ namespace Avalonia.Media throw new NotImplementedException(); } + public object? GetFeature(Type t) => null; + public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) { throw new NotImplementedException(); diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index d84a509234..6aa5eeea3d 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -172,6 +172,20 @@ namespace Avalonia.Platform /// /// Custom draw operation void Custom(ICustomDrawOperation custom); + + /// + /// Attempts to get an optional feature from the drawing context implementation + /// + object? GetFeature(Type t); + } + + public static class DrawingContextImplExtensions + { + /// + /// Attempts to get an optional feature from the drawing context implementation + /// + public static T? GetFeature(this IDrawingContextImpl context) where T : class => + (T?)context.GetFeature(typeof(T)); } public interface IDrawingContextLayerImpl : IRenderTargetBitmapImpl diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs index 01216d19ed..30b57883fc 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -156,6 +156,8 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW ++_drawOperationIndex; } + public object? GetFeature(Type t) => null; + /// public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 7eb35a68ed..03859d241f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -1,3 +1,4 @@ +using System; using System.Numerics; using Avalonia.Media; using Avalonia.Media.Imaging; @@ -155,6 +156,8 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont _impl.Custom(custom); } + public object? GetFeature(Type t) => _impl.GetFeature(t); + public class VisualBrushRenderer : IVisualBrushRenderer { public CompositionDrawList? VisualBrushDrawList { get; set; } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index c20594aaca..5c1ac0312c 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -78,6 +78,13 @@ namespace Avalonia.Rendering.Composition.Server if (Root == null) return; + + if ((_renderTarget as IRenderTargetWithCorruptionInfo)?.IsCorrupted == true) + { + _renderTarget!.Dispose(); + _renderTarget = null; + } + _renderTarget ??= _renderTargetFactory(); Compositor.UpdateServerTime(); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 07082e4ac3..d6766fa9b8 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -203,6 +203,8 @@ namespace Avalonia.Rendering.SceneGraph ++_drawOperationindex; } + public object? GetFeature(Type t) => null; + /// public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { diff --git a/src/Avalonia.Controls/Converters/StringFormatConverter.cs b/src/Avalonia.Controls/Converters/StringFormatConverter.cs index ae920dac7e..82e75d9c34 100644 --- a/src/Avalonia.Controls/Converters/StringFormatConverter.cs +++ b/src/Avalonia.Controls/Converters/StringFormatConverter.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using Avalonia.Data; using Avalonia.Data.Converters; namespace Avalonia.Controls.Converters; @@ -15,13 +14,17 @@ public class StringFormatConverter : IMultiValueConverter { public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) { - try + if (values[0] is string format) { - return string.Format((string)values[0]!, values.Skip(1).ToArray()); - } - catch (Exception e) - { - return new BindingNotification(e, BindingErrorType.Error); + try + { + return string.Format(format, values.Skip(1).ToArray()); + } + catch + { + return AvaloniaProperty.UnsetValue; + } } + return AvaloniaProperty.UnsetValue; } } diff --git a/src/Avalonia.Controls/TransitioningContentControl.cs b/src/Avalonia.Controls/TransitioningContentControl.cs index 451e234653..70b21b7248 100644 --- a/src/Avalonia.Controls/TransitioningContentControl.cs +++ b/src/Avalonia.Controls/TransitioningContentControl.cs @@ -84,13 +84,19 @@ public class TransitioningContentControl : ContentControl _lastTransitionCts?.Cancel(); _lastTransitionCts = new CancellationTokenSource(); + var localToken = _lastTransitionCts.Token; if (PageTransition != null) - await PageTransition.Start(this, null, true, _lastTransitionCts.Token); + await PageTransition.Start(this, null, true, localToken); + + if (localToken.IsCancellationRequested) + { + return; + } CurrentContent = content; if (PageTransition != null) - await PageTransition.Start(null, this, true, _lastTransitionCts.Token); + await PageTransition.Start(null, this, true, localToken); } } diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 5576368240..cb23c6c336 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -416,6 +416,11 @@ namespace Avalonia.Headless } + public object GetFeature(Type t) + { + return null; + } + public void DrawLine(IPen pen, Point p1, Point p2) { } diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs index 68442c1fd3..7b65cfb595 100644 --- a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs @@ -146,6 +146,13 @@ namespace Avalonia.OpenGL.Controls return false; } + if (_context == null) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL: unable to create additional OpenGL context."); + return false; + } + GlVersion = _context.Version; try { diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index 9269dc70f8..775014d419 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -64,6 +64,12 @@ namespace Avalonia.ReactiveUI public static readonly StyledProperty ViewContractProperty = AvaloniaProperty.Register(nameof(ViewContract)); + /// + /// for the property. + /// + public static readonly StyledProperty DefaultContentProperty = + ViewModelViewHost.DefaultContentProperty.AddOwner(); + /// /// Initializes a new instance of the class. /// @@ -106,6 +112,15 @@ namespace Avalonia.ReactiveUI set => SetValue(ViewContractProperty, value); } + /// + /// Gets or sets the content displayed whenever there is no page currently routed. + /// + public object? DefaultContent + { + get => GetValue(DefaultContentProperty); + set => SetValue(DefaultContentProperty, value); + } + /// /// Gets or sets the ReactiveUI view locator used by this router. /// diff --git a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs b/src/Avalonia.ReactiveUI/TransitioningContentControl.cs deleted file mode 100644 index d26e90b2da..0000000000 --- a/src/Avalonia.ReactiveUI/TransitioningContentControl.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Threading; - -using Avalonia.Animation; -using Avalonia.Controls; -using Avalonia.Styling; - -namespace Avalonia.ReactiveUI -{ - /// - /// A ContentControl that animates the transition when its content is changed. - /// - [Obsolete("Use TransitioningContentControl in Avalonia.Controls namespace")] - public class TransitioningContentControl : ContentControl, IStyleable - { - /// - /// for the property. - /// - public static readonly StyledProperty PageTransitionProperty = - AvaloniaProperty.Register(nameof(PageTransition), - new CrossFade(TimeSpan.FromSeconds(0.5))); - - /// - /// for the property. - /// - public static readonly StyledProperty DefaultContentProperty = - AvaloniaProperty.Register(nameof(DefaultContent)); - - private CancellationTokenSource? _lastTransitionCts; - - /// - /// Gets or sets the animation played when content appears and disappears. - /// - public IPageTransition? PageTransition - { - get => GetValue(PageTransitionProperty); - set => SetValue(PageTransitionProperty, value); - } - - /// - /// Gets or sets the content displayed whenever there is no page currently routed. - /// - public object? DefaultContent - { - get => GetValue(DefaultContentProperty); - set => SetValue(DefaultContentProperty, value); - } - - /// - /// Gets or sets the content with animation. - /// - public new object? Content - { - get => base.Content; - set => UpdateContentWithTransition(value); - } - - /// - /// TransitioningContentControl uses the default ContentControl - /// template from Avalonia default theme. - /// - Type IStyleable.StyleKey => typeof(ContentControl); - - /// - /// Updates the content with transitions. - /// - /// New content to set. - private async void UpdateContentWithTransition(object? content) - { - _lastTransitionCts?.Cancel(); - _lastTransitionCts = new CancellationTokenSource(); - - if (PageTransition != null) - await PageTransition.Start(this, null, true, _lastTransitionCts.Token); - base.Content = content; - if (PageTransition != null) - await PageTransition.Start(null, this, true, _lastTransitionCts.Token); - } - } -} diff --git a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs index 16dee00ebc..869238b377 100644 --- a/src/Avalonia.ReactiveUI/ViewModelViewHost.cs +++ b/src/Avalonia.ReactiveUI/ViewModelViewHost.cs @@ -1,5 +1,8 @@ using System; using System.Reactive.Disposables; + +using Avalonia.Controls; + using ReactiveUI; using Splat; @@ -24,6 +27,12 @@ namespace Avalonia.ReactiveUI public static readonly StyledProperty ViewContractProperty = AvaloniaProperty.Register(nameof(ViewContract)); + /// + /// for the property. + /// + public static readonly StyledProperty DefaultContentProperty = + AvaloniaProperty.Register(nameof(DefaultContent)); + /// /// Initializes a new instance of the class. /// @@ -55,6 +64,15 @@ namespace Avalonia.ReactiveUI set => SetValue(ViewContractProperty, value); } + /// + /// Gets or sets the content displayed whenever there is no page currently routed. + /// + public object? DefaultContent + { + get => GetValue(DefaultContentProperty); + set => SetValue(DefaultContentProperty, value); + } + /// /// Gets or sets the view locator. /// diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index add97a660b..d462a2210e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -39,6 +39,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions targetType = setter.Property.PropertyType; } + var previousWasControlTheme = false; + // Look upwards though the ambient context for IResourceNodes // which might be able to give us the resource. foreach (var parent in stack.Parents) @@ -47,6 +49,21 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions { return ColorToBrushConverter.Convert(value, targetType); } + + // HACK: Temporary fix for #8678. Hard-coded to only work for the DevTools main + // window as we don't want 3rd parties to start relying on this hack. + // + // We need to implement compile-time merging of resource dictionaries and this + // hack can be removed. + if (previousWasControlTheme && + parent is ResourceDictionary hack && + hack.Owner?.GetType().FullName == "Avalonia.Diagnostics.Views.MainWindow" && + hack.Owner.TryGetResource(ResourceKey, out value)) + { + return ColorToBrushConverter.Convert(value, targetType); + } + + previousWasControlTheme = parent is ControlTheme; } if (provideTarget.TargetObject is IControl target && @@ -69,3 +86,4 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions } } } + diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 8293769138..99ab60d1ac 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -10,6 +10,7 @@ using Avalonia.Rendering.SceneGraph; using Avalonia.Rendering.Utilities; using Avalonia.Utilities; using Avalonia.Media.Imaging; +using JetBrains.Annotations; using SkiaSharp; namespace Avalonia.Skia @@ -17,7 +18,7 @@ namespace Avalonia.Skia /// /// Skia based drawing context. /// - internal class DrawingContextImpl : IDrawingContextImpl, ISkiaDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport + internal class DrawingContextImpl : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport { private IDisposable[] _disposables; private readonly Vector _dpi; @@ -38,7 +39,8 @@ namespace Avalonia.Skia private readonly SKPaint _fillPaint = new SKPaint(); private readonly SKPaint _boxShadowPaint = new SKPaint(); private static SKShader s_acrylicNoiseShader; - private readonly ISkiaGpuRenderSession _session; + private readonly ISkiaGpuRenderSession _session; + private bool _leased = false; /// /// Context create info. @@ -83,6 +85,47 @@ namespace Avalonia.Skia public ISkiaGpuRenderSession CurrentSession; } + class SkiaLeaseFeature : ISkiaSharpApiLeaseFeature + { + private readonly DrawingContextImpl _context; + + public SkiaLeaseFeature(DrawingContextImpl context) + { + _context = context; + } + + public ISkiaSharpApiLease Lease() + { + _context.CheckLease(); + return new ApiLease(_context); + } + + class ApiLease : ISkiaSharpApiLease + { + private DrawingContextImpl _context; + private readonly SKMatrix _revertTransform; + + public ApiLease(DrawingContextImpl context) + { + _revertTransform = context.Canvas.TotalMatrix; + _context = context; + _context._leased = true; + } + + public SKCanvas SkCanvas => _context.Canvas; + public GRContext GrContext => _context.GrContext; + public SKSurface SkSurface => _context.Surface; + public double CurrentOpacity => _context._currentOpacity; + + public void Dispose() + { + _context.Canvas.SetMatrix(_revertTransform); + _context._leased = false; + _context = null; + } + } + } + /// /// Create new drawing context. /// @@ -123,20 +166,23 @@ namespace Avalonia.Skia public SKCanvas Canvas { get; } public SKSurface Surface { get; } - SKCanvas ISkiaDrawingContextImpl.SkCanvas => Canvas; - SKSurface ISkiaDrawingContextImpl.SkSurface => Surface; - GRContext ISkiaDrawingContextImpl.GrContext => _grContext; - double ISkiaDrawingContextImpl.CurrentOpacity => _currentOpacity; - + private void CheckLease() + { + if (_leased) + throw new InvalidOperationException("The underlying graphics API is currently leased"); + } + /// public void Clear(Color color) { + CheckLease(); Canvas.Clear(color.ToSKColor()); } /// public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) { + CheckLease(); var drawableImage = (IDrawableBitmapImpl)source.Item; var s = sourceRect.ToSKRect(); var d = destRect.ToSKRect(); @@ -157,6 +203,7 @@ namespace Avalonia.Skia /// public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { + CheckLease(); PushOpacityMask(opacityMask, opacityMaskRect); DrawBitmap(source, 1, new Rect(0, 0, source.Item.PixelSize.Width, source.Item.PixelSize.Height), destRect, BitmapInterpolationMode.Default); PopOpacityMask(); @@ -165,6 +212,7 @@ namespace Avalonia.Skia /// public void DrawLine(IPen pen, Point p1, Point p2) { + CheckLease(); using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) { if (paint.Paint is object) @@ -177,6 +225,7 @@ namespace Avalonia.Skia /// public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) { + CheckLease(); var impl = (GeometryImpl) geometry; var size = geometry.Bounds.Size; @@ -260,6 +309,7 @@ namespace Avalonia.Skia { if (rect.Rect.Height <= 0 || rect.Rect.Width <= 0) return; + CheckLease(); var rc = rect.Rect.ToSKRect(); var isRounded = rect.IsRounded; @@ -296,6 +346,7 @@ namespace Avalonia.Skia { if (rect.Rect.Height <= 0 || rect.Rect.Width <= 0) return; + CheckLease(); // Arbitrary chosen values // On OSX Skia breaks OpenGL context when asked to draw, e. g. (0, 0, 623, 6666600) rect if (rect.Rect.Height > 8192 || rect.Rect.Width > 8192) @@ -421,7 +472,8 @@ namespace Avalonia.Skia { if (rect.Height <= 0 || rect.Width <= 0) return; - + CheckLease(); + var rc = rect.ToSKRect(); if (brush != null) @@ -447,6 +499,7 @@ namespace Avalonia.Skia /// public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) { + CheckLease(); using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Size)) { var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; @@ -459,18 +512,21 @@ namespace Avalonia.Skia /// public IDrawingContextLayerImpl CreateLayer(Size size) { + CheckLease(); return CreateRenderTarget(size, true); } /// public void PushClip(Rect clip) { + CheckLease(); Canvas.Save(); Canvas.ClipRect(clip.ToSKRect()); } public void PushClip(RoundedRect clip) { + CheckLease(); Canvas.Save(); Canvas.ClipRoundRect(clip.ToSKRoundRect(), antialias:true); } @@ -478,12 +534,14 @@ namespace Avalonia.Skia /// public void PopClip() { + CheckLease(); Canvas.Restore(); } /// public void PushOpacity(double opacity) { + CheckLease(); _opacityStack.Push(_currentOpacity); _currentOpacity *= opacity; } @@ -491,6 +549,7 @@ namespace Avalonia.Skia /// public void PopOpacity() { + CheckLease(); _currentOpacity = _opacityStack.Pop(); } @@ -499,6 +558,7 @@ namespace Avalonia.Skia { if(_disposed) return; + CheckLease(); try { if (_grContext != null) @@ -523,6 +583,7 @@ namespace Avalonia.Skia /// public void PushGeometryClip(IGeometryImpl clip) { + CheckLease(); Canvas.Save(); Canvas.ClipPath(((GeometryImpl)clip).EffectivePath, SKClipOperation.Intersect, true); } @@ -530,12 +591,14 @@ namespace Avalonia.Skia /// public void PopGeometryClip() { + CheckLease(); Canvas.Restore(); } /// public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) { + CheckLease(); _blendingModeStack.Push(_currentBlendingMode); _currentBlendingMode = blendingMode; } @@ -543,14 +606,20 @@ namespace Avalonia.Skia /// public void PopBitmapBlendMode() { + CheckLease(); _currentBlendingMode = _blendingModeStack.Pop(); } - public void Custom(ICustomDrawOperation custom) => custom.Render(this); + public void Custom(ICustomDrawOperation custom) + { + CheckLease(); + custom.Render(this); + } /// public void PushOpacityMask(IBrush mask, Rect bounds) { + CheckLease(); // TODO: This should be disposed var paint = new SKPaint(); @@ -561,6 +630,7 @@ namespace Avalonia.Skia /// public void PopOpacityMask() { + CheckLease(); using (var paint = new SKPaint { BlendMode = SKBlendMode.DstIn }) { Canvas.SaveLayer(paint); @@ -580,6 +650,7 @@ namespace Avalonia.Skia get { return _currentTransform; } set { + CheckLease(); if (_currentTransform == value) return; @@ -596,6 +667,14 @@ namespace Avalonia.Skia } } + [CanBeNull] + public object GetFeature(Type t) + { + if (t == typeof(ISkiaSharpApiLeaseFeature)) + return new SkiaLeaseFeature(this); + return null; + } + /// /// Configure paint wrapper for using gradient brush. /// diff --git a/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs index d0b45b7c5d..a078c364a2 100644 --- a/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs @@ -33,7 +33,7 @@ namespace Avalonia.Skia.Helpers /// Unsupported - Wraps a GPU Backed SkiaSurface in an Avalonia DrawingContext. /// [Obsolete] - public static ISkiaDrawingContextImpl WrapSkiaSurface(this SKSurface surface, GRContext grContext, Vector dpi, params IDisposable[] disposables) + public static IDrawingContextImpl WrapSkiaSurface(this SKSurface surface, GRContext grContext, Vector dpi, params IDisposable[] disposables) { var createInfo = new DrawingContextImpl.CreateInfo { @@ -50,7 +50,7 @@ namespace Avalonia.Skia.Helpers /// Unsupported - Wraps a non-GPU Backed SkiaSurface in an Avalonia DrawingContext. /// [Obsolete] - public static ISkiaDrawingContextImpl WrapSkiaSurface(this SKSurface surface, Vector dpi, params IDisposable[] disposables) + public static IDrawingContextImpl WrapSkiaSurface(this SKSurface surface, Vector dpi, params IDisposable[] disposables) { var createInfo = new DrawingContextImpl.CreateInfo { @@ -63,7 +63,7 @@ namespace Avalonia.Skia.Helpers } [Obsolete] - public static ISkiaDrawingContextImpl CreateDrawingContext(Size size, Vector dpi, GRContext grContext = null) + public static IDrawingContextImpl CreateDrawingContext(Size size, Vector dpi, GRContext grContext = null) { if (grContext is null) { @@ -90,9 +90,11 @@ namespace Avalonia.Skia.Helpers } [Obsolete] - public static void DrawTo(this ISkiaDrawingContextImpl source, ISkiaDrawingContextImpl destination, SKPaint paint = null) + public static void DrawTo(this IDrawingContextImpl source, IDrawingContextImpl destination, SKPaint paint = null) { - destination.SkCanvas.DrawSurface(source.SkSurface, new SKPoint(0, 0), paint); + var src = (DrawingContextImpl)source; + var dst = (DrawingContextImpl)destination; + dst.Canvas.DrawSurface(src.Surface, new SKPoint(0, 0), paint); } } } diff --git a/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs b/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs deleted file mode 100644 index 1b60154d46..0000000000 --- a/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Avalonia.Metadata; -using Avalonia.Platform; -using SkiaSharp; - -namespace Avalonia.Skia -{ - [Unstable] - public interface ISkiaDrawingContextImpl : IDrawingContextImpl - { - SKCanvas SkCanvas { get; } - GRContext GrContext { get; } - SKSurface SkSurface { get; } - double CurrentOpacity { get; } - } -} diff --git a/src/Skia/Avalonia.Skia/ISkiaSharpApiLeaseFeature.cs b/src/Skia/Avalonia.Skia/ISkiaSharpApiLeaseFeature.cs new file mode 100644 index 0000000000..b3966c0324 --- /dev/null +++ b/src/Skia/Avalonia.Skia/ISkiaSharpApiLeaseFeature.cs @@ -0,0 +1,20 @@ +using System; +using Avalonia.Metadata; +using SkiaSharp; + +namespace Avalonia.Skia; + +[Unstable] +public interface ISkiaSharpApiLeaseFeature +{ + public ISkiaSharpApiLease Lease(); +} + +[Unstable] +public interface ISkiaSharpApiLease : IDisposable +{ + SKCanvas SkCanvas { get; } + GRContext GrContext { get; } + SKSurface SkSurface { get; } + double CurrentOpacity { get; } +} \ No newline at end of file diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index a7f1d9c3e5..180ae491b3 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -614,5 +614,6 @@ namespace Avalonia.Direct2D1.Media } public void Custom(ICustomDrawOperation custom) => custom.Render(this); + public object GetFeature(Type t) => null; } } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 6d5cba9946..0f243fcf9f 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -285,11 +285,12 @@ namespace Avalonia.Win32 set { - if (IsWindowVisible(_hwnd)) + if (IsWindowVisible(_hwnd) && _lastWindowState != value) { ShowWindow(value, value != WindowState.Minimized); // If the window is minimized, it shouldn't be activated } + _lastWindowState = value; _showWindowState = value; } } diff --git a/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs index 6fb2a5ac24..8436881122 100644 --- a/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs +++ b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs @@ -1,4 +1,5 @@ -using Avalonia.Media; +using System; +using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; @@ -99,5 +100,7 @@ namespace Avalonia.Benchmarks public void Custom(ICustomDrawOperation custom) { } + + public object GetFeature(Type t) => null; } } diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index 2b10c302bc..382306ac83 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -1,7 +1,9 @@ using System; +using System.Linq; using System.Runtime.InteropServices; using System.Threading; using Avalonia.Controls; +using OpenQA.Selenium; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Interactions; using Xunit; @@ -55,6 +57,43 @@ namespace Avalonia.IntegrationTests.Appium } } } + + [PlatformFact(TestPlatforms.Windows)] + public void OnWindows_Docked_Windows_Retain_Size_Position_When_Restored() + { + using (OpenWindow(new Size(400, 400), ShowWindowMode.NonOwned, WindowStartupLocation.Manual)) + { + var windowState = _session.FindElementByAccessibilityId("WindowState"); + + Assert.Equal("Normal", windowState.GetComboBoxValue()); + + + var window = _session.FindElements(By.XPath("//Window")).First(); + + new Actions(_session) + .KeyDown(Keys.Meta) + .SendKeys(Keys.Left) + .KeyUp(Keys.Meta) + .Perform(); + + var original = GetWindowInfo(); + + windowState.Click(); + _session.FindElementByName("Minimized").SendClick(); + + new Actions(_session) + .KeyDown(Keys.Alt) + .SendKeys(Keys.Tab) + .KeyUp(Keys.Alt) + .Perform(); + + var current = GetWindowInfo(); + + Assert.Equal(original.Position, current.Position); + Assert.Equal(original.FrameSize, current.FrameSize); + + } + } [Theory] @@ -92,7 +131,8 @@ namespace Avalonia.IntegrationTests.Appium Assert.True(clientSize.Width >= current.ScreenRect.Width); Assert.True(clientSize.Height >= current.ScreenRect.Height); - windowState.Click(); + windowState.SendClick(); + _session.FindElementByName("Normal").SendClick(); current = GetWindowInfo(); diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index ecbdd5bade..4e5344dd25 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -211,6 +211,35 @@ namespace Avalonia.IntegrationTests.Appium } } + [PlatformFact(TestPlatforms.MacOS)] + public void Hidden_Child_Window_Is_Not_Reshown_When_Parent_Clicked() + { + var mainWindow = _session.FindElementByAccessibilityId("MainWindow"); + + // We don't use dispose to close the window here, because it seems that hiding and re-showing a window + // causes Appium to think it's a different window. + OpenWindow(null, ShowWindowMode.Owned, WindowStartupLocation.Manual); + + var secondaryWindow = FindWindow(_session, "SecondaryWindow"); + var hideButton = secondaryWindow.FindElementByAccessibilityId("HideButton"); + + hideButton.Click(); + + var windows = _session.FindElementsByXPath("XCUIElementTypeWindow"); + Assert.Single(windows); + + mainWindow.Click(); + + windows = _session.FindElementsByXPath("XCUIElementTypeWindow"); + Assert.Single(windows); + + _session.FindElementByAccessibilityId("RestoreAll").Click(); + + // Close the window manually. + secondaryWindow = FindWindow(_session, "SecondaryWindow"); + secondaryWindow.GetChromeButtons().close.Click(); + } + private IDisposable OpenWindow(PixelSize? size, ShowWindowMode mode, WindowStartupLocation location) { var sizeTextBox = _session.FindElementByAccessibilityId("ShowWindowSize"); diff --git a/tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs index 6ca617d2d4..daa3830b7d 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/TransitioningContentControlTest.cs @@ -12,14 +12,6 @@ namespace Avalonia.ReactiveUI.UnitTests { public class TransitioningContentControlTest { - [Fact] - public void Transitioning_Control_Should_Derive_Template_From_Content_Control() - { - var target = new TransitioningContentControl(); - var stylable = (IStyledElement)target; - Assert.Equal(typeof(ContentControl),stylable.StyleKey); - } - [Fact] public void Transitioning_Control_Template_Should_Be_Instantiated() {